Skip to content

Commit e7cfec0

Browse files
authored
fix(media): don't hang flush() after a repeated shutdown() (#2033)
1 parent 9ed90ab commit e7cfec0

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

‎langfuse/_task_manager/media_manager.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def process_next_media_upload(self) -> None:
6262
upload_job = self._queue.get(block=True, timeout=1)
6363

6464
if upload_job is _SHUTDOWN_SENTINEL:
65-
self._queue.task_done()
65+
# Already marked done by signal_shutdown().
6666
return
6767

6868
logger.debug(
@@ -92,6 +92,10 @@ def signal_shutdown(self, *, count: int = 1) -> None:
9292
# observe the paused flag on the next loop iteration.
9393
break
9494

95+
# Mark it done now, since no consumer may take it (e.g. after a
96+
# repeated shutdown()) and queue.join() would then never return.
97+
self._queue.task_done()
98+
9599
def _find_and_process_media(
96100
self,
97101
*,

‎tests/unit/test_resource_manager.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Test the LangfuseResourceManager and get_client() function."""
22

33
import logging
4+
import threading
45
from queue import Queue
56
from types import SimpleNamespace
67
from typing import Sequence
78
from unittest.mock import Mock
89

10+
import pytest
911
from opentelemetry.sdk.trace import ReadableSpan
1012
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
1113

@@ -471,3 +473,36 @@ def signal_shutdown(self, *, count):
471473
("join", 0),
472474
("join", 1),
473475
]
476+
477+
478+
@pytest.mark.filterwarnings("error::pytest.PytestUnhandledThreadExceptionWarning")
479+
@pytest.mark.parametrize("concurrent", [False, True], ids=["sequential", "concurrent"])
480+
def test_flush_returns_after_repeated_shutdown(concurrent):
481+
client = Langfuse(
482+
public_key="pk-repeated-shutdown",
483+
secret_key="sk-repeated-shutdown",
484+
span_exporter=NoOpSpanExporter(),
485+
)
486+
media_queue = client._resources._media_upload_queue
487+
shutdowns = [
488+
threading.Thread(target=client.shutdown, daemon=True) for _ in range(2)
489+
]
490+
491+
for shutdown in shutdowns:
492+
shutdown.start()
493+
if not concurrent:
494+
shutdown.join(timeout=5)
495+
for shutdown in shutdowns:
496+
shutdown.join(timeout=5)
497+
498+
flush = threading.Thread(target=client.flush, daemon=True)
499+
flush.start()
500+
flush.join(timeout=2)
501+
hung = [thread for thread in (*shutdowns, flush) if thread.is_alive()]
502+
# Acknowledge shutdown signals that no consumer took, so a hung thread and
503+
# the reset() in the autouse fixture can finish.
504+
while media_queue.unfinished_tasks:
505+
media_queue.get_nowait()
506+
media_queue.task_done()
507+
508+
assert not hung

0 commit comments

Comments
 (0)