Skip to content

Commit 4053b60

Browse files
committed
fix(scores): poll gevent's queue instead of using its join(timeout)
When flush() ran on a gevent hub threadpool thread with a score pending, gevent's Event-based join(timeout) made the consumer's task_done() loop forever in gevent's cross-thread wakeup while holding the GIL, freezing the process. v5.0.2's join() raised LoopExit there instead. The gevent path now polls unfinished_tasks with time.sleep(): sleep(0) first, so the consumer can take the wake signal and an idle flush() returns right away, then every 5 ms. The stdlib path is unchanged.
1 parent 3c75969 commit 4053b60

2 files changed

Lines changed: 34 additions & 31 deletions

File tree

‎langfuse/_client/resource_manager.py‎

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
import os
1919
import sys
2020
import threading
21+
import time
2122
import urllib.request
2223
import weakref
23-
from contextlib import nullcontext
2424
from queue import Full, Queue
2525
from typing import Any, Callable, Dict, List, Literal, Optional, cast
2626

@@ -610,25 +610,37 @@ def _join_score_queue(self) -> None:
610610
Queue.join() would block forever when the consumers stop first, e.g.
611611
when shutdown() runs concurrently and a wake signal is never taken.
612612
"""
613-
score_queue = self._score_queue
614-
# gevent's monkey-patched Queue has no all_tasks_done, but its join()
615-
# takes a timeout.
616-
all_tasks_done = getattr(score_queue, "all_tasks_done", None)
617-
618-
with nullcontext() if all_tasks_done is None else all_tasks_done:
619-
while score_queue.unfinished_tasks:
620-
if not any(c.is_alive() for c in self._score_consumers):
621-
langfuse_logger.debug(
622-
"Score consumers stopped with %s queue item(s) unprocessed",
623-
score_queue.unfinished_tasks,
624-
)
625-
626-
return
627-
628-
if all_tasks_done is None:
629-
score_queue.join(timeout=0.1) # type: ignore[call-arg]
630-
else:
631-
all_tasks_done.wait(timeout=0.1)
613+
all_tasks_done = getattr(self._score_queue, "all_tasks_done", None)
614+
615+
if all_tasks_done is None:
616+
# gevent's patched Queue has no all_tasks_done, and its join()
617+
# freezes the process when flush() runs on a hub threadpool thread.
618+
# sleep(0) first lets the consumer take the wake signal, so an idle
619+
# flush() returns right away.
620+
time.sleep(0)
621+
while self._should_wait_for_score_queue():
622+
time.sleep(0.005)
623+
624+
return
625+
626+
with all_tasks_done:
627+
while self._should_wait_for_score_queue():
628+
all_tasks_done.wait(timeout=0.1)
629+
630+
def _should_wait_for_score_queue(self) -> bool:
631+
unfinished_tasks = self._score_queue.unfinished_tasks
632+
if not unfinished_tasks:
633+
return False
634+
635+
if any(c.is_alive() for c in self._score_consumers):
636+
return True
637+
638+
langfuse_logger.debug(
639+
"Score consumers stopped with %s queue item(s) unprocessed",
640+
unfinished_tasks,
641+
)
642+
643+
return False
632644

633645
def shutdown(self) -> None:
634646
self._shutdown = True

‎tests/unit/test_score_consumer.py‎

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import time
77
from datetime import datetime, timezone
88
from queue import Queue
9-
from typing import Any, Callable, Dict, List, Optional, Sequence
9+
from typing import Any, Callable, Dict, List, Sequence
1010

1111
import httpx
1212
import pytest
@@ -419,10 +419,7 @@ def test_create_score_posts_to_scores_endpoint(client_factory):
419419

420420

421421
class GeventLikeQueue:
422-
"""queue.Queue as gevent's monkey patching replaces it.
423-
424-
It has no all_tasks_done Condition, and join() takes a timeout.
425-
"""
422+
"""queue.Queue as gevent's monkey patching replaces it: no all_tasks_done."""
426423

427424
def __init__(self, maxsize: int = 0) -> None:
428425
self._queue: Queue = Queue(maxsize)
@@ -432,12 +429,6 @@ def __getattr__(self, name: str) -> Any:
432429
raise AttributeError(name)
433430
return getattr(self._queue, name)
434431

435-
def join(self, timeout: Optional[float] = None) -> bool:
436-
with self._queue.all_tasks_done:
437-
return self._queue.all_tasks_done.wait_for(
438-
lambda: not self._queue.unfinished_tasks, timeout
439-
)
440-
441432

442433
@pytest.fixture(params=[Queue, GeventLikeQueue], ids=["stdlib", "gevent"])
443434
def queue_class(request, monkeypatch):

0 commit comments

Comments
 (0)