fix(queue): use a thread lock for JobQueue across event loops - #76
fix(queue): use a thread lock for JobQueue across event loops#76SebTardif wants to merge 1 commit into
Conversation
Space UI calls asyncio.run(queue.submit()) on a new loop while the eval worker holds the same lock on its own loop. asyncio.Lock is loop-bound, so public submit can hang or raise. Use threading.Lock and release it before Hub uploads. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
🦞👀 Pull request received. I will update this pull request when review starts. |
|
Codex review: blocked before merge. Reviewed September 3, 2026, 5:51 AM ET / 09:51 UTC. ClawSweeper reviewWhat this changesThe PR replaces JobQueue’s event-loop-bound lock with a thread lock, releases it before Hugging Face dataset synchronization, and adds cross-event-loop queue tests. Regression provenancePossible regression — suspected (reviewed change). No predecessor PR is attributed. Merge readiness⛔ Blocked before merge - 5 items remain The cross-event-loop lock fix is useful and its supplied terminal trace demonstrates the intended path, but this unchanged head still allows concurrent remote queue uploads to complete out of order and restore stale job state. The prior P1 finding therefore remains a merge blocker. Priority: P1 Review scores
Verification
How this fits togetherJobQueue accepts submissions from the Gradio Space and claims/progress updates from the background evaluation worker. It saves queue state locally and publishes a shared snapshot to a Hugging Face dataset so job state can be restored after restart. flowchart LR
UI[Space submission] --> Queue[Job queue]
Worker[Evaluation worker] --> Queue
Queue --> Save[Locked local snapshot]
Save --> Upload[Dataset snapshot upload]
Upload --> Dataset[Hugging Face dataset]
Dataset --> Recovery[Restart recovery]
Before merge
Findings
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Copy recommended automerge instructionTechnical reviewBest possible solution: Keep the fast cross-loop mutation path, but assign ordered snapshots under the queue lock and serialize or version publication so only the newest Hugging Face snapshot can become the restart source of truth; cover delayed old-upload completion. Do we have a high-confidence way to reproduce the issue? Yes. A deterministic delayed first dataset upload followed by a later queue transition can make the later upload complete first and then let the captured older snapshot overwrite queue/jobs.json; startup then applies that remote row after local load. Is this the best way to solve the issue? No. A thread lock resolves the cross-event-loop lock ownership problem, but releasing all uploads without ordered publication creates a stale persistent-state path; serialized or revision-gated snapshots are the narrower safe solution. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: not found in the target repository. Codex review notes: model internal, reasoning high; reviewed against c1a79f731541. LabelsLabel justifications:
EvidenceAcceptance criteria:
What I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (5 earlier review cycles)
|
What Problem This Solves
The Hugging Face Space UI submits jobs with
asyncio.run(queue.submit())on a brand-new event loop. The backgroundEvalWorkeralready ownsJobQueue._lockon a different loop. That lock was anasyncio.Lock, which is bound to the loop that first used it.When a visitor clicks Submit while the worker is claiming a job or writing queue state, the public submit path can hang until the Space is restarted, or it can raise that the lock belongs to another loop. The form looks stuck even though the request already reached
submit_model.This change switches
JobQueueto athreading.Lockso Gradio's per-clickasyncio.run()and the worker loop can share the same queue. The lock now covers only in-memory mutation and the localjobs.jsonwrite. Hub uploads run after release, so a slow Hugging Face dataset push cannot pin the Space submit button.Evidence
Public call chain: Gradio
submit_model/submit_all_presetsinapp.pycallasyncio.run(queue.submit()). The worker thread callsclaim_pendingandupdate_progresson its own loop. Both paths used the sameasyncio.Lock.Before (same two-
asyncio.run()shape as Space submit vs worker):After (patched
JobQueue, sameasyncio.run(queue.submit())asapp.py):The lock has been an
asyncio.Locksince the initial queue in1df8c43(2026-04-07). No open pull request already changes this file for the same hang.Real behavior proof
Behavior or issue addressed: Space Submit can hang or raise when
asyncio.run(queue.submit())runs on a new loop while the eval worker holdsJobQueue._lockon another loop.Real environment tested: macOS Darwin 25.6.0 arm64, Python 3.14.7, checkout
/tmp/oc-pr-shellbench-F003at branchfix/f003-jobqueue-thread-lock, noHF_TOKEN.Exact steps or command run after this patch:
Evidence after fix: terminal output from the patched tree:
Observed result after fix:
JobQueue._lockis_thread.lock. A second-loopasyncio.run(submit)returned a pending job in 56.1ms after the worker released the lock. A laterasyncio.run(submit)returned in 0.5ms while a previous hub sync was still blocked, so the lock is not held across uploads.What was not tested: A live Hugging Face Space with a real dataset upload, Gradio click-through, or an in-progress native eval on the public Space.
Changes
asyncio.Lockwiththreading.LockonJobQueue._sync_to_hub().python -m ruff check clawbench/queue.py tests/test_queue.pypassed on the changed files.