mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-25 20:29:56 +00:00
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:
co-authored by
Claude Opus 4.8
parent
960acfce19
commit
1e537e4e46
+80
-27
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,10 +53,11 @@ from twisted.python.threadpool import ThreadPool
|
||||
from synapse.logging.loggers import ExplicitlyConfiguredLogger
|
||||
from synapse.synapse_rust.logcontext import (
|
||||
DEBUG_LOGGER_NAME,
|
||||
SENTINEL_CONTEXT as SENTINEL_CONTEXT,
|
||||
ContextResourceUsage as ContextResourceUsage,
|
||||
LoggingContext as LoggingContext,
|
||||
_Sentinel as _Sentinel,
|
||||
current_context as current_context,
|
||||
register_sentinel,
|
||||
set_current_context as set_current_context,
|
||||
)
|
||||
from synapse.util.stringutils import random_string_insecure_fast
|
||||
@@ -109,69 +110,13 @@ class ContextRequest:
|
||||
user_agent: str
|
||||
|
||||
|
||||
LoggingContextOrSentinel = Union["LoggingContext", "_Sentinel"]
|
||||
LoggingContextOrSentinel = Union[LoggingContext, _Sentinel]
|
||||
|
||||
|
||||
class _Sentinel:
|
||||
"""
|
||||
Sentinel to represent the root context
|
||||
|
||||
This should only be used for tasks outside of Synapse like when we yield control
|
||||
back to the Twisted reactor (event loop) so we don't leak the current logging
|
||||
context to other tasks that are scheduled next in the event loop.
|
||||
|
||||
Nothing from the Synapse homeserver should be logged with the sentinel context. i.e.
|
||||
we should always know which server the logs are coming from.
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
"previous_context",
|
||||
"finished",
|
||||
"scope",
|
||||
"server_name",
|
||||
"request",
|
||||
"tag",
|
||||
]
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Minimal set for compatibility with LoggingContext
|
||||
self.previous_context = None
|
||||
self.finished = False
|
||||
self.server_name = "unknown_server_from_sentinel_context"
|
||||
self.request = None
|
||||
self.scope = None
|
||||
self.tag = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "sentinel"
|
||||
|
||||
def start(self, rusage: "tuple[float, float] | None") -> None:
|
||||
pass
|
||||
|
||||
def stop(self, rusage: "tuple[float, float] | None") -> None:
|
||||
pass
|
||||
|
||||
def add_database_transaction(self, duration_sec: float) -> None:
|
||||
pass
|
||||
|
||||
def add_database_scheduled(self, sched_sec: float) -> None:
|
||||
pass
|
||||
|
||||
def record_event_fetch(self, event_count: int) -> None:
|
||||
pass
|
||||
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
|
||||
SENTINEL_CONTEXT = _Sentinel()
|
||||
|
||||
# Hand the sentinel to the Rust logcontext slot, which owns the "current context"
|
||||
# storage (see `synapse.synapse_rust.logcontext` / `rust/src/logcontext.rs`). Rust
|
||||
# returns this exact object when no context is set, so `context is SENTINEL_CONTEXT`
|
||||
# identity and `bool(context)` semantics are preserved. We push it in from here
|
||||
# rather than have Rust import this module, to avoid a circular import.
|
||||
register_sentinel(SENTINEL_CONTEXT)
|
||||
# `_Sentinel` (the root "no logcontext" marker) and its singleton `SENTINEL_CONTEXT`
|
||||
# are now defined and owned by the Rust logcontext module, which holds the "current
|
||||
# context" storage (see `synapse.synapse_rust.logcontext` / `rust/src/logging/context.rs`).
|
||||
# Rust returns this exact object when no context is set, so `context is SENTINEL_CONTEXT`
|
||||
# identity and `bool(context)` semantics are preserved without a Python-side bootstrap.
|
||||
|
||||
|
||||
class LoggingContextFilter(logging.Filter):
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Literal, Optional
|
||||
|
||||
from synapse.logging.context import ContextRequest, LoggingContextOrSentinel
|
||||
|
||||
@@ -86,6 +86,27 @@ class LoggingContext:
|
||||
def add_database_scheduled(self, sched_sec: float) -> None: ...
|
||||
def record_event_fetch(self, event_count: int) -> None: ...
|
||||
|
||||
class _Sentinel:
|
||||
"""The root "no logcontext" marker. A falsy singleton (see `SENTINEL_CONTEXT`)
|
||||
whose fields are inert defaults and whose methods are no-ops."""
|
||||
|
||||
previous_context: None
|
||||
finished: bool
|
||||
scope: None
|
||||
server_name: str
|
||||
request: None
|
||||
tag: None
|
||||
|
||||
def __str__(self) -> str: ...
|
||||
def start(self, rusage: "Optional[tuple[float, float]]") -> None: ...
|
||||
def stop(self, rusage: "Optional[tuple[float, float]]") -> None: ...
|
||||
def add_database_transaction(self, duration_sec: float) -> None: ...
|
||||
def add_database_scheduled(self, sched_sec: float) -> None: ...
|
||||
def record_event_fetch(self, event_count: int) -> None: ...
|
||||
def __bool__(self) -> Literal[False]: ...
|
||||
|
||||
SENTINEL_CONTEXT: _Sentinel
|
||||
|
||||
def current_context() -> LoggingContextOrSentinel:
|
||||
"""Get the current logging context.
|
||||
|
||||
@@ -114,11 +135,3 @@ def set_current_context(
|
||||
Reads the thread CPU usage once via `getrusage(RUSAGE_THREAD)` and does the
|
||||
`stop`/`start` accounting natively; raises `TypeError` if `context` is `None`.
|
||||
"""
|
||||
|
||||
def register_sentinel(sentinel: LoggingContextOrSentinel) -> None:
|
||||
"""Register the Python sentinel logcontext with the Rust slot.
|
||||
|
||||
Called once from `synapse.logging.context` at import time so that the object
|
||||
returned by `current_context()` when no context is set is Python's
|
||||
`SENTINEL_CONTEXT` singleton (preserving identity and `bool()` semantics).
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user