Skip to content

Registry-write race survives #6641: a publish that does not come through the publisher endpoint is invisible to the mutation tracker #6654

Description

@delchev

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions