Repository navigation
Keep compatibility rescans from refusing work they confirm - #19
Merged
Merged
Conversation
The periodic rescan classified retained items through control-slot pure execution without a lease of its own. When in-flight reads held all four control slots, the capacity refusal was recorded as inventory_incomplete, the process turned restricted and closed its effects until the next rescan. The scan now holds a dedicated inventory slot for its traversal. A rescan that confirmed a ready verdict still withdrew readiness before releasing its catalog connection, so a mutation that reached require_operational() in that window answered 503 WV-COMPATIBILITY. A confirmed ready verdict now stays in force through bounded catalog cleanup and the no-op ready callback. A failed cleanup, a failed callback or an interrupted traversal still withdraws it, and restricted-to-ready and ready-to-restricted transitions keep their order. When a rescan withdraws readiness, the server logs the finding kinds and codes and the class of the error it caught, never messages or connection details. A WV-COMPATIBILITY refusal carries Retry-After with the seconds until the next automatic rescan, and Studio replays reads and keyed changes when that falls within its five-second pause.
This was referenced Oct 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Acceptance runs intermittently got
503 WV-COMPATIBILITYfrom the periodic compatibility rescan. There are two separate causes.scan()withdrew readiness in itsfinallybefore disposing the catalog engine, even when the rescan confirmed "ready". Studio'sPOST .../runswas admitted by the transport, logged its firstrun.startauthorization, then hitrequire_operational()inside that window./health/readyanswered 200 every ~2.3 s for the whole log, and worker claims kept their 260 ms cadence, so the window lasted milliseconds. Catalog disposal takes about 2 ms against a real PostgreSQL.execute_pure(control=True)and no lease of its own. Every in-flight GET holds one of the four control slots for its whole duration, and the Studio host forwards up to eight concurrent requests. When four reads were in flight, classification raisedCatalogError(429, WV-OPERATION-CAPACITY). The scan recorded it asinventory_incomplete, the verdict becamerestricted, effects were closed, and every change answered 503 until the next rescan. In both runs GETs were also getting 429 at the same time. In CI those 429s don't line up with any database lock timeout, so the control slots really do saturate in this flow.What changes
The owner approved these semantics before implementation.
inventory_execution()inoperations/execution.pyis a one-slot lease that the scan holds as the inherited request lease for its traversal. A request burst can no longer refuse classification. If a cancelled scan's classification thread still holds the slot, the next scan is refused, which still fails closed.on_ready. A failed cleanup, a failed callback, or an interrupted traversal still withdraws it. Restricted→ready and ready→restricted transitions keep their existing order.Compatibility rescan withdrew readiness: findings=<kind:code,...> error=<Class [CODE]>. Exception messages and connection details are never logged.Retry-Afteron503 WV-COMPATIBILITY.CatalogErrorgains an optionalretry_after.ConnectorRegistry.require_operational()fills it fromCompatibilityService.retry_after_seconds(), the seconds until the next automatic rescan. BothErrorAdviceand theBodyBoundarymiddleware render it.Idempotency-Key, after503 WV-COMPATIBILITYwithin its existing limit of four attempts, but only whenRetry-Afteris 5 seconds or less. A longer restriction fails at once.reference/api.md(retries),operations/upgrades.md(rescan behavior and the warning) andoperations/troubleshooting.md(a new symptom row).Tests
tests/unit/operations/test_compatibility_rescan.pycovers:Retry-Aftercounting down, and refusals carryingRetry-Aftertests/unit/operations/test_execution.py: the inventory slot is independent of request admission and covers the actual thread lifetime.tests/unit/operations/test_transport.py:Retry-Afteron both the middleware refusal and an in-request refusal.tests/integration/test_operations.pywith the real app and PostgreSQL:POST /runsis admitted while a confirming rescan cleans upRetry-Afterorigin/main, both new integration tests fail for the reasons above:restricted [inventory:inventory_incomplete], andready == Falseduring cleanup.studio/tests/api.test.ts: replay of a keyed change, no replay without a key, no replay for other 503s, and an immediate failure for a longRetry-After.Verification (local)
check_docs.py,source_coverage.py --strict,mkdocs build --strict: passpytest tests/unit tests/contracts: 4389 passed. After review follow-ups,tests/unit/operationsandtests/unit/connectorswere rerun: 422 passed.npm run check,format:check,npm test(778),npm run build,npm run test:browser(921 passed, 6 skipped)tests/integrationagainst a disposable PostgreSQL only: 571 passed, 1 skipped. The other 59 stop at their explicit prerequisites, which were not provided locally: an owned Kafka image, a Keycloak endpoint, the PostgreSQL connector control database, the release Docker context and a built native image.Not run: the PR #15 acceptance harness, which is not on
mainyet.Follow-up
RecoveryLoopand other lifespan loops also borrow request capacity for pure execution. PR #22 (fix/background-loop-admission) handles that withReservedSlots, which reserves one slot per pure call.inventory_execution()stays separate: it holds one lease for the whole traversal so that an overlapping rescan is refused. PR #22 reports that the two merge cleanly.