Skip to content

[NOGIL] Guard AdminClient against concurrent close() vs method-call races - #2317

Open
Ojasva Jain (ojasvajain) wants to merge 9 commits into
dev_producer_no_gilfrom
dev_admin_no_gil
Open

[NOGIL] Guard AdminClient against concurrent close() vs method-call races#2317
Ojasva Jain (ojasvajain) wants to merge 9 commits into
dev_producer_no_gilfrom
dev_admin_no_gil

Conversation

@ojasvajain

Copy link
Copy Markdown
Member

Problem

An AdminClient instance can be shared across multiple threads — this is a common, previously-supported pattern. One long-standing race in this pattern: one thread destroys the Admin client (setting rk to NULL) while another thread is concurrently calling a librdkafka API on that same rk. This race has always existed, but on free-threaded builds the window widens significantly since there's no GIL to serialize the two threads.

Change
To close this race, every Admin API is now gated using active_calls/closing atomic variables. A CAS (compare-and-swap) construct has been added to exit() to correctly handle concurrent exit() calls. This mirrors the gating already implemented for Producer.

Other changes

  1. APIs shared across clients (e.g. list_topics, set_sasl_credentials) were previously left unguarded. They are now gated for Producer and Admin. Gating is not needed for Consumer yet, since concurrent usage of a Consumer's rk will be restricted in a future PR.

  2. Adds concurrency-related unit and integration test cases.

What

Checklist

  • Contains customer facing changes? Including API/behavior changes
  • Did you add sufficient unit test and/or integration test coverage for this PR?
    • If not, please explain why it is not required

References

JIRA:

Test & Review

Open questions / Follow-ups

* Add CI verification jobs for free-threaded Python 3.14t

  - Add a Semaphore block running source package verification and
    integration tests on CPython 3.14t (classic and consumer group
    protocols). cimpl does not declare free-threading support yet, so
    importing it re-enables the GIL: these jobs validate the 3.14t
    toolchain and packaging until that declaration ships.
  - Skip the CI-only orjson install on free-threaded interpreters: no
    free-threaded orjson wheels exist and the source build would fail;
    the stdlib JSON fallback path stays covered.
  - Add a module-scoped autouse fixture (defined on free-threaded builds
    only) that warns when the GIL is re-enabled around a test module.
    Hard asserts are staged behind TODO FTS markers, to be enabled in the
    same PR that declares Py_MOD_GIL_NOT_USED.

  Interpreter detection follows the free-threading HOWTO:
  https://docs.python.org/3/howto/free-threading-python.html

* Handle deps without free-threaded wheels in test setup

  On free-threaded (no-GIL) builds, the rules and json-fast extras'
  compiled deps (tink, google-re2, grpcio; orjson) ship no free-threaded
  wheels and fail to build from source, so:

  - Add requirements-tests-install-nogil.txt, a variant of
    requirements-tests-install.txt without those extras, and install it
    from source-package-verification.sh when the interpreter is
    free-threaded (detected via Py_GIL_DISABLED).
  - Exclude the schema_registry test modules that import tink/celpy/orjson
    at the top of the file from collection on free-threaded builds only;
    on regular builds a missing dep stays a loud collection error rather
    than a silent skip. Plain serdes coverage recovery is marked as a
    TODO NOGIL follow-up.

* Style fixes
…usions (#2309)

* [NOGIL] Split schema_registry tests to narrow free-threaded test exclusions

  Each of test_avro_serdes.py, test_config.py, test_json_serdes.py, and
  test_proto_serdes.py is split into a plain file (no rules/encryption
  dependency) and a _rules file (CEL/encryption/JSONata-dependent tests).
  This lets the plain tests run on free-threaded (3.14t) builds, where
  tink/celpy/orjson have no free-threaded wheels, while only the _rules
  files stay excluded via conftest.py's collect_ignore.

* Fix isort/black formatting in split schema_registry tests
…stry tests from free-threaded collection

test_azure_aead.py, test_azure_client.py, test_azure_driver.py, and
test_encrypt_executor.py import azure/tink unconditionally at module level
but were missing from conftest.py's free-threaded collect_ignore list,
causing collection errors on 3.14t CI. These tests were added to master
after the original NOGIL schema_registry exclusion list (#2309) landed,
so they weren't accounted for; rebasing onto master surfaced the gap.
Producer.close() previously raced with concurrent produce()/poll()/
flush()/produce_batch()/transaction calls and with itself when called
from multiple threads, both leading to use-after-free/double-free on
the underlying rd_kafka_t handle. Adds an active_calls/closing guard
(Handle_enter_rk_use/Handle_exit_rk_use) so every method that touches
self->rk registers itself before use, and close() drains in-flight
calls before tearing down; a CAS on `closing` ensures only one
concurrent close() call performs the actual teardown, with losing
callers waiting for it to finish rather than racing it.

Adds tests/parallel/test_producer_close_race.py covering each affected
method racing close(), close() racing itself, and close()'s blocking
behavior. Uses pytest-forked (POSIX only) so a regression segfault
fails only that test.

Integration tests against a real broker are still pending.
…aces

Adds the same active_calls/closing gate Producer.close() already has to
AdminClient: every Admin-owned method now enters/exits the gate around
self->rk, and Admin_exit() uses the same CAS-based teardown protocol as
Producer_close(), draining in-flight calls before destroying the client.

Also closes a gap in the functions shared across Producer/Consumer/Admin
(list_topics, list_groups, set_sasl_credentials), which were previously
unguarded entirely -- they're now gated for Producer/Admin (Consumer gating
is tracked separately).

Adds concurrency unit tests (subprocess-isolated, no broker) and real-broker
integration tests covering exit-races-in-flight-call, exit-races-itself, and
shared-instance multi-API usage across threads.
@confluent-cla-assistant

Copy link
Copy Markdown

🎉 All Contributor License Agreements have been signed. Ready to merge.
Please push an empty commit if you would like to re-run the checks to verify CLA status for all contributors.

@sonarqube-confluent

Copy link
Copy Markdown

Quality Gate failed Quality Gate failed

Failed conditions
19.5% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube

@airlock-confluentinc
airlock-confluentinc Bot force-pushed the dev_producer_no_gil branch 2 times, most recently from b0425e3 to 7d63d3b Compare July 31, 2026 08:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant