mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-21 07:21:44 +02:00
## Summary De-flake `test_arun_with_retry_timeout_observer_emits_progress_on_heartbeat` — the test was hitting a CI-runner-load-sensitive race where the idle-timeout watchdog could fire before the task body's first await ran. ## Root cause `_TimedAttemptScope.__init__` sets `_last_progress = time.monotonic()` immediately, but the watchdog itself doesn't start polling until *after* `wrap_config` and task scheduling. Under heavy CI load that gap can grow large enough that: ``` T₀ scope.__init__() → _last_progress = T₀ … some scheduling slack … Tₙ watchdog runs, computes remaining = T₀ + 0.2 − Tₙ ≤ 0 → TimeoutError fires ``` The error reports `elapsed: 0.000s` because `elapsed` is measured from the post-scheduling `start` (≈Tₙ), not from `_last_progress` (T₀). The previous test set `idle_timeout=0.2s`, which left almost no headroom for that scheduling slack. ## Fix (test-side only — no production change) - **Heartbeat at task-body entry**: `runtime.heartbeat()` is now called before the first `await asyncio.sleep(...)`, which resets `_last_progress` to "now" the moment the task body actually starts running. This eliminates the scope-init-to-first-await gap as a flake source. - **Idle timeout 0.2s → 1.0s**: gives ~5× headroom over the ~400ms task duration, so scheduling pressure stays comfortably within budget. ## Why test-side instead of fixing the production race The proper production fix would be to set `_last_progress` at watchdog-entry time rather than at scope-init time. That's a behaviour change in the retry/timeout machinery and out of scope for a flaky-test fix. The two test-side defenses make this particular test stable without touching production semantics; the underlying race in `_TimedAttemptScope` is worth a separate follow-up. ## Test plan - 10/10 repeated local runs pass: ``` uv run pytest tests/test_retry.py::test_arun_with_retry_timeout_observer_emits_progress_on_heartbeat --count=10 ``` - All assertions still meaningful: still verifies start/finish events, at least one progress event, rate-limited progress count (≤ total events), and per-event metadata (task_name, attempt, idle_timeout_secs, progress_at). Co-authored-by: Cursor <cursoragent@cursor.com>