)]}'
{
  "commit": "46ebf5031b751f59337b498ce630df8659177492",
  "tree": "aee8932f774497abb998a63fcf55259822162684",
  "parents": [
    "f891299e6adb7f90155d979907d3787836dd47d9"
  ],
  "author": {
    "name": "ymwang78",
    "email": "wangym@gmail.com",
    "time": "Mon May 25 21:31:14 2026 +0800"
  },
  "committer": {
    "name": "GitHub",
    "email": "noreply@github.com",
    "time": "Mon May 25 09:31:14 2026 -0400"
  },
  "message": "feat(subinterpreter): reusable PyThreadState via subinterpreter_thread_state (#6073)\n\n* feat(subinterpreter): add opt-in TLS-cached thread state mode\n\nsubinterpreter_scoped_activate previously created and destroyed a fresh\nPyThreadState on every activation when the calling OS thread was not\nalready running the target interpreter. Workloads that repeatedly\nre-enter the same sub-interpreter from the same thread therefore churn\nthread states and lose per-thread interpreter state between activations\n(see pybind/pybind11#6040).\n\nAdd an opt-in subinterpreter_thread_state::cached policy: on first use a\nPyThreadState is created and stored in OS-thread-local storage keyed by\nthe target interpreter; subsequent activations on that thread only swap\nit in/out and never destroy it. The default stays transient, so existing\nbehavior is unchanged.\n\nSince pybind11 does not control thread lifetime, cleanup is explicit:\nsubinterpreter::release_cached_thread_state() releases the calling\nthread\u0027s cached state for one interpreter, and the static\nrelease_all_cached_thread_states() releases all of the calling thread\u0027s\ncached states as an end-of-thread hook. The TLS map\u0027s destructor only\nfrees its own nodes and never touches the Python C API, so an\nunreleased state leaks rather than crashing at thread exit.\n\nIncludes test coverage and embedding docs.\n\nCo-Authored-By: Claude Opus 4.7 \u003cnoreply@anthropic.com\u003e\n\n* style: pre-commit fixes\n\n* refactor(subinterpreter): replace cached enum/TLS with subinterpreter_thread_state RAII\n\nAddress review feedback on the original \"cached\" mode by switching to an\nexplicit two-RAII design suggested by @b-pass:\n\n  \"Create a class ... to RAII-manage the PyThreadState but start its\n   lifetime in an already released state. You could create another\n   class (or modify scoped_activate) to scoped/RAII activate the\n   inactive threadstate.\"\n\nRemoved\n  - enum subinterpreter_thread_state { transient, cached } and the\n    defaulted ctor parameter on subinterpreter_scoped_activate.\n  - detail::subinterpreter_thread_state_cache thread_local map.\n  - subinterpreter::release_cached_thread_state() and\n    subinterpreter::release_all_cached_thread_states().\n\nThis eliminates: the hidden per-thread map, the \"release_all\" footgun\nacross pybind11 modules (the cache was module-local), and the implicit\n\"must not be active when called\" contract on the release functions.\n\nAdded\n  - Public class subinterpreter_thread_state that owns one PyThreadState\n    for a given subinterpreter on its constructing OS thread, created in\n    a released state (not current, no GIL). Non-copyable, non-movable\n    (PyThreadState is bound to its creating OS thread).\n  - subinterpreter_scoped_activate(subinterpreter_thread_state \u0026)\n    overload: swaps the owned PyThreadState in on entry, swaps it out\n    on exit, does not touch its lifetime.\n\nBehavior\n  - The existing subinterpreter_scoped_activate(subinterpreter const \u0026)\n    overload is unchanged (still transient: New on entry, Delete on\n    exit). All previously-working code keeps working.\n  - With subinterpreter_thread_state, one OS thread can alternate\n    between multiple subinterpreters and each PyThreadState is preserved\n    across activations -- the use case that gil_scoped_release/acquire\n    + a long-lived scoped_activate cannot solve alone (the per-thread\n    internals.tstate slot holds only one inactive tstate).\n  - The dtor of subinterpreter_thread_state guards against the\n    \"destroyed-while-active\" contract violation: if Swap reveals the\n    cached tstate was current, do not Swap back to a now-deleted\n    pointer (the safe-when-active fix b-pass requested for the old\n    release_* functions, applied at the natural location instead).\n\nLifetime contract is enforced by ordinary C++ scope: typical placement\nis `thread_local`. No new release/cleanup APIs are required.\n\nTests cover (a) tstate identity preserved across activations on a\nthread, (b) transient and reusing modes do not share state, (c)\ndifferent OS threads get distinct PyThreadStates, and (d) the\nmulti-subinterpreter alternation case.\n\nCo-Authored-By: Claude Opus 4.7 \u003cnoreply@anthropic.com\u003e\n\n* fix(subinterpreter): address review on #6073 (same-thread checks, test scoping)\n\nPer @b-pass\u0027s review:\n\n- ~subinterpreter_thread_state(): add a PYBIND11_DETAILED_ERROR_MESSAGES-\n  guarded check that destruction happens on the OS thread that created the\n  PyThreadState (same PyThread_get_thread_native_id pattern as ~subinterpreter),\n  failing with pybind11_fail otherwise.\n- subinterpreter_scoped_activate(subinterpreter_thread_state \u0026): add the\n  matching DETAILED_ERROR_MESSAGES check that activation happens on the\n  creating OS thread, enforcing the newly documented rule.\n- docs: document that activating a subinterpreter_thread_state on another OS\n  thread is illegal.\n- tests: keep each subinterpreter (and its subinterpreter_thread_state) in an\n  enclosing scope so destruction order is thread-state -\u003e subinterpreter -\u003e\n  unsafe_reset_internals_for_single_interpreter(). The previous top-level\n  declarations ran the reset while the subinterpreters were still alive, which\n  is the likely cause of the CI crashes.\n\nCo-Authored-By: Claude Opus 4.7 \u003cnoreply@anthropic.com\u003e\n\n* docs: fix codespell (re-used -\u003e reused) in embedding.rst\n\nCo-Authored-By: Claude Opus 4.7 \u003cnoreply@anthropic.com\u003e\n\n---------\n\nCo-authored-by: Claude Opus 4.7 \u003cnoreply@anthropic.com\u003e\nCo-authored-by: pre-commit-ci[bot] \u003c66853113+pre-commit-ci[bot]@users.noreply.github.com\u003e",
  "tree_diff": [
    {
      "type": "modify",
      "old_id": "c41aec152b7ade36f6076254ad08af2fdb2ab19f",
      "old_mode": 33188,
      "old_path": "docs/advanced/embedding.rst",
      "new_id": "46a1c457c6c996d1596980dd90bd2d34369997ba",
      "new_mode": 33188,
      "new_path": "docs/advanced/embedding.rst"
    },
    {
      "type": "modify",
      "old_id": "547545263e7dd378bf1e459d600250e47eda990e",
      "old_mode": 33188,
      "old_path": "include/pybind11/subinterpreter.h",
      "new_id": "def101ff6233801d55726d6e6db3e77bebc0060f",
      "new_mode": 33188,
      "new_path": "include/pybind11/subinterpreter.h"
    },
    {
      "type": "modify",
      "old_id": "35b7f02334b258b0a59325737fbff7c2cde21e1c",
      "old_mode": 33188,
      "old_path": "tests/test_with_catch/test_subinterpreter.cpp",
      "new_id": "3af100f2a9272a72802fe5cf6152b2f36adec5b0",
      "new_mode": 33188,
      "new_path": "tests/test_with_catch/test_subinterpreter.cpp"
    }
  ]
}
