Skip to content

Releases: Query-farm/vgi-rpc-python

v0.15.0

01 May 01:20

Choose a tag to compare

Workers can now learn which transport they are bound to

Workers (RPC service implementations) had no way to know which transport (pipe / http / unix) they were being served over. CallContext.transport_metadata only carried HTTP fields and was ambiguous for non-HTTP transports — an empty dict could mean "pipe" or "HTTP request without those headers." There was no startup hook either.

TransportKind and the on_serve_start hook

A new TransportKind enum (PIPE, HTTP, UNIX) is exposed via three knobs:

  • RpcServer.transport_kind — coarse identifier of the bound transport, populated once serving begins.
  • RpcServer.transport_capabilitiesfrozenset[str] of capability flags, currently {"shm"} when bound to a ShmPipeTransport.
  • CallContext.kind — per-call view of the same TransportKind for methods that already accept ctx.

Implementations may opt into a ServeStartHook lifecycle method:

from vgi_rpc import CallContext, TransportKind


class MyServiceImpl:
    def on_serve_start(self, kind: TransportKind) -> None:
        if kind is TransportKind.HTTP:
            self._cache = build_http_cache()

    def fetch(self, key: str, ctx: CallContext) -> str:
        if ctx.kind is TransportKind.HTTP and self._cache is not None:
            return self._cache.get(key)
        return load_from_disk(key)

The hook is duck-typed (no base class needed); a ServeStartHook Protocol is exported for type-hinting.

Fork-safe HTTP firing

For pipe / unix transports the hook fires inside RpcServer.serve(transport). For HTTP it fires lazily on the first request handled in the current process, via a tiny one-shot Falcon middleware. Pre-fork WSGI servers (gunicorn, uwsgi) therefore run startup work in each child worker, not the master — per-process resources (DB pools, threads, file handles) are no longer fork-unsafely inherited. Subprocess workers report PIPE because they speak Arrow IPC over the parent's stdin/stdout.

Failure semantics

Hook exceptions propagate (and are logged via logging.getLogger("vgi_rpc.rpc").exception first) — a misconfigured worker dies loudly rather than serving in a broken state. Rebinding the same RpcServer to a different transport re-fires the hook with the new kind rather than raising, so test fixtures that exercise multiple transports against one server are supported.

SHM as a capability, not an enum value

Shared-memory availability is exposed via transport_capabilities, not the enum, so coarse transport-kind checks stay simple while workers that need zero-copy paths can still detect SHM:

def on_serve_start(self, kind: TransportKind) -> None:
    if "shm" in self._server.transport_capabilities:
        self._enable_zero_copy()

v0.14.0

30 Apr 19:11

Choose a tag to compare

Sentry: richer user data and protocol-level tagging

Two improvements to the auto-attached Sentry dispatch hook. Server-only — no client cooperation required.

Standards-aligned set_user

Previously, the JWT sub claim was placed in user.username, leaving Sentry showing opaque IdP identifiers (4FySzCeE4zIYuvph49iD9tcJL0_zDWfpMqarUUPc1uA) where a human handle should be. Now auth.principal populates user.id (Sentry's canonical opaque identifier) and the decoded JWT claims feed user.username, user.email, user.name from standard OIDC claim names:

Sentry user field JWT claim
id auth.principal (typically sub)
username preferred_username
email email
name name

SentryConfig gains a user_claim_map so non-standard IdPs (e.g. Auth0 namespaced claims like https://example.com/email) can override per-key. Static bearer tokens (no JWT, empty claims) populate only user.id — the same effective behavior as before but in the correct field.

Generic vgi.attach_id / vgi.transaction_id scope tags

Every dispatch now auto-tags vgi.attach_id and vgi.transaction_id on the Sentry scope when present. _extract_well_known walks one level into kwargs to handle the three protocol shapes:

  • direct kwargs — catalog_detach(attach_id=...)
  • request dataclasses — bind(request=BindRequest(attach_id=...))
  • InitRequest.bind_call nesting — init(request=InitRequest(bind_call=BindRequest(attach_id=...)))

No per-method wiring required. Tag values are 12-char SHA-256 prefixes via the new public short_hash() helper — bytes and their .hex() form produce the same value, so Sentry's tag-value distribution UI stays bounded while same-input → same-output preserves cross-event correlation. Full hex remains in catalog breadcrumbs for direct lookup.

Public API additions

  • vgi_rpc.sentry.short_hash(value, *, length=12) — stable 12-char SHA-256 prefix, accepts bytes | str | None. Reusable for any high-cardinality opaque ID you want to tag in Sentry without exhausting the tag-value distribution UI.
  • SentryConfig.user_claim_mapMapping[str, str] of Sentry user-field name → JWT claim name. Defaults to OIDC standard names.

Internal

  • _SentryDispatchHook.on_dispatch_start walks request dataclasses for attach_id/transaction_id after the existing tag/context logic. Hook signature unchanged.

v0.13.0

30 Apr 16:50

Choose a tag to compare

Sentry: useful traces, not just errors

Phase A of making Sentry traces actually useful for finding slow RPC calls. Three changes ship together — all server-only, no client cooperation required.

Transaction names

By default, vgi-rpc now overrides Sentry's WSGI-derived /{method} transaction name (a literal route template — the actual method name was getting lost) with rpc {method}. Transactions group by RPC method in the Performance dashboard. Disable with SentryConfig(set_transaction_name=False) if you have alerts pinned to the route names.

Searchable span attributes

Every dispatch attaches rpc.system, rpc.service, rpc.method, and rpc.method_type as span data on the active transaction's root span — searchable in Trace Explorer / Insights. Streams also carry rpc.stream_id (one uuid shared across all /init and /exchange HTTP turns of one logical call) on span data so you can find sibling turns in the UI.

Opt-in RPC parameter recording

SentryConfig(record_params=True) exposes kwargs as rpc.param.<k> span attributes — the executeScan(table='orders', predicate=...) use case:

span.op:rpc.server rpc.method:executeScan rpc.param.table:orders
-> chart p99(span.duration) GROUP BY rpc.param.predicate

Default off because kwargs may carry user data and Sentry's default scrubber matches key names only. New SentryConfig knobs:

Field Default Purpose
set_transaction_name True Override WSGI route-template names with rpc {method}
record_params False Record kwargs as rpc.param.<k> span attributes
tag_params () Operator-curated whitelist of params duplicated as scope tags for Issues filtering (low cardinality)
param_redactor key-based default Sanitiser; default strips password|token|secret|key|authorization keys. noop_redactor opts out
max_param_value_bytes 1024 Truncate string values; matches Relay's per-attribute cap

Span attributes accept primitives only (str/bool/int/float and homogeneous lists thereof) — non-primitive kwargs are silently dropped to avoid Sentry ingestion errors. Free-text PII is the operator's responsibility; the default scrubber won't catch values like predicate=\"email = 'alice@x.com'\".

See docs/api/sentry.md for the full caveat list.

Internal contract change

_DispatchHook.on_dispatch_start Protocol gains a kwargs: Mapping[str, Any] parameter so observability backends can attach RPC parameters to traces. OTel hook accepts-and-ignores it for now (param recording on OTel is a separate follow-up). The hook is _-prefixed and internal — no public-API impact.

Other

  • fix(utils): empty_batch now supports schemas with sparse and dense union fields. pyarrow's pa.array([], type=union) raises ArrowNotImplementedError, so empty union arrays are now built via Array.from_buffers from empty children plus an empty type-codes buffer (and an empty offsets buffer for dense unions).
  • docs(sentry): corrected misleading wording about SENTRY_DSN. The Sentry SDK does not auto-initialise on import; sentry_sdk.init() must be called explicitly. It will fall back to reading SENTRY_DSN from the environment only if you don't pass an explicit dsn=.

v0.12.0

28 Apr 21:46

Choose a tag to compare

Sentry: zero-config auto-attach

If sentry_sdk is initialised in the worker process (via sentry_sdk.init(), or by setting SENTRY_DSN so the SDK auto-inits), every RpcServer now wires default-config Sentry instrumentation automatically. No flag, no extra env var — sentry_sdk.is_initialized() is the signal of intent.

import sentry_sdk
sentry_sdk.init(dsn="https://...")        # or just set SENTRY_DSN

from vgi_rpc import RpcServer
server = RpcServer(MyService, MyServiceImpl())   # Sentry already wired

The check is gated on sentry_sdk already being importable in the process, so workers without vgi-rpc[sentry] pay nothing.

Customising

instrument_server_sentry() (and make_wsgi_app(sentry_config=...) / serve_http(sentry_config=...)) now use replace semantics: an explicit configured call strips any existing _SentryDispatchHook from the chain (preserving non-Sentry hooks like OTel) before installing the new one. So explicit config always wins, regardless of timing relative to auto-attach.

from vgi_rpc.sentry import SentryConfig, instrument_server_sentry

instrument_server_sentry(server, SentryConfig(custom_tags={"env": "prod"}, enable_performance=True))

See docs/api/sentry.md for the full configuration reference.

v0.11.1

28 Apr 20:55

Choose a tag to compare

Performance

Cut subprocess worker startup time by ~55% (312ms → 140ms) and the full test suite by ~29% (132s → 94s).

  • Lazy-load optional integrations (vgi_rpc.http, vgi_rpc.sentry, vgi_rpc.otel, vgi_rpc.s3, vgi_rpc.gcs) via module-level __getattr__ instead of importing them eagerly from vgi_rpc/__init__.py. Cuts import vgi_rpc cost roughly in half (~250ms → ~115ms) for callers that don't need those extras — including every pipe/subprocess worker.
  • Test fixture service (Protocol, Impl, state classes) extracted from tests.test_rpc into tests/_fixture_service.py so subprocess fixtures don't drag pytest and conftest into worker import paths.

No public API changes. from vgi_rpc import http_connect, vgi_rpc.SentryConfig, from vgi_rpc import * all behave identically.

v0.11.0

28 Apr 17:51

Choose a tag to compare

Highlights

  • HTTP response caps reworked: separate max_response_bytes (HTTP body) and max_externalized_response_bytes (external upload volume) knobs; worker-visible budgets on OutputCollector; strict-fail enforcement; pre-flight externalization check. Deprecated max_stream_response_bytes retained for one cycle.
  • Slim __describe__ v4 + protocol_hash: language-neutral 8-column wire format; SHA-256 protocol hash surfaced in describe metadata and access-log records.
  • Access-log spec: cross-language conformance (access_log.schema.json), record truncation, size/time rotation, {pid}/{server_id} path placeholders, Vector + Fluent Bit shipper configs.
  • OAuth PKCE token-exchange proxy for SPA clients.
  • HTTP dispatcher symmetry: extracted _run_http_exchange_turn / _mint_continuation_token; refactored producer-stream dispatch.
  • Windows CI is green.
  • Sequential Unix-socket listen backlog bumped to 16.
  • OutputCollector.emit select-then-cast so projection works correctly.

See git log v0.10.0..v0.11.0 for the full diff.

v0.10.0

27 Apr 03:25

Choose a tag to compare

Highlights

  • DESCRIBE_VERSION 4: slim language-neutral wire format with protocol_hash.
  • Cross-language access-log conformance: Go, TypeScript, Java aligned (Rust pending).
  • Shared memory transport hardening with new cross-language and header-format tests.
  • Java benchmark suite for direct comparison against the Python results.

See the commit log for the full set of changes since v0.9.0.

v0.9.0: HTTP cookie support

21 Apr 15:28

Choose a tag to compare

HTTP cookie support for unary RPC methods

RPC methods served over the HTTP transport can now read incoming cookies and set/delete cookies on the response via CallContext.

API additions (vgi_rpc.rpc.CallContext)

  • ctx.cookies — read-only Mapping[str, str] of incoming request cookies. Empty on non-HTTP transports.
  • ctx.set_cookie(name, value, *, expires=None, max_age=None, domain=None, path=None, secure=None, http_only=True, same_site=None, partitioned=False) — queue a Set-Cookie on the HTTP response. Unary HTTP methods only.
  • ctx.delete_cookie(name, *, path=None, domain=None) — queue an unset-cookie. Unary HTTP methods only.

Behavior

  • Cookies queued before an exception are still emitted on the 4xx/5xx response.
  • Calling set_cookie/delete_cookie from a streaming method or non-HTTP transport raises RuntimeError (surfaced to clients as RpcError).
  • _AuthMiddleware now installs unconditionally, so transport_metadata (remote_addr, user_agent, cookies) is available even on services without an authenticate callback.

Example

class Session(Protocol):
    def login(self, user: str, password: str) -> str: ...
    def whoami(self) -> str: ...

class SessionImpl:
    def login(self, user: str, password: str, ctx: CallContext) -> str:
        ctx.set_cookie("sid", generate_token(user), max_age=3600, http_only=True, secure=True, same_site="Lax")
        return user

    def whoami(self, ctx: CallContext) -> str:
        return lookup_user(ctx.cookies.get("sid", ""))

v0.8.0

19 Apr 22:59

Choose a tag to compare

Stream cancellation

Client-initiated cancellation for streaming RPC calls. StreamSession and HttpStreamSession now expose a cancel() method that notifies the server to stop producing/processing and discard pending work.

Highlights

  • StreamSession.cancel() and HttpStreamSession.cancel() — idempotent, best-effort.
  • Server detects the new vgi_rpc.cancel custom-metadata key on input batches and breaks the stream loop before calling state.process().
  • Optional StreamState.on_cancel(ctx) hook so server-side state classes can release resources (GPU buffers, upstream connections, etc.).
  • Access log records now carry cancelled=true for cancelled calls (status stays ok).
  • After cancel(), subsequent exchange() / tick() raise RpcError("ProtocolError", ...).

Conformance

Four new conformance methods: cancellable_producer, cancellable_exchange, cancel_probe_counters, reset_cancel_probe (total 52, up from 48). A new TestCancel suite exercises producer and exchange cancellation, idempotency, the on_cancel hook, and transport-cleanliness across pipe, subprocess, shm_pipe, pool, unix, unix_threaded, and HTTP.

Wire protocol

  • New metadata key: vgi_rpc.cancel. Presence on an input batch signals cancellation; value is ignored.
  • Pipe/subprocess: cancel arrives as a zero-row batch of the current input schema (or empty schema if no batch has been written yet).
  • HTTP: cancel arrives as POST {prefix}/{method}/exchange with both vgi_rpc.stream_state#b64 and vgi_rpc.cancel metadata; the server returns an empty IPC stream (no continuation token).

Language implementations are tracked in vgi-rpc-go and vgi-rpc-typescript.

v0.7.1

13 Apr 02:27

Choose a tag to compare

  • Support multiple JWT issuers in jwt_authenticate for multi-tenant setups (e.g. Microsoft Entra)
  • Docs site restructured: 4 tabs (Home, API Reference, Guides, About)
  • Access Log and Conformance Testing pages added to Guides
  • Sidebar navigation restored on all grouped pages