fix(resource-manager): prevent stale clients from queueing after shutdown - #1826
fix(resource-manager): prevent stale clients from queueing after shutdown#1826trungminhdo4-glitch wants to merge 2 commits into
Conversation
…rage Add deterministic unit tests for post-shutdown admission gating, admission vs shutdown atomicity, concurrent shutdown completion, and media force_flush ordering. Each test uses Event/Barrier synchronization, is bounded, and asserts unfinished_tasks==0 to prevent Queue.join hangs. Also covers MediaManager TOCTOU. Refs langfuse#1799
| if not is_first: | ||
| # Wait indefinitely for the first shutdown to complete. | ||
| # A timeout would hide an incomplete shutdown and violate | ||
| # the invariant that shutdown() only returns after resources | ||
| # are drained and consumers stopped. | ||
| self._shutdown_event.wait() |
There was a problem hiding this comment.
Reentrant shutdown self-deadlocks
When a user-supplied span-export callback synchronously calls shutdown() during the initial shutdown's force_flush(), this branch waits on _shutdown_event from the same thread responsible for eventually setting it, causing shutdown to block indefinitely.
Knowledge Base Used: Client Core
Prompt To Fix With AI
This is a comment left during a code review.
Path: langfuse/_client/resource_manager.py
Line: 650-655
Comment:
**Reentrant shutdown self-deadlocks**
When a user-supplied span-export callback synchronously calls `shutdown()` during the initial shutdown's `force_flush()`, this branch waits on `_shutdown_event` from the same thread responsible for eventually setting it, causing shutdown to block indefinitely.
**Knowledge Base Used:** [Client Core](https://app.greptile.com/personal-org-4986/-/custom-context/knowledge-base/langfuse/langfuse-python/-/docs/client-core.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| with self._state_lock: | ||
| if self._shutdown: | ||
| logger.warning( | ||
| f"Media: Skipping upload for media_id={media._media_id} because the Langfuse client has already been shut down." | ||
| ) | ||
| return |
There was a problem hiding this comment.
Shutdown creates dangling media references
When an application thread is traversing a multi-media payload as another thread reaches begin_shutdown(), later _process_media calls return without queueing uploads while their callers still replace the payload values with media references, causing those referenced media objects never to be uploaded.
Knowledge Base Used:
Prompt To Fix With AI
This is a comment left during a code review.
Path: langfuse/_task_manager/media_manager.py
Line: 297-302
Comment:
**Shutdown creates dangling media references**
When an application thread is traversing a multi-media payload as another thread reaches `begin_shutdown()`, later `_process_media` calls return without queueing uploads while their callers still replace the payload values with media references, causing those referenced media objects never to be uploaded.
**Knowledge Base Used:**
- [Client Core](https://app.greptile.com/personal-org-4986/-/custom-context/knowledge-base/langfuse/langfuse-python/-/docs/client-core.md)
- [Task Manager: Media Upload and Score Ingestion](https://app.greptile.com/personal-org-4986/-/custom-context/knowledge-base/langfuse/langfuse-python/-/docs/task-manager.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary
Fixes a shutdown lifecycle race where a
LangfuseResourceManagercan acceptnew score, trace, or media tasks after its background consumers have stopped.
Those tasks increment
Queue.unfinished_tasks, but no live consumer remains tocall
task_done(), so a laterflush()/shutdown()can block indefinitelyin
Queue.join().This change treats
shutdown()as terminal for the shared resource manager:force_flush();Why not recreate the manager automatically?
A previous approach in #1800 evicted the shut-down manager so a new same-key
client could create fresh workers.
This PR intentionally does not do that.
LangfuseSpanProcessorinstances are registered on the shared OpenTelemetryTracerProvider, which does not expose a normal processor-removal API.
Automatically recreating managers therefore needs a separate processor-lifetime
design to avoid accumulating registered processors.
The current SDK documentation describes
shutdown()as application-terminal,so this PR keeps the smaller contract: post-shutdown work cannot poison queues
or deadlock later flushes.
Regression coverage
Five durable tests (deterministic
Event/Barriersynchronization, no sleep-based timing, assertunfinished_tasks == 0):test_1799_stale_client_score_trace_dropped_after_shutdown— stale score/trace clients cannot enqueue after shutdown;test_1799_admission_vs_shutdown_atomicity— admission vs shutdown is atomic;test_1799_concurrent_shutdown_completion— concurrent shutdown waits for actual completion;test_1799_media_admission_toctou_and_begin_shutdown— media admission cannot cross the final shutdown boundary (tests/unit/test_media.py);test_1799_media_during_force_flush_not_dropped_and_after_shutdown_dropped— media discovered during OTELforce_flush()is still processed before media shutdown.Verification
uv run --frozen pytest tests/unit/test_resource_manager.py— 16 passeduv run --frozen pytest tests/unit/test_media.py— 21 passeduv run --frozen pytest tests/unit/test_otel.py— 65 passed, 2 skippeduv run --frozen ruff check langfuse/_client/resource_manager.py langfuse/_task_manager/media_manager.py tests/unit/test_resource_manager.py tests/unit/test_media.py— passeduv run --frozen ruff format --check langfuse/_client/resource_manager.py langfuse/_task_manager/media_manager.py tests/unit/test_resource_manager.py tests/unit/test_media.py— passeduv run --frozen mypy langfuse --no-error-summary— passedgit diff --check— passedBroader unit run had two pre-existing
test_prompt_atexitfailures also reproducible onmain(not introduced by this change).Refs #1799
Greptile Summary
This PR makes resource-manager shutdown terminal and synchronizes score, trace, and media admission with consumer teardown. It also makes concurrent shutdown callers wait for completion, recreates lifecycle synchronization after forks, and adds deterministic race regression tests.
Confidence Score: 3/5
The PR should not merge until reentrant shutdown and partially admitted media traversal are handled without deadlock or dangling media references.
A callback can re-enter shutdown on the thread that must set its completion event, and an independent media traversal can cross begin_shutdown after rewriting payload values but before their upload jobs are admitted.
Files Needing Attention: langfuse/_client/resource_manager.py and langfuse/_task_manager/media_manager.py
Sequence Diagram
sequenceDiagram participant App as Application thread participant RM as Resource manager participant OTel as Tracer provider participant MM as Media manager participant Q as Background queues App->>RM: shutdown() RM->>RM: set terminal shutdown state RM->>OTel: force_flush() OTel->>MM: discover and enqueue media MM->>Q: enqueue upload jobs RM->>Q: join queues RM->>MM: begin_shutdown() RM->>RM: stop and join consumers RM->>RM: set shutdown completion event Note over App,RM: Concurrent callers wait for completionPrompt To Fix All With AI
Reviews (1): Last reviewed commit: "test(resource-manager): harden #1799 shu..." | Re-trigger Greptile
Context used (3)
Learned From
langfuse/langfuse-python#1387