Two fixes to the `starter` closure that drives a Python awaitable on the
reactor thread on behalf of a tokio task:
- Run the fallible `run_in_background`/`addCallbacks` section inside a
closure so an error can no longer skip the restore: previously the `?`
returned early and permanently leaked the restored context onto the
reactor thread, misattributing all subsequent reactor work.
- Don't restore a context that has already finished (the request completed
or was cancelled while the task was still running — `create_deferred`
does not propagate cancellation). Doing so tripped the 'Re-starting
finished log context' abuse check and accounted work against a context
whose metrics were already finalised; run in the reactor's current
context (normally the sentinel) instead, as the pre-port code did.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFbRtswu7rsHrttJFauUUb
`_Sentinel` and its `SENTINEL_CONTEXT` singleton — the root "no logcontext"
marker — are now a native pyclass owned by the Rust logcontext module, which
already owns the "current context" storage. Rust both defines the type and
creates the one instance lazily, so the `register_sentinel` bootstrap (Python
constructing the sentinel and pushing it into the Rust slot to dodge a circular
import) is gone: the module exports `SENTINEL_CONTEXT` directly.
Kept as a separate class from `LoggingContext` (the Null Object pattern, as
upstream has it): the sentinel is falsy, inert (all methods no-op), and
thread-agnostic, the opposite of an active thread-affine context. Merging would
scatter `is_sentinel` branches through `__bool__` and every accounting method.
Invariants preserved: `current_context() is SENTINEL_CONTEXT`, `bool()` is
False, `str()` is "sentinel", and `server_name` is
"unknown_server_from_sentinel_context" (read by the log filter). Verified via
95 trial tests + a Rust≡Python identity/bool/round-trip script; full lint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFbRtswu7rsHrttJFauUUb
The operator methods take `other: &ContextResourceUsage`, so passing
`&other` to `add_assign`/`sub_assign` (which already take a reference) is a
double-borrow that fails `cargo clippy -- -D warnings`. Drop the `&`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFbRtswu7rsHrttJFauUUb
Port `set_current_context` — the logcontext switch primitive, on the hottest
path in the logging system — from Python into Rust, and read the thread CPU
usage directly via `getrusage(RUSAGE_THREAD)` (libc) instead of through the
`resource` module. This removes the per-switch Python frame, the `stop`/`start`
method dispatch, and the `struct_rusage` object allocated on every switch;
`current.stop()`/`context.start()` for the common base-`LoggingContext` case
now run inline in Rust.
`usage_start` becomes a private native `(ru_utime, ru_stime)` snapshot rather
than a Python `struct_rusage` — nothing outside the module read it. The
`start`/`stop` methods keep their abuse-detection behaviour (thread affinity,
double-start, stop-without-start) via a shared native core (`start_inner`/
`stop_inner`) that both the pymethods and the switch fast path call, so those
checks still run on every switch. `_get_cputime` folds into a native
`cputime_delta` helper.
Subclasses (`BackgroundProcessLoggingContext`) still work unchanged: the switch
dispatches to them through Python (`obj.start(rusage)` / `obj.stop(rusage)`),
materialising the rusage as a plain `(utime, stime)` tuple only on that
off-hot-path branch, so their overrides run. `getrusage(RUSAGE_THREAD)` is
Linux-only; other platforms return `None` (no per-context CPU accounting), as
before.
Removes the now-dead Python rusage plumbing (`get_thread_resource_usage`, the
`RUSAGE_THREAD` support probe, `is_thread_resource_usage_supported`) — Rust owns
rusage now. The Phase 0 characterization test's rusage stand-in changes from an
opaque `object()` to a `(0.0, 0.0)` tuple to match the native representation;
it still pins the same observable abuse-detection behaviour.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LnJSXQ86AAWNtihR4C5PH
Move `LoggingContext` from a pure-Python class to a native `#[pyclass]` in
`rust/src/logcontext.rs`, re-exported from `synapse.logging.context`. The
attribute surface, methods, error-message wording and abuse-detection
behaviour are all preserved, so callers — and the Python subclass
`BackgroundProcessLoggingContext` — are unaffected.
`set_current_context` stays in Python for now: it still calls `ctx.start`/
`ctx.stop` via normal attribute dispatch, so subclass overrides continue to
work. The native switch primitive (with in-Rust `getrusage`) is a later
increment.
Notes on the port:
- Construction is split between `__new__` (allocates a blank instance) and
`__init__` (does the real work), mirroring a pure-Python class. This is what
lets `BackgroundProcessLoggingContext`, which composes a name and then calls
`super().__init__(name=..., server_name=...)`, keep working unchanged.
- Methods that both mutate `self` and call back into Python (`start`, `stop`,
`__enter__`, `__exit__`) take an owned `Bound<Self>` and only hold a
`borrow_mut()` across the field write itself, never across a Python call, so
a re-entrant log record (which resolves the current context and reads this
object's getters) cannot hit an "already borrowed" error.
- `logcontext_error`, `set_current_context`, `logger` and
`logcontext_debug_logger` are re-resolved through the module on every call so
that test patches take effect.
- `__traverse__`/`__clear__` cover `previous_context`/`parent_context`/
`request`/`scope` for the cyclic GC (scope <-> context is a real cycle).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LnJSXQ86AAWNtihR4C5PH
Replace the Python `ContextResourceUsage` class with a native `#[pyclass]`
(exposed via `synapse.synapse_rust.logcontext`, re-exported under the same name
from `synapse.logging.context`). The public surface is preserved drop-in:
the six get/set attributes, `copy`/`reset`, `+`/`-`/`+=`/`-=`, the
`ContextResourceUsage(copy_from=...)` constructor and the exact `repr`.
This is the first step of moving the logcontext machinery to Rust: keeping the
usage accounting in a native struct lets the switch path (a later commit) do
its rusage bookkeeping without allocating a Python object per operation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LnJSXQ86AAWNtihR4C5PH
Move the "current logcontext" storage out of the Python `threading.local`
and into a Rust slot (`rust/src/logcontext.rs`, exposed as
`synapse.synapse_rust.logcontext`). `current_context()` now consults the
tokio task-local first, then the OS thread-local, then the sentinel.
This makes a single source of truth visible from both worlds:
- Log records emitted from Rust while a tokio task is being polled (e.g.
reqwest connecting, and any `log::` records from our async code or its
dependencies) resolve `current_context()` to the context that was current
when Python called into Rust, so `LoggingContextFilter` attributes them
correctly — no per-record stamping machinery, pyo3-log unchanged.
- `run_python_awaitable` now restores that captured context on the reactor
thread before driving the awaitable, so Python invoked from Rust (e.g.
`DatabasePool.runInteraction` from the native `/versions` handler) runs in
the right logcontext and its DB-transaction accounting lands on the right
request. Resolves the FIXME at deferred.rs:223.
`create_deferred` captures the caller's context at the FFI boundary and
scopes it onto the spawned task. Python keeps the accounting policy:
`set_current_context` still does the `getrusage` start/stop bookkeeping and
only delegates the raw slot write to `swap_current_context`. The sentinel is
pushed into Rust at import (`register_sentinel`) so `context is
SENTINEL_CONTEXT` identity and `bool(context)` semantics are preserved, and
Rust never imports `synapse.logging.context` (no circular import).
Tests: Rust unit tests for the slot/lookup precedence; a trial test that a
Rust-origin log record is attributed to the caller's logcontext; and a trial
test that the `/versions` per-user DB lookup is accounted against the caller's
logcontext (the resolved FIXME). Existing tests/util/test_logcontext.py and
tests/synapse_rust pass unmodified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LnJSXQ86AAWNtihR4C5PH