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
This commit is contained in:
Erik Johnston
2026-07-16 14:05:10 +00:00
co-authored by Claude Opus 4.8
parent 960acfce19
commit 1e537e4e46
3 changed files with 110 additions and 99 deletions
+80 -27
View File
@@ -54,13 +54,73 @@ use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};
use pyo3::{PyTraverseError, PyVisit};
/// The Python sentinel logcontext (`synapse.logging.context.SENTINEL_CONTEXT`).
///
/// Pushed in from Python at import time via [`register_sentinel`] rather than
/// imported here, to avoid a circular import at module-registration time (Rust
/// must not import `synapse.logging.context`; see [`crate::deferred`]).
/// The sentinel logcontext singleton (`synapse.logging.context.SENTINEL_CONTEXT`),
/// created lazily by [`sentinel`]. Owned natively: Rust defines the [`Sentinel`]
/// type *and* holds the one instance, so there is no Python-side bootstrap and no
/// import of `synapse.logging.context` at registration time (which would be a
/// circular import; see [`crate::deferred`]).
static SENTINEL: OnceCell<Py<PyAny>> = OnceCell::new();
/// The root "no logcontext" marker (`synapse.logging.context.SENTINEL_CONTEXT`).
///
/// A drop-in for the former Python `_Sentinel`: a singleton whose fields are inert
/// defaults and whose methods are no-ops, and which is *falsy* so callers can test
/// `if not current_context()` to detect "no logcontext". [`switch_context`]
/// special-cases it by identity, so its `start`/`stop` never run on the hot path;
/// the other no-op methods exist only so code holding a `LoggingContextOrSentinel`
/// can call them without first checking the concrete type.
#[pyclass(name = "_Sentinel", get_all, set_all)]
pub struct Sentinel {
previous_context: Option<Py<PyAny>>,
finished: bool,
scope: Option<Py<PyAny>>,
server_name: String,
request: Option<Py<PyAny>>,
tag: Option<Py<PyAny>>,
}
impl Sentinel {
/// The singleton's initial state (mirrors the former Python `_Sentinel.__init__`).
fn instance() -> Self {
Sentinel {
previous_context: None,
finished: false,
scope: None,
server_name: "unknown_server_from_sentinel_context".to_owned(),
request: None,
tag: None,
}
}
}
#[pymethods]
impl Sentinel {
fn __str__(&self) -> &'static str {
"sentinel"
}
/// No-op: the sentinel is never actually running, so there is nothing to
/// account.
fn start(&self, _rusage: Option<(f64, f64)>) {}
/// No-op counterpart to [`Self::start`].
fn stop(&self, _rusage: Option<(f64, f64)>) {}
/// No-op: work done under the sentinel is attributed to no context.
fn add_database_transaction(&self, _duration_sec: f64) {}
/// No-op counterpart to [`Self::add_database_transaction`].
fn add_database_scheduled(&self, _sched_sec: f64) {}
/// No-op: event fetches under the sentinel are attributed to no context.
fn record_event_fetch(&self, _event_count: i64) {}
/// The sentinel is falsy, matching the former Python `_Sentinel.__bool__`.
fn __bool__(&self) -> bool {
false
}
}
/// Name of the opt-in logger for logcontext switch tracing.
///
/// This is the single source of truth for the logger name: it is used as the
@@ -830,26 +890,18 @@ pub fn set_current_context(py: Python<'_>, context: Bound<'_, PyAny>) -> PyResul
Ok(current)
}
/// Register the Python sentinel logcontext.
/// Get a reference to the sentinel logcontext singleton, creating it on first use.
///
/// Called once from `synapse.logging.context` at import time. Registering twice
/// is a no-op (the first registration wins); this keeps the identity of the
/// sentinel object we return from [`current_context`] equal to Python's
/// `SENTINEL_CONTEXT` singleton, preserving `context is SENTINEL_CONTEXT` and
/// `bool(context)` semantics.
#[pyfunction]
pub fn register_sentinel(sentinel: Py<PyAny>) {
let _ = SENTINEL.set(sentinel);
}
/// Get a fresh reference to the sentinel logcontext.
/// The instance is owned here (not pushed in from Python), so its identity is
/// stable and equal to the `SENTINEL_CONTEXT` exported by [`register_module`],
/// preserving `context is SENTINEL_CONTEXT` and `bool(context)` semantics.
fn sentinel(py: Python<'_>) -> Py<PyAny> {
SENTINEL
.get()
.expect(
"synapse.logging.context sentinel not registered with the Rust logcontext slot; \
synapse.logging.context must call register_sentinel() at import",
)
.get_or_init(|| {
Py::new(py, Sentinel::instance())
.expect("failed to create the sentinel logcontext")
.into_any()
})
.clone_ref(py)
}
@@ -895,11 +947,14 @@ pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()>
let child_module: Bound<'_, PyModule> = PyModule::new(py, "logcontext")?;
child_module.add_class::<ContextResourceUsage>()?;
child_module.add_class::<LoggingContext>()?;
child_module.add_class::<Sentinel>()?;
child_module.add_function(wrap_pyfunction!(current_context, &child_module)?)?;
child_module.add_function(wrap_pyfunction!(swap_current_context, &child_module)?)?;
child_module.add_function(wrap_pyfunction!(set_current_context, &child_module)?)?;
child_module.add_function(wrap_pyfunction!(register_sentinel, &child_module)?)?;
child_module.add("DEBUG_LOGGER_NAME", DEBUG_LOGGER_NAME)?;
// The sentinel singleton is owned by Rust; export the one instance so Python's
// `SENTINEL_CONTEXT` is that exact object (identity preserved).
child_module.add("SENTINEL_CONTEXT", sentinel(py))?;
m.add_submodule(&child_module)?;
@@ -920,11 +975,9 @@ mod tests {
use super::*;
/// Register a sentinel exactly once (the `OnceCell` keeps the first) and
/// return whichever object is actually registered, so identity assertions
/// hold regardless of which test ran first.
/// The native sentinel singleton (created lazily on first use), used for
/// identity assertions.
fn registered_sentinel(py: Python<'_>) -> Py<PyAny> {
register_sentinel(PyString::new(py, "SENTINEL").into_any().unbind());
sentinel(py)
}