Skip to content

Add safe context watcher API - #6227

Open
florentinl wants to merge 16 commits into
PyO3:mainfrom
florentinl:florentin.labelle/add-safe-wrappers-for-context-watching-api
Open

Add safe context watcher API#6227
florentinl wants to merge 16 commits into
PyO3:mainfrom
florentinl:florentin.labelle/add-safe-wrappers-for-context-watching-api

Conversation

@florentinl

@florentinl florentinl commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Adds PyContext and safe context-watcher bindings for GIL-enabled CPython 3.14:

  • typed context-switch events
  • RAII watcher registration
  • panic- and error-safe callback trampoline
use pyo3::{context::ContextEvent, prelude::*};

fn context_changed(_py: Python<'_>, event: ContextEvent<'_, '_>) -> PyResult<()> {
    if let ContextEvent::Switched(context) = event {
        println!("current context: {context:?}");
    }
    Ok(())
}

fn main() -> PyResult<()> {
    Python::initialize();

    Python::attach(|py| {
        let _watcher = pyo3::register_context_watcher!(py, context_changed)?;
        py.run(
            c"import contextvars\ncontextvars.Context().run(lambda: None)",
            None,
            None,
        )
    })
}

Tested with unit tests, doctests, and Clippy on Python 3.14.

Comment thread src/context.rs Outdated
/// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@florentinl florentinl Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can should requireSend + Sync + 'static on the provided closure.

@florentinl florentinl Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bschoenmaeckers can I ask you to give it another look ? Thanks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Where do you conclude this? We should make a CPython issue for this if not already present.

@florentinl florentinl Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely agree, I raised an issue in CPython for this already: python/cpython#155619

Comment thread src/context.rs Outdated
@florentinl
florentinl force-pushed the florentin.labelle/add-safe-wrappers-for-context-watching-api branch from eb0dff3 to 66ad857 Compare July 28, 2026 07:47
@florentinl
florentinl marked this pull request as ready for review July 28, 2026 08:19
@florentinl
florentinl requested a review from chirizxc July 28, 2026 08:19
Comment thread src/context.rs Outdated
@florentinl
florentinl force-pushed the florentin.labelle/add-safe-wrappers-for-context-watching-api branch from 7259b06 to 69b141c Compare July 28, 2026 10:40
@florentinl
florentinl requested a review from chirizxc July 28, 2026 11:20
@chirizxc

Copy link
Copy Markdown
Contributor

LGTM, but im not a maintainer

@florentinl

Copy link
Copy Markdown
Contributor Author

Still, thank you for taking a look.

@florentinl
florentinl force-pushed the florentin.labelle/add-safe-wrappers-for-context-watching-api branch 2 times, most recently from b253b69 to 2041732 Compare August 13, 2026 13:27
gh-worker-dd-mergequeue-cf854d Bot pushed a commit to DataDog/dd-trace-py that referenced this pull request Aug 13, 2026
## 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>
@florentinl
florentinl force-pushed the florentin.labelle/add-safe-wrappers-for-context-watching-api branch 2 times, most recently from d0a8de6 to cf95e72 Compare August 20, 2026 13:32
@codspeed-hq

codspeed-hq Bot commented Aug 20, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 141 untouched benchmarks
⏩ 6 skipped benchmarks1


Comparing florentinl:florentin.labelle/add-safe-wrappers-for-context-watching-api (68d00cc) with main (fbc8456)

Open in CodSpeed

Footnotes

  1. 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.

@davidhewitt

Copy link
Copy Markdown
Member

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 davidhewitt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened florentinl#1 which has a suggestion how I'd like this API to look.

Also placed a few other comments here.

Comment thread src/context.rs Outdated
/// 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Where do you conclude this? We should make a CPython issue for this if not already present.

Comment thread src/context.rs Outdated
Comment thread src/context.rs Outdated
Comment thread src/context.rs Outdated
florentinl and others added 10 commits August 27, 2026 14:16
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.
@florentinl
florentinl force-pushed the florentin.labelle/add-safe-wrappers-for-context-watching-api branch from cf95e72 to e4ac1c4 Compare August 27, 2026 12:24
@florentinl

Copy link
Copy Markdown
Contributor Author

Thank you a lot @davidhewitt for the thorough review and your design suggestion.

I updated the PR to remove the macro API in favor of the design you suggested and tried to address your other comments.

@florentinl
florentinl requested a review from davidhewitt August 27, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants