Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 59 additions & 14 deletions fila/async_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,22 @@ async def intercept_unary_stream(
return await continuation(new_details, request)


_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present."""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class AsyncClient:
"""Asynchronous client for the Fila message broker.

Expand DownExpand Up@@ -162,32 +178,41 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.aio.Channel:
"""Create an async gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

interceptors: list[grpc.aio.ClientInterceptor] = []
if api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(api_key))
if self._api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(self._api_key))

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.aio.secure_channel(
return grpc.aio.secure_channel(
addr, creds, interceptors=interceptors or None
)
else:
self._channel = grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

async def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -238,6 +263,10 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
server stream closes or an error occurs. Nil message frames (keepalive
signals) are skipped automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -253,10 +282,26 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = await self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

async def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
await self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

async def _consume_iter(
self,
stream: Any,
Expand Down
73 changes: 62 additions & 11 deletions fila/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,25 @@
if TYPE_CHECKING:
from collections.abc import Iterator

_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present.

The server sets ``x-fila-leader-addr`` in trailing metadata alongside an
UNAVAILABLE status when the node is not the leader for the requested queue.
"""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class _ClientCallDetails(
grpc.ClientCallDetails, # type: ignore[misc]
Expand DownExpand Up@@ -143,28 +162,40 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.Channel:
"""Create a gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.secure_channel(addr, creds)
channel: grpc.Channel = grpc.secure_channel(addr, creds)
else:
self._channel = grpc.insecure_channel(addr)
channel = grpc.insecure_channel(addr)

if api_key is not None:
interceptor = _ApiKeyInterceptor(api_key)
self._channel = grpc.intercept_channel(self._channel, interceptor)
if self._api_key is not None:
interceptor = _ApiKeyInterceptor(self._api_key)
channel = grpc.intercept_channel(channel, interceptor)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return channel

def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -215,6 +246,10 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
server stream closes or an error occurs. Skip nil message frames
(keepalive signals) automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -230,10 +265,26 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

def _consume_iter(
self,
stream: Any,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 59 additions & 14 deletions fila/async_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,22 @@ async def intercept_unary_stream(
return await continuation(new_details, request)


_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present."""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class AsyncClient:
"""Asynchronous client for the Fila message broker.

Expand DownExpand Up@@ -162,32 +178,41 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.aio.Channel:
"""Create an async gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

interceptors: list[grpc.aio.ClientInterceptor] = []
if api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(api_key))
if self._api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(self._api_key))

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.aio.secure_channel(
return grpc.aio.secure_channel(
addr, creds, interceptors=interceptors or None
)
else:
self._channel = grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

async def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -238,6 +263,10 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
server stream closes or an error occurs. Nil message frames (keepalive
signals) are skipped automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -253,10 +282,26 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = await self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

async def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
await self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

async def _consume_iter(
self,
stream: Any,
Expand Down
73 changes: 62 additions & 11 deletions fila/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,25 @@
if TYPE_CHECKING:
from collections.abc import Iterator

_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present.

The server sets ``x-fila-leader-addr`` in trailing metadata alongside an
UNAVAILABLE status when the node is not the leader for the requested queue.
"""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class _ClientCallDetails(
grpc.ClientCallDetails, # type: ignore[misc]
Expand DownExpand Up@@ -143,28 +162,40 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.Channel:
"""Create a gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.secure_channel(addr, creds)
channel: grpc.Channel = grpc.secure_channel(addr, creds)
else:
self._channel = grpc.insecure_channel(addr)
channel = grpc.insecure_channel(addr)

if api_key is not None:
interceptor = _ApiKeyInterceptor(api_key)
self._channel = grpc.intercept_channel(self._channel, interceptor)
if self._api_key is not None:
interceptor = _ApiKeyInterceptor(self._api_key)
channel = grpc.intercept_channel(channel, interceptor)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return channel

def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -215,6 +246,10 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
server stream closes or an error occurs. Skip nil message frames
(keepalive signals) automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -230,10 +265,26 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

def _consume_iter(
self,
stream: Any,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 59 additions & 14 deletions fila/async_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,22 @@ async def intercept_unary_stream(
return await continuation(new_details, request)


_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present."""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class AsyncClient:
"""Asynchronous client for the Fila message broker.

Expand DownExpand Up@@ -162,32 +178,41 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.aio.Channel:
"""Create an async gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

interceptors: list[grpc.aio.ClientInterceptor] = []
if api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(api_key))
if self._api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(self._api_key))

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.aio.secure_channel(
return grpc.aio.secure_channel(
addr, creds, interceptors=interceptors or None
)
else:
self._channel = grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

async def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -238,6 +263,10 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
server stream closes or an error occurs. Nil message frames (keepalive
signals) are skipped automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -253,10 +282,26 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = await self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

async def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
await self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

async def _consume_iter(
self,
stream: Any,
Expand Down
73 changes: 62 additions & 11 deletions fila/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,25 @@
if TYPE_CHECKING:
from collections.abc import Iterator

_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present.

The server sets ``x-fila-leader-addr`` in trailing metadata alongside an
UNAVAILABLE status when the node is not the leader for the requested queue.
"""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class _ClientCallDetails(
grpc.ClientCallDetails, # type: ignore[misc]
Expand DownExpand Up@@ -143,28 +162,40 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.Channel:
"""Create a gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.secure_channel(addr, creds)
channel: grpc.Channel = grpc.secure_channel(addr, creds)
else:
self._channel = grpc.insecure_channel(addr)
channel = grpc.insecure_channel(addr)

if api_key is not None:
interceptor = _ApiKeyInterceptor(api_key)
self._channel = grpc.intercept_channel(self._channel, interceptor)
if self._api_key is not None:
interceptor = _ApiKeyInterceptor(self._api_key)
channel = grpc.intercept_channel(channel, interceptor)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return channel

def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -215,6 +246,10 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
server stream closes or an error occurs. Skip nil message frames
(keepalive signals) automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -230,10 +265,26 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

def _consume_iter(
self,
stream: Any,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 59 additions & 14 deletions fila/async_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,22 @@ async def intercept_unary_stream(
return await continuation(new_details, request)


_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present."""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class AsyncClient:
"""Asynchronous client for the Fila message broker.

Expand DownExpand Up@@ -162,32 +178,41 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.aio.Channel:
"""Create an async gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

interceptors: list[grpc.aio.ClientInterceptor] = []
if api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(api_key))
if self._api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(self._api_key))

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.aio.secure_channel(
return grpc.aio.secure_channel(
addr, creds, interceptors=interceptors or None
)
else:
self._channel = grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

async def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -238,6 +263,10 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
server stream closes or an error occurs. Nil message frames (keepalive
signals) are skipped automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -253,10 +282,26 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = await self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

async def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
await self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

async def _consume_iter(
self,
stream: Any,
Expand Down
73 changes: 62 additions & 11 deletions fila/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,25 @@
if TYPE_CHECKING:
from collections.abc import Iterator

_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present.

The server sets ``x-fila-leader-addr`` in trailing metadata alongside an
UNAVAILABLE status when the node is not the leader for the requested queue.
"""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class _ClientCallDetails(
grpc.ClientCallDetails, # type: ignore[misc]
Expand DownExpand Up@@ -143,28 +162,40 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.Channel:
"""Create a gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.secure_channel(addr, creds)
channel: grpc.Channel = grpc.secure_channel(addr, creds)
else:
self._channel = grpc.insecure_channel(addr)
channel = grpc.insecure_channel(addr)

if api_key is not None:
interceptor = _ApiKeyInterceptor(api_key)
self._channel = grpc.intercept_channel(self._channel, interceptor)
if self._api_key is not None:
interceptor = _ApiKeyInterceptor(self._api_key)
channel = grpc.intercept_channel(channel, interceptor)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return channel

def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -215,6 +246,10 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
server stream closes or an error occurs. Skip nil message frames
(keepalive signals) automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -230,10 +265,26 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

def _consume_iter(
self,
stream: Any,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 59 additions & 14 deletions fila/async_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,22 @@ async def intercept_unary_stream(
return await continuation(new_details, request)


_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present."""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class AsyncClient:
"""Asynchronous client for the Fila message broker.

Expand DownExpand Up@@ -162,32 +178,41 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.aio.Channel:
"""Create an async gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

interceptors: list[grpc.aio.ClientInterceptor] = []
if api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(api_key))
if self._api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(self._api_key))

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.aio.secure_channel(
return grpc.aio.secure_channel(
addr, creds, interceptors=interceptors or None
)
else:
self._channel = grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

async def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -238,6 +263,10 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
server stream closes or an error occurs. Nil message frames (keepalive
signals) are skipped automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -253,10 +282,26 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = await self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

async def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
await self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

async def _consume_iter(
self,
stream: Any,
Expand Down
73 changes: 62 additions & 11 deletions fila/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,25 @@
if TYPE_CHECKING:
from collections.abc import Iterator

_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present.

The server sets ``x-fila-leader-addr`` in trailing metadata alongside an
UNAVAILABLE status when the node is not the leader for the requested queue.
"""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class _ClientCallDetails(
grpc.ClientCallDetails, # type: ignore[misc]
Expand DownExpand Up@@ -143,28 +162,40 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.Channel:
"""Create a gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.secure_channel(addr, creds)
channel: grpc.Channel = grpc.secure_channel(addr, creds)
else:
self._channel = grpc.insecure_channel(addr)
channel = grpc.insecure_channel(addr)

if api_key is not None:
interceptor = _ApiKeyInterceptor(api_key)
self._channel = grpc.intercept_channel(self._channel, interceptor)
if self._api_key is not None:
interceptor = _ApiKeyInterceptor(self._api_key)
channel = grpc.intercept_channel(channel, interceptor)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return channel

def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -215,6 +246,10 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
server stream closes or an error occurs. Skip nil message frames
(keepalive signals) automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -230,10 +265,26 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

def _consume_iter(
self,
stream: Any,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 59 additions & 14 deletions fila/async_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,22 @@ async def intercept_unary_stream(
return await continuation(new_details, request)


_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present."""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class AsyncClient:
"""Asynchronous client for the Fila message broker.

Expand DownExpand Up@@ -162,32 +178,41 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.aio.Channel:
"""Create an async gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

interceptors: list[grpc.aio.ClientInterceptor] = []
if api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(api_key))
if self._api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(self._api_key))

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.aio.secure_channel(
return grpc.aio.secure_channel(
addr, creds, interceptors=interceptors or None
)
else:
self._channel = grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

async def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -238,6 +263,10 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
server stream closes or an error occurs. Nil message frames (keepalive
signals) are skipped automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -253,10 +282,26 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = await self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

async def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
await self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

async def _consume_iter(
self,
stream: Any,
Expand Down
73 changes: 62 additions & 11 deletions fila/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,25 @@
if TYPE_CHECKING:
from collections.abc import Iterator

_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present.

The server sets ``x-fila-leader-addr`` in trailing metadata alongside an
UNAVAILABLE status when the node is not the leader for the requested queue.
"""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class _ClientCallDetails(
grpc.ClientCallDetails, # type: ignore[misc]
Expand DownExpand Up@@ -143,28 +162,40 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.Channel:
"""Create a gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.secure_channel(addr, creds)
channel: grpc.Channel = grpc.secure_channel(addr, creds)
else:
self._channel = grpc.insecure_channel(addr)
channel = grpc.insecure_channel(addr)

if api_key is not None:
interceptor = _ApiKeyInterceptor(api_key)
self._channel = grpc.intercept_channel(self._channel, interceptor)
if self._api_key is not None:
interceptor = _ApiKeyInterceptor(self._api_key)
channel = grpc.intercept_channel(channel, interceptor)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return channel

def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -215,6 +246,10 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
server stream closes or an error occurs. Skip nil message frames
(keepalive signals) automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -230,10 +265,26 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

def _consume_iter(
self,
stream: Any,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 59 additions & 14 deletions fila/async_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,22 @@ async def intercept_unary_stream(
return await continuation(new_details, request)


_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present."""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class AsyncClient:
"""Asynchronous client for the Fila message broker.

Expand DownExpand Up@@ -162,32 +178,41 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.aio.Channel:
"""Create an async gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

interceptors: list[grpc.aio.ClientInterceptor] = []
if api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(api_key))
if self._api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(self._api_key))

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.aio.secure_channel(
return grpc.aio.secure_channel(
addr, creds, interceptors=interceptors or None
)
else:
self._channel = grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

async def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -238,6 +263,10 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
server stream closes or an error occurs. Nil message frames (keepalive
signals) are skipped automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -253,10 +282,26 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = await self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

async def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
await self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

async def _consume_iter(
self,
stream: Any,
Expand Down
73 changes: 62 additions & 11 deletions fila/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,25 @@
if TYPE_CHECKING:
from collections.abc import Iterator

_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present.

The server sets ``x-fila-leader-addr`` in trailing metadata alongside an
UNAVAILABLE status when the node is not the leader for the requested queue.
"""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class _ClientCallDetails(
grpc.ClientCallDetails, # type: ignore[misc]
Expand DownExpand Up@@ -143,28 +162,40 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.Channel:
"""Create a gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.secure_channel(addr, creds)
channel: grpc.Channel = grpc.secure_channel(addr, creds)
else:
self._channel = grpc.insecure_channel(addr)
channel = grpc.insecure_channel(addr)

if api_key is not None:
interceptor = _ApiKeyInterceptor(api_key)
self._channel = grpc.intercept_channel(self._channel, interceptor)
if self._api_key is not None:
interceptor = _ApiKeyInterceptor(self._api_key)
channel = grpc.intercept_channel(channel, interceptor)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return channel

def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -215,6 +246,10 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
server stream closes or an error occurs. Skip nil message frames
(keepalive signals) automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -230,10 +265,26 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

def _consume_iter(
self,
stream: Any,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 59 additions & 14 deletions fila/async_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,22 @@ async def intercept_unary_stream(
return await continuation(new_details, request)


_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present."""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class AsyncClient:
"""Asynchronous client for the Fila message broker.

Expand DownExpand Up@@ -162,32 +178,41 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.aio.Channel:
"""Create an async gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

interceptors: list[grpc.aio.ClientInterceptor] = []
if api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(api_key))
if self._api_key is not None:
interceptors.append(_AsyncApiKeyInterceptor(self._api_key))

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.aio.secure_channel(
return grpc.aio.secure_channel(
addr, creds, interceptors=interceptors or None
)
else:
self._channel = grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return grpc.aio.insecure_channel(
addr, interceptors=interceptors or None
)

async def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -238,6 +263,10 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
server stream closes or an error occurs. Nil message frames (keepalive
signals) are skipped automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -253,10 +282,26 @@ async def consume(self, queue: str) -> AsyncIterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = await self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

async def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
await self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

async def _consume_iter(
self,
stream: Any,
Expand Down
73 changes: 62 additions & 11 deletions fila/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,25 @@
if TYPE_CHECKING:
from collections.abc import Iterator

_LEADER_HINT_KEY = "x-fila-leader-addr"


def _extract_leader_hint(err: grpc.RpcError) -> str | None:
"""Return the leader address from trailing metadata, if present.

The server sets ``x-fila-leader-addr`` in trailing metadata alongside an
UNAVAILABLE status when the node is not the leader for the requested queue.
"""
if err.code() != grpc.StatusCode.UNAVAILABLE:
return None
trailing = err.trailing_metadata()
if trailing is None:
return None
for key, value in trailing:
if key == _LEADER_HINT_KEY:
return str(value)
return None


class _ClientCallDetails(
grpc.ClientCallDetails, # type: ignore[misc]
Expand DownExpand Up@@ -143,28 +162,40 @@ def __init__(
api_key: API key for authentication. When set, every RPC includes an
``authorization: Bearer <key>`` metadata header.
"""
use_tls = tls or ca_cert is not None
self._tls = tls
self._ca_cert = ca_cert
self._client_cert = client_cert
self._client_key = client_key
self._api_key = api_key

use_tls = tls or ca_cert is not None
if (client_cert is not None or client_key is not None) and not use_tls:
raise ValueError(
"client_cert and client_key require ca_cert or tls=True to establish a TLS channel"
)

self._channel = self._make_channel(addr)
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]

def _make_channel(self, addr: str) -> grpc.Channel:
"""Create a gRPC channel to the given address using stored credentials."""
use_tls = self._tls or self._ca_cert is not None

if use_tls:
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
root_certificates=self._ca_cert,
private_key=self._client_key,
certificate_chain=self._client_cert,
)
self._channel = grpc.secure_channel(addr, creds)
channel: grpc.Channel = grpc.secure_channel(addr, creds)
else:
self._channel = grpc.insecure_channel(addr)
channel = grpc.insecure_channel(addr)

if api_key is not None:
interceptor = _ApiKeyInterceptor(api_key)
self._channel = grpc.intercept_channel(self._channel, interceptor)
if self._api_key is not None:
interceptor = _ApiKeyInterceptor(self._api_key)
channel = grpc.intercept_channel(channel, interceptor)

self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
return channel

def close(self) -> None:
"""Close the underlying gRPC channel."""
Expand DownExpand Up@@ -215,6 +246,10 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
server stream closes or an error occurs. Skip nil message frames
(keepalive signals) automatically.

If the server returns UNAVAILABLE with an ``x-fila-leader-addr``
trailing metadata entry, the client transparently reconnects to the
leader address and retries the consume call once.

Args:
queue: Queue to consume from.

Expand All@@ -230,10 +265,26 @@ def consume(self, queue: str) -> Iterator[ConsumeMessage]:
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e
leader_addr = _extract_leader_hint(e)
if leader_addr is not None:
stream = self._reconnect_and_consume(leader_addr, queue)
else:
raise _map_consume_error(e) from e

return self._consume_iter(stream)

def _reconnect_and_consume(self, leader_addr: str, queue: str) -> Any:
"""Create a new channel to *leader_addr* and retry the consume call."""
self._channel.close()
self._channel = self._make_channel(leader_addr)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
self._stub = service_pb2_grpc.FilaServiceStub(self._channel) # type: ignore[no-untyped-call]
try:
return self._stub.Consume(
service_pb2.ConsumeRequest(queue=queue)
)
except grpc.RpcError as e:
raise _map_consume_error(e) from e

def _consume_iter(
self,
stream: Any,
Expand Down
Loading