Fix: Python 3.14 compatibility#249
Open
griffinmilsap wants to merge 2 commits into
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #235
What happened
The Python 3.14 failure was caused by
ezmsgsubclassingconcurrent.futures.ThreadPoolExecutorand overriding its private_adjust_thread_count()implementation in order to force daemon worker threads for the event loop's default executor.That override depended on private CPython attributes used by older Python versions:
_initializer_initargsIn Python 3.14,
ThreadPoolExecutorchanged its internal worker setup. The executor now uses a worker-context factory path (for example_create_worker_context()) instead of the old initializer fields. As a result, our subclass crashed at runtime with:AttributeError: '_DaemonThreadPoolExecutor' object has no attribute '_initializer'Why we were doing this at all
The custom executor exists to make default-executor threads daemon threads during non-strict shutdown mode.
That behavior helps
ezmsgavoid hanging forever on process shutdown when blocking executor work is still outstanding. In particular, it lets the runtime exit even if some blocking work submitted throughrun_in_executor()does not cooperate with cancellation.So the goal was real and useful:
Why this approach is fragile
The current approach subclasses a stdlib executor and overrides private implementation details.
That is brittle because:
In short: the shutdown intent is valid, but the mechanism is coupled to internals that are prone to breakage.
Minimal fix in this PR
This PR keeps the existing design but makes it compatible with both pre-3.14 and 3.14-style executor internals.
The compatibility patch updates
_DaemonThreadPoolExecutor._adjust_thread_count()to:_initializer/_initargspath on earlier versionsThis is the smallest targeted fix to restore Python 3.14 compatibility without changing shutdown semantics.
Better long-term direction
A better design is to stop replacing the event loop's default executor with a custom
ThreadPoolExecutorsubclass.Option 1: targeted daemon-thread helper (recommended)
Instead of globally swapping the loop's default executor, create a small helper for the specific blocking operations that must not prevent process exit.
For example, a helper could:
threading.Threadasyncio.FutureThis keeps the stdlib executor untouched and confines the special shutdown behavior to the few places that actually need it.
Option 2: explicit daemon threads at known call sites
If only a handful of shutdown-sensitive operations need this behavior, start daemon threads directly at those sites instead of routing them through the loop's default executor.
That is even simpler and avoids a generalized executor abstraction entirely.
Option 3: custom executor implementation
We could replace the subclass with a minimal executor implementation that we own end-to-end.
This would remove the dependency on CPython private
ThreadPoolExecutorinternals, but it is more code and more maintenance surface than the targeted-thread approach.Recommendation
Keep the compatibility fix in this PR because it is small and restores Python 3.14 support quickly.
Then follow up with a refactor that removes
_DaemonThreadPoolExecutorentirely in favor of targeted daemon-thread handling for shutdown-sensitive blocking work. This follow-up is noted in issue #250That would preserve the desired shutdown behavior while avoiding future breakage from stdlib internal changes.