Commit Graph
9 Commits
Author SHA1 Message Date
Erik JohnstonandClaude Fable 5 62e4b39cc1 Fix the logcontext restore protocol in run_python_awaitable
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
2026-07-16 15:05:27 +00:00
Erik JohnstonandClaude Opus 4.8 1e537e4e46 Port the sentinel logcontext to Rust
`_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
2026-07-16 14:05:10 +00:00
Erik JohnstonandClaude Opus 4.8 960acfce19 Fix clippy needless_borrow in ContextResourceUsage operators
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
2026-07-16 13:48:05 +00:00
Erik Johnston 4681dae117 Simplify 2 2026-07-16 14:38:17 +01:00
Erik Johnston cafe75cf73 Simplify 2026-07-16 14:34:15 +01:00
Erik JohnstonandClaude Fable 5 62067f1e00 Make the logcontext switch native (getrusage via libc)
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
2026-07-16 13:27:17 +00:00
Erik JohnstonandClaude Fable 5 ad59e71e2a Port LoggingContext to a Rust pyclass
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
2026-07-16 13:25:30 +00:00
Erik JohnstonandClaude Fable 5 fa91dc5e3c Port ContextResourceUsage to a Rust pyclass
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
2026-07-16 13:22:04 +00:00
Erik JohnstonandClaude Fable 5 ec2894b4a7 Store the current logcontext in Rust (unified task-local + thread-local)
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
2026-07-16 13:21:46 +00:00