Follow-up to #6641, which closed one instance of this and left a second one open. Three integration tests failed on it in a single day (2026-08-10), each time looking like an unrelated flake in whichever PR happened to be running.
The mechanism
A publish replaces a registry collection by deleting it and copying it back milliseconds later. #6641 taught SynchronizationProcessor to defer artefact cleanup when a registry mutation was in flight during a pass, and taught JavaSynchronizer not to compile a batch it knows is incomplete. Both guards ask RegistryMutationTracker.
But the tracker is fed only by RegistryMutationFilter, which is registered on the /services/ide/publisher/** and workspace endpoints. A publish performed by anything else is invisible to it — and the most common publisher in practice is not that endpoint:
// components/ui/service-generate/.../generate.mjs
lifecycle.publish(user.getName(), workspace, project); // line 107
lifecycle.unpublish(projectName + "/gen/" + genFolderName); // line 195
service-generate/generate.mjs is served from /services/js/..., so the filter never runs, the tracker never counts, and a scheduled pass that lands in the hole deletes artefacts whose sources are about to reappear.
The evidence
From the smoke leg of run 31368339695 (PR #6644), whose build already contained #6641 — verified by ancestry, b5021cf is an ancestor of the run's head:
09:06:13.905 POST /services/js/service-generate/generate.mjs/model/workspace/retire-owner <- not the publisher endpoint
09:06:14.783 [http-nio-auto-1-exec-8] PublisherService - Unpublished ... <- the hole opens
09:06:15.372 [EclipseDirigibleScheduler_Worker-4] JavaSynchronizer - Removing Java artefact
[/retire-owner/gen/invoices/api/invoice/InvoiceController.java] - its source is gone (x6)
09:06:15.463 [http-nio-auto-1-exec-8] PublisherService - Published ... <- the hole closes, 680 ms later
09:06:17.810 Compiled batch: [8] units, [6] class file(s); [2] failed
(gen.events.*.PrintFeeder: package gen.invoices.data.invoice does not exist)
09:06:20.619 Compiled batch: [12] units, [0] class file(s); [12] failed
(cannot find symbol: class InvoiceRepository, location: package gen.invoices.data.invoice)
The 12-unit batch contains the owner's entities, controllers and feeders but never InvoiceRepository / InvoiceItemRepository. Client Java compiles all-or-nothing, so the batch yields zero class files, no controller is registered, and IntentCrossModelFieldRetirementIT's 60 s poll sees only 404s. That 09:06:20 batch was the last one in the run — nothing repaired the state afterwards.
The three failures
| Test |
Where |
Surface symptom |
IntentCrossModelFieldRetirementIT |
PR #6644 smoke |
ConditionTimeout polling a generated controller (404) |
IntentEmissionCoverageIT |
PR #6647 smoke |
"Status": null where the DB default init: 1 was expected — POST ran ~2 s after a republish cycle |
EntityDecoratorsSampleProjectIT |
master nightly |
(separate cause — the pinned OpenAPI version, fixed by #6653) |
The first two are this race. The third is listed only because it was in the same cluster of red and turned out to be unrelated — worth recording so the next person does not conflate them.
Both real symptoms are retry-wrapped, so they surface as ConditionTimeout or a wrong value rather than pointing at the compile. Neither reproduces locally, which is what makes this expensive: it burns reruns on whichever PR is unlucky, and it trains everyone to re-run rather than read.
Proposed fix
Track the mutation where it happens rather than at the URL: bracket PublisherService.publish(...) / unpublish(...) with RegistryMutationTracker.enter() / exit(). That makes every caller correct by construction — the HTTP endpoint, the JS lifecycle API, generated client Java, and tests — instead of requiring each new publish path to remember to register a filter mapping. Enumerating URLs is the bug; the service is the one place all of them funnel through.
Secondary, worth deciding separately: a batch that compiles to zero class files leaves the instance with no client-Java beans and, as this run shows, is not necessarily retried. Making that state self-healing (mark dirty and rebuild on the next pass) would turn a permanent outage into a transient one even when a hole is missed.
Follow-up to #6641, which closed one instance of this and left a second one open. Three integration tests failed on it in a single day (2026-08-10), each time looking like an unrelated flake in whichever PR happened to be running.
The mechanism
A publish replaces a registry collection by deleting it and copying it back milliseconds later. #6641 taught
SynchronizationProcessorto defer artefact cleanup when a registry mutation was in flight during a pass, and taughtJavaSynchronizernot to compile a batch it knows is incomplete. Both guards askRegistryMutationTracker.But the tracker is fed only by
RegistryMutationFilter, which is registered on the/services/ide/publisher/**and workspace endpoints. A publish performed by anything else is invisible to it — and the most common publisher in practice is not that endpoint:service-generate/generate.mjsis served from/services/js/..., so the filter never runs, the tracker never counts, and a scheduled pass that lands in the hole deletes artefacts whose sources are about to reappear.The evidence
From the smoke leg of run 31368339695 (PR #6644), whose build already contained #6641 — verified by ancestry,
b5021cfis an ancestor of the run's head:The 12-unit batch contains the owner's entities, controllers and feeders but never
InvoiceRepository/InvoiceItemRepository. Client Java compiles all-or-nothing, so the batch yields zero class files, no controller is registered, andIntentCrossModelFieldRetirementIT's 60 s poll sees only 404s. That 09:06:20 batch was the last one in the run — nothing repaired the state afterwards.The three failures
IntentCrossModelFieldRetirementITConditionTimeoutpolling a generated controller (404)IntentEmissionCoverageIT"Status": nullwhere the DB defaultinit: 1was expected — POST ran ~2 s after a republish cycleEntityDecoratorsSampleProjectITThe first two are this race. The third is listed only because it was in the same cluster of red and turned out to be unrelated — worth recording so the next person does not conflate them.
Both real symptoms are retry-wrapped, so they surface as
ConditionTimeoutor a wrong value rather than pointing at the compile. Neither reproduces locally, which is what makes this expensive: it burns reruns on whichever PR is unlucky, and it trains everyone to re-run rather than read.Proposed fix
Track the mutation where it happens rather than at the URL: bracket
PublisherService.publish(...)/unpublish(...)withRegistryMutationTracker.enter()/exit(). That makes every caller correct by construction — the HTTP endpoint, the JSlifecycleAPI, generated client Java, and tests — instead of requiring each new publish path to remember to register a filter mapping. Enumerating URLs is the bug; the service is the one place all of them funnel through.Secondary, worth deciding separately: a batch that compiles to zero class files leaves the instance with no client-Java beans and, as this run shows, is not necessarily retried. Making that state self-healing (mark dirty and rebuild on the next pass) would turn a permanent outage into a transient one even when a hole is missed.