Add safe context watcher API - #6227
Conversation
| /// storage. State can still be shared through safe static synchronization primitives. | ||
| /// | ||
| /// Panics and returned [`PyErr`][crate::PyErr] values are reported as unraisable exceptions and | ||
| /// never unwind across the C boundary. |
There was a problem hiding this comment.
Should we mention this as well?
/// # Thread Safety
///
/// On GIL-enabled builds, callbacks are invoked synchronously. On free-threaded
/// builds (when this API is available), callbacks may be invoked concurrently.
/// Ensure your callback is thread-safe if needed.
There was a problem hiding this comment.
That's true, It should definitely be mentioned if we enable the API on free threaded builds.
The problem is that PyContext_AddWatcher and PyContext_ClearWatcher while available on free threaded are not callable concurrently and require external synchronization, which is not something we can guarantee from an extension.
I was thinking of making this API only available on GIL-enabled builds for this reason.
There was a problem hiding this comment.
I think we can should requireSend + Sync + 'static on the provided closure.
There was a problem hiding this comment.
The API in this PR accepts a function path, which is coerced to a function pointer. Function pointers carry no captured state and are already Send + Sync + 'static, so there is nothing additional to enforce.
CPython’s watcher API does not expose a user-data pointer where we could store a closure, which is why I did not try to implement a closure registration api.
There was a problem hiding this comment.
@bschoenmaeckers can I ask you to give it another look ? Thanks
There was a problem hiding this comment.
The problem is that
PyContext_AddWatcherandPyContext_ClearWatcherwhile available on free threaded are not callable concurrently and require external synchronization, which is not something we can guarantee from an extension.
Where do you conclude this? We should make a CPython issue for this if not already present.
eb0dff3 to
66ad857
Compare
7259b06 to
69b141c
Compare
|
LGTM, but im not a maintainer |
|
Still, thank you for taking a look. |
b253b69 to
2041732
Compare
## Description Add a generic CPython 3.14+ publisher for the internal `python.context.switch` event. It registers the CPython `PyContext_AddWatcher` API from the native extension and dispatches the event after each context switch. The C callback catches Rust panics at the FFI boundary and preserves any exception already pending when CPython invokes it. This PR is stacked on #19604, which adds the public `DD_TRACE_OTEL_CTX_ENABLED` configuration and activation-listener setup. The setting defaults to `true`; setting it to `false` disables both the listener and this Python 3.14 watcher. It also complements #19336, which independently provides the asyncio, uvloop, AnyIO, and greenlet emitters for older Python versions. ## Testing - `scripts/lint checks` - `reno lint` - CPython 3.14 native context watcher: 4 passed - Linux CPython 3.14.5 OTel thread context: 6 passed ## Risks Low ## Additional Notes - The PR contains raw external C bindings to CPython. I am currently upstreaming them to PyO3; once that work is merged and released, we can rely on the PyO3 bindings and simplify this code: - PyO3/pyo3#6204 - PyO3/pyo3#6227 Co-authored-by: florentin.labelle <florentin.labelle@datadoghq.com>
9826318 to
d0a8de6
Compare
Fix the newsfragment's overclaim that PyContext requires GIL-enabled CPython 3.14+ (it's available on all supported versions; only the watcher API needs 3.14+), clarify the SAFETY comments on ContextWatcherGuard around single-interpreter attachment, and drop a redundant doctest cfg attribute already covered by the module-level gate in lib.rs.
d0a8de6 to
cf95e72
Compare
Merging this PR will improve performance by 10.31%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | Simulation | test_empty_class_init_py |
27.1 µs | 24.5 µs | +10.31% |
Tip
Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.
Comparing florentinl:florentin.labelle/add-safe-wrappers-for-context-watching-api (cf95e72) with main (6b67723)
Footnotes
-
6 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
|
I would quite like to avoid the macro for this API, it seems unfortunate. I understand why it's there, however. I'm thinking about what we can do as alternatives. |
davidhewitt
left a comment
There was a problem hiding this comment.
I opened florentinl#1 which has a suggestion how I'd like this API to look.
Also placed a few other comments here.
| /// storage. State can still be shared through safe static synchronization primitives. | ||
| /// | ||
| /// Panics and returned [`PyErr`][crate::PyErr] values are reported as unraisable exceptions and | ||
| /// never unwind across the C boundary. |
There was a problem hiding this comment.
The problem is that
PyContext_AddWatcherandPyContext_ClearWatcherwhile available on free threaded are not callable concurrently and require external synchronization, which is not something we can guarantee from an extension.
Where do you conclude this? We should make a CPython issue for this if not already present.
| /// This guard is bound to the [`Python`] attachment used to create it. It therefore cannot be sent | ||
| /// to another thread, moved outside that attachment, or moved into [`Python::detach`]. |
There was a problem hiding this comment.
I think this is potentially awkward, we might want to consider the same strategy as PyBuffer here which automatically attaches on drop.
That said, a bound type is more efficient so maybe we want to support the ability to bind & unbind this and have two guard types? 🤔
I have a feeling that for most usecases the lifetime will be a problem.
| /// | ||
| /// - The thread must be attached to Python. | ||
| /// - `object` must follow the contract for the supplied `event`. | ||
| pub unsafe extern "C" fn context_watcher<Callback: ContextWatcherCallbackDef>( |
There was a problem hiding this comment.
It would be desirable to reduce the amount of unsafe code in here by using PyO3's higher-level APIs where possible.
| // Although normal PyO3 APIs return errors as `PyResult`, `PyErr::restore` can be | ||
| // called directly. Do not allow an Ok return with an exception still set. | ||
| if crate::PyErr::occurred(py) { | ||
| return Err(crate::PyErr::fetch(py)); | ||
| } |
There was a problem hiding this comment.
This seems like a general problem that would be worth addressing in all trampolines rather than special casing here
Adds
PyContextand safe context-watcher bindings for GIL-enabled CPython 3.14:Tested with unit tests, doctests, and Clippy on Python 3.14.