From 45fc3111dc6ae89c744b39e99bc6386c9a8e5351 Mon Sep 17 00:00:00 2001 From: yucheng Date: Tue, 15 Sep 2026 00:00:36 +0000 Subject: [PATCH] fix(langfuse): propagate interrupts raised during deferred client teardown Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/integrations/langfuse/langfuse_sdk.py | 16 +++------- .../langfuse/test_langfuse_sdk.py | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/litellm/integrations/langfuse/langfuse_sdk.py b/litellm/integrations/langfuse/langfuse_sdk.py index 378590aca13..5dc079d2f63 100644 --- a/litellm/integrations/langfuse/langfuse_sdk.py +++ b/litellm/integrations/langfuse/langfuse_sdk.py @@ -342,21 +342,15 @@ def lease_langfuse_client(client: Langfuse) -> Generator[None]: try: yield finally: - _run_teardowns(state, state.release_lease(), propagate_base_exception=False) + _run_teardowns(state, state.release_lease()) -def _run_teardowns( - state: _LangfuseLifecycleState, - clients: tuple[Langfuse, ...], - *, - propagate_base_exception: bool = True, -) -> None: +def _run_teardowns(state: _LangfuseLifecycleState, clients: tuple[Langfuse, ...]) -> None: """Tear down ``clients``, then whatever eviction queued meanwhile, and hand the flag back. A failing ordinary teardown is logged and skipped rather than raised: the thread here is usually a request callback that merely held the last lease, and its request must not fail on eviction's behalf. - Interrupts requeue the unfinished batch and normally propagate, while a callback exception already - in flight takes precedence over an eviction interrupt. + An interrupt requeues the unfinished batch for the next eviction or lease exit and propagates. """ batch = clients # rebind-ok: drains each batch queued while the previous one was being torn down try: @@ -370,9 +364,7 @@ def _run_teardowns( verbose_logger.exception("Langfuse client teardown failed during cache eviction") except BaseException: state.requeue(batch[index:]) - if propagate_base_exception: - raise - return + raise batch = state.next_teardown_batch() finally: state.end_teardown() diff --git a/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py b/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py index 9e091983685..9ea6f34c115 100644 --- a/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py +++ b/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py @@ -722,6 +722,35 @@ def test_teardown_failure_does_not_strand_queued_clients(monkeypatch): assert not state.teardown_in_progress +def test_interrupt_during_deferred_teardown_propagates_and_requeues_the_client(monkeypatch): + """A Ctrl-C landing in the lease exit's teardown must reach the caller, not be swallowed.""" + client = Langfuse(public_key=PUBLIC_KEY, secret_key="sk-original", host="http://127.0.0.1:1") + register_langfuse_client(client) + state = _lifecycle_state(client) + original_teardown = _teardown_langfuse_client + calls = [] + + def teardown(target): + calls.append(target) + if len(calls) == 1: + raise KeyboardInterrupt + original_teardown(target) + + monkeypatch.setattr("litellm.integrations.langfuse.langfuse_sdk._teardown_langfuse_client", teardown) + with pytest.raises(KeyboardInterrupt): + with lease_langfuse_client(client): + shutdown_langfuse_client(client) + + assert state.pending_clients == {client} + assert not state.teardown_in_progress + + with lease_langfuse_client(client): + pass + + assert calls == [client, client] + assert not state.pending_clients + + def test_queued_eviction_waits_for_the_last_of_two_overlapping_leases(): exporter = InMemorySpanExporter() provider = build_isolated_tracer_provider(environment=None, release=None)