fix(cpg): end an engine session with the transaction that owns it (M2)

The first session-enabled replay produced correct detections and no speedup,
reporting session_query_failed:census:session_request_failed on both sides. A
targeted probe ruled out the sequence I suspected: an overlay followed by a census
in one session both answer correctly, with the census confirming the overlays were
saved.

What the probe exposed instead is a leak. A build runs on a copy of the backend, so
the session started during the build belonged to that copy and was never closed,
leaving a JVM holding the configured 2 GB heap for the life of the process. Two
builds per transaction meant two of them, which is enough to exhaust a 3 GB
container and make every later request fail. It also matches the host running out
of memory during these runs.

The build now closes any session its copy started, and the driver ends the session
with the transaction, since that is what transaction-owned means. Controls cover
both: a build leaves nothing alive on the copy that started it, and a completed
scan closes the backend's session. Closing never fails a scan that found things.

This is a genuine defect found by measurement rather than by the suite, which
passed throughout. Whether it was the whole cause of the census failure is not yet
established; the next session-enabled run will say.

1,399 tests passed, nine skipped. Rollout stays NO-GO.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-16 21:15:00 +02:00
co-authored by Claude Opus 5
parent d2362cd18d
commit 566b862b0a
3 changed files with 43 additions and 1 deletions
+27
View File
@@ -290,3 +290,30 @@ def test_a_transport_failure_carries_the_server_log(tmp_path, fake):
assert live.failure == "session_request_failed"
assert "OutOfMemoryError" in live.last_body
assert "OutOfMemoryError" in live.log_tail()
def test_a_build_closes_the_session_it_started(tmp_path, fake, monkeypatch):
"""M2: an unclosed session leaks a JVM holding the configured heap for the whole process."""
from openultrasast.cpg.backend import JoernBackend
closed: list[str] = []
monkeypatch.setattr(EngineSession, "start", lambda self: True)
monkeypatch.setattr(EngineSession, "close", lambda self: closed.append(str(self.scratch)))
backend = JoernBackend(session_transport=True)
# The build runs on a copy, so the session it starts is the copy's to close.
monkeypatch.setattr(JoernBackend, "_build_impl", lambda self, root, **k: self._session_for(tmp_path / "cpg.bin"))
backend.build(tmp_path, execution_budget=ExecutionBudget(time.monotonic() + 20, 1.0))
assert closed, "the build copy left its session open, which leaks a JVM holding the heap"
assert backend._session is None
def test_the_driver_ends_the_session_with_the_transaction(tmp_path):
from openultrasast.cpg.backend import JoernBackend
from openultrasast.model.scan import scan_repository
backend = JoernBackend()
closed: list[bool] = []
backend.close_session = lambda: closed.append(True) # type: ignore[method-assign]
(tmp_path / "a.js").write_text("const x = 1;\n")
scan_repository(tmp_path, [], backend=backend)
assert closed == [True]