From 18d0319436ade0495e8a3ea9b1a3e1ffc1e611c8 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:26:54 +0200 Subject: [PATCH] fix(cpg): a query that exceeds its own ceiling no longer cancels the scan Two different timeouts reached the same handler and got the same consequence. If the SCAN deadline is gone, cancellation is right: stop everything promptly, which is what the push two-second gate measures. If a single query merely exceeded its own per-query ceiling, the scan may have most of its budget left and every other batch is independent and cheaper -- and cancelling took them all. Measured on Paid Memberships Pro: one batch of 116 requests hit a 300 s ceiling 734 s into a 2,700 s scan, and the four batches behind it returned in 0.0 s unanswered with thirty-three minutes to spare. The handler already distinguished the two cases in the reason it recorded, deadline_exhausted against process_timeout, and then set the cancel deadline in both. Fourth instance this week of one rule, now written in the design: a failure is owned by the narrowest scope that caused it. 1,420 tests pass, eighteen skipped, including the cancellation gate. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo --- src/openultrasast/cpg/backend.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/openultrasast/cpg/backend.py b/src/openultrasast/cpg/backend.py index f29defb..c8082e3 100644 --- a/src/openultrasast/cpg/backend.py +++ b/src/openultrasast/cpg/backend.py @@ -1318,8 +1318,18 @@ class JoernBackend: return None return result except subprocess.TimeoutExpired: - self._cancel_deadline = min(self._cleanup_limit(), time.monotonic() + self.execution_budget.cancellation_allowance_seconds) - self._note("deadline_exhausted" if time.monotonic() >= self.execution_budget.deadline_monotonic else "process_timeout") + # Two different timeouts end here and they must not have the same consequence. If the SCAN's + # deadline is gone, cancellation is right: stop everything promptly, which is what the push's + # two-second gate measures. If a single query merely exceeded its own ceiling, the scan may have + # most of its budget left and every other batch is independent and cheaper -- and cancelling took + # them all. Measured 2026-09-22: one batch of 116 requests hit a 300 s ceiling 734 s into a + # 2,700 s scan, and the four batches behind it returned in 0.0 s unanswered with 33 minutes to + # spare. Fourth instance of one rule this week: a failure is owned by the narrowest scope that + # caused it. + exhausted = time.monotonic() >= self.execution_budget.deadline_monotonic + if exhausted: + self._cancel_deadline = min(self._cleanup_limit(), time.monotonic() + self.execution_budget.cancellation_allowance_seconds) + self._note("deadline_exhausted" if exhausted else "process_timeout") return None except (OSError, subprocess.SubprocessError): self._note("process_failed")