Repository navigation
fix(scores): make flush() and shutdown() work under gevent and eventlet - #2032
Closed
DavidTraina wants to merge 1 commit into
Closed
DavidTraina wants to merge 1 commit into
DavidTraina wants to merge 1 commit into
Conversation
DavidTraina
marked this pull request as ready for review
October 20, 2026 23:19
5 tasks done
_join_score_queue() waited on queue.Queue's all_tasks_done Condition. gevent (25.4.1+) and eventlet replace queue.Queue with classes that have no all_tasks_done, so flush() and shutdown() raised AttributeError and shutdown() left the score consumer running. The score consumer now notifies a Condition owned by the resource manager after task_done(), and flush() waits on that instead, so it works with any queue type. flush() still wakes the consumer, returns as soon as the queue drains, and stops waiting once no consumer is alive.
DavidTraina
force-pushed
the
fix/score-queue-join-gevent
branch
from
October 21, 2026 06:23
4053b60 to
6c35c5f
Compare
Collaborator
|
Closing this in favor of #2034 - thanks David! |
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.
What does this PR do?
Since v5.0.3 (#2025),
flush()andshutdown()raiseAttributeErrorwhen gevent or eventlet has monkey-patched the process:_join_score_queue()waits onall_tasks_done, an undocumented Condition of the standard libraryqueue.Queue. Since gevent 25.4.1,monkey.patch_all()replacesqueue.Queuewith gevent's ownQueue, and eventlet'smonkey_patch()replaces it too. Neither replacement hasall_tasks_done. On an app server running gevent workers, this raised on everyflush()and on every worker exit.shutdown()raised before_stop_and_join_consumer_threads(), so the score consumer was never stopped and queued scores were not sent. v5.0.2 calledQueue.join(), which all three queues provide.The fix:
ScoreConsumernotifies athreading.Conditionowned by the resource manager aftertask_done()._join_score_queue()waits on that Condition, with the same 0.1 s timeout and consumer-liveness check as before. It no longer usesall_tasks_done, so the same code runs on every queue type. It still readsunfinished_tasks, which all three queues have.threading, so the wait is cooperative whenever the consumer is a greenlet.#2025's two guarantees hold for every queue type:
flush()still wakes the consumer and returns as soon as the queue drains, because the consumer's notify wakes it. It does not wait forflush_interval.flush()still cannot hang when the consumers stop first. It rechecks every 0.1 s whether a consumer is alive, even when a consumer dies without notifying, as when its greenlet is killed.Considered and not done:
Queue.join(timeout=...)when the queue has noall_tasks_done. It is the smallest change, but eventlet'sjoin()takes no timeout, so it would raiseTypeErrorthere. With gevent 25.9.1 it also froze the whole process whenflush()ran on gevent's hub threadpool with a score queued: the consumer'stask_done()spun in gevent's cross-thread wakeup while holding the GIL. gevent 26.9.0 did not freeze.unfinished_taskswith a short sleep when the queue has noall_tasks_done. It works everywhere, but adds up to one poll interval to everyflush()with pending work. With a 5 ms interval, aflush()with one score over a local HTTP server took 6.0-6.4 ms under gevent, against 0.7 ms with this PR.Queue.join()on a helper thread and waiting on that thread with a timeout. That needs only the public queue API, but it starts a thread on everyflush(), which raised the median emptyflush()from about 9 us to 44 us. It also leaves the helper blocked injoin()when every consumer stops first.queue.Queue. That only covers gevent, so eventlet stays broken, and it opts out of the queue gevent adopted to fix a urllib3 deadlock.Workarounds on v5.0.3 until a release:
flush()works under gevent and eventlet, but waits up toflush_intervalfor pending scores.GEVENT_MONKEY_DISABLE_QUEUE_QUEUE=true. It keeps the standard libraryqueue.Queuefor the whole process, which brings back the queue gevent replaced to fix a urllib3 deadlock. gevent marks this switch as temporary.#2031 adds the same
all_tasks_donewait for the media upload queue. It would raise the same error under gevent.Type of change
Verification
List the main commands you ran:
The three targeted files also pass on Python 3.11.
The three flush tests from #2025 now run twice: once with the standard library
Queueand once withGeventLikeQueue, a test double that wraps a standard libraryQueueand hidesall_tasks_done. Onmain, the gevent cases fail withAttributeError: all_tasks_done.The new
test_flush_returns_when_consumer_stops_while_flush_waitsalso runs for both queue types. It blocks the consumer in an upload whileflush()waits, then stops the consumer without a final notify and with an item still queued, and checks thatflush()returns. It fails if the wait has no timeout or skips the liveness check.ruff format, which pre-commit runs, also removes the trailing whitespace that #2025 left inflush().flush()latency withflush_interval=5, on Python 3.14 and macOS arm64. Each cell is the range of the median across 2-3 runs.mainAttributeErrorAttributeErrorManual gevent check with the script below, run against gevent 25.9.1 and 26.9.0:
mainflush()AttributeErrorshutdown()AttributeError, consumer still aliveflush_interval=5:flush()with one scoreAttributeErrorflush()with nothing queuedAttributeErrorflush()waits, leaving an item unprocessedAttributeErrorflush()on gevent's hub threadpool, one score queuedAttributeErrorThe same scenarios, apart from the threadpool one, pass under eventlet 0.41.3, where
mainraises the sameAttributeError. I also checked thatflush()returns withmonkey.patch_all(time=False), withtime.sleepmocked, from a native OS thread and at interpreter exit. A greenlet running alongsideflush()keeps being scheduled whileflush()waits on a slow POST.Mixing threads is still limited, as on v5.0.2: after a
flush()on gevent's threadpool and another on the main thread, in either order, the second raises gevent'sInvalidThreadUseErrorfrom the media queue'sjoin().gevent check script
Run it with
uv run --frozen --with gevent python gevent_check.pyfrom the checkout.Not run:
tests/e2eandtests/live_provider, which need a server and provider keys. The request bodies are unchanged.Checklist
code_review.md..env.templateif needed. (n/a, no public behavior change)The PR appears safe to merge, with a non-blocking gap in the test for a consumer stopping during the timed wait.
Summary
Adds a timed
join()fallback for score queues withoutall_tasks_done, allowingflush()andshutdown()to work with gevent.Reviews (1) · Last reviewed commit: "fix(scores): make flush() and shutdown()..." · Reviewed by Greptile