registry: a deep registry write schedules a synchronization pass (#7303) - #7337
Merged
iliyan-velichkov merged 2 commits intoSep 11, 2026
Merged
Conversation
LocalRegistryWatcher watched /registry/public recursively and handed every event to List<LocalRegistryWatcherHandler>, an interface with zero implementations - so each instance started a thread, opened a watch service, walked the whole registry once and kept polling it, to notify nobody. Next to it sits SynchronizationWatcher, which decides whether a pass runs at all and registers the registry root NON-recursively: a file written several folders deep produces no event, so nothing is ever scheduled for it. Only a publish (SynchronizationWatcherPublisherHandler forces a pass) and the external-folder copy (#7299) reported themselves; anything else was invisible for the life of the process - the shape of #7192, a partial client-Java generation that stayed installed with every source on disk. So the SPI is dropped and the watcher is given the one job it was next to all along: it marks the registry modified on every create/modify/delete under it, which makes any deep writer self-healing. The six fan-out loops and the boot-time walk go with the interface - a pass reconciles the whole registry, so a per-file notification is not needed and the walk was only there to feed one. DIRIGIBLE_REGISTRY_LOCAL_IGNORED_FOLDERS now means something observable: those top-level folders are neither watched nor reported. This does not retire RegistryMutationTracker. A pass is now scheduled while a multi-file write is still arriving, and only the bracket tells that pass to defer its cleanup instead of reaping artefacts whose sources have not landed yet - a component that writes the registry outside the publisher pipeline still brackets the write, it just no longer has to remember to announce it. Two details that are load-bearing. A new folder is REGISTERED before it is reported: a file written into it before the registration produces no event of its own and is only covered because the pass the report schedules walks the subtree afterwards. And an OVERFLOW now reports instead of being skipped - events were dropped, so what changed is unknown, which is when a pass is most needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ten (#7303) How many events one write produces is the platform's business: inotify reports a created file as ENTRY_CREATE and again as ENTRY_MODIFY, while the polling watch service macOS falls back to reports it once. The assertions were written against the macOS count, so on the Linux CI leg two forces met a verification wanting exactly one and the two single-write cases failed with TooManyActualInvocations after burning the full 60s timeout. Marking the registry modified is idempotent, so the count carries nothing worth pinning down - every assertion is now atLeastOnce(), and the steps of a sequence are separated by clearing the recorded calls instead of by counting them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
iliyan-velichkov
deleted the
issue-7303-registry-watcher-forces-sync
branch
September 11, 2026 14:55
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.
Cause
LocalRegistryWatcherwatched/registry/publicrecursively, registered folders created after startup, and fanned every event out toList<LocalRegistryWatcherHandler>- an interface with zero implementations anywhere in the repository. Every instance therefore started a thread, opened aWatchService, walked the entire registry once and kept polling it, in order to hand each event to an empty list. Its configuration key,DIRIGIBLE_REGISTRY_LOCAL_IGNORED_FOLDERS, could not affect anything observable either.Next to it sits the watcher everything depends on.
SynchronizationWatcherdecides whether a synchronization pass runs at all, and it registers the registry root non-recursively: a file written several folders deep produces no event,isSynchronizationNeeded()keeps answering false, and no pass is ever scheduled for it. A publish was covered (SynchronizationWatcherPublisherHandler.afterPublishforces one) and, since #7299, so is the external-folder copy - anything else that wrote the registry was invisible for the life of the process. That is the shape of #7192: a client-Java generation that lost the race against the copy stayed partial, controllers answering 404 with all their sources on disk.The change
Option (2) of the issue. The dead SPI is dropped and the recursive watcher is given the one job it was sitting next to all along: it marks the registry modified on every create / modify / delete under
/registry/public, so any deep writer becomes self-healing rather than only the two that remember to announce themselves.LocalRegistryWatcherHandlerand the six fan-out loops are gone, and so is the boot-time walk of the whole registry - a pass reconciles the entire registry, so a per-file notification is not needed and the walk existed only to produce one. The class is 177 lines shorter.DIRIGIBLE_REGISTRY_LOCAL_IGNORED_FOLDERSnow means something observable: those top-level folders are neither watched nor reported (asserted end to end, see below).closeOffThread, the stop-loop-then-close ordering) is untouched -RecursiveFolderWatcher.destroy()uses it andLocalRegistryWatcherShutdownTeststill guards it.Two details are load-bearing and are commented as such:
OVERFLOWnow reports instead of being skipped. Events were dropped, so what changed is unknown - which is when a pass is most needed.This does not retire
RegistryMutationTracker. A pass is now scheduled while a multi-file write is still arriving, and only the bracket tells that pass to defer its cleanup instead of reaping artefacts whose sources have not landed yet. A component that writes the registry outside the publisher pipeline still brackets the write; what it no longer has to do is remember to announce it..claude/docs/synchronizer-model.mdsays so, replacing the paragraph that described the gap.Verification
RegistryDeepWriteSyncIT(HTTP-only, untagged, so it runs in the PR smoke gate): writes a client-Java source several folders deep straight onto the registry's file system path - no publish, noforceProcessSynchronizers(), no external folder - and waits for the endpoint to serve it. Green in 38 s. It genuinely bites: re-run withDIRIGIBLE_REGISTRY_LOCAL_IGNORED_FOLDERSnaming the test's project (which suppresses exactly this report) it times out on a 404 at 208 s, which is also the end-to-end proof that the ignore key works.LocalRegistryWatcherTest(4 cases): a file written deep schedules a pass; a folder created after startup is watched too and its content schedules a pass; a deletion schedules a pass; a change under an ignored top-level folder schedules nothing.LocalRegistryWatcherShutdownTest(3) andRecursiveFolderWatcherTest(2) still green;components/core/core-registry,core-initializers,core-baseunit suites green withclean.ExternalRegistryFolderSyncIT,SynchronizerCleanupRaceIT,JavaEngineIT,CsvimIdentityRestartIT,RoleSynchronizerCleanupIT,IntentEngineIT(80),IntentEmissionCoverageIT,ModelGenerationIT,DependencyResolutionIT,PerspectiveGroupAggregationIT,ArtefactStatusEndpointIT.mvn formatter:formaton the changed modules, thenmvn -T 1C formatter:validatewith the formatter cache wiped: SUCCESS. Javadoc release profile oncore-registry -am: noerror:.Not verified: the full UI / sample-project suite, and PostgreSQL. Both are worth watching on this PR specifically, because the issue's own reservation about this option was that it changes behaviour on every deployment and every integration test - passes are now scheduled that previously were not. The measured shape of that cost is one flag write per event and one pass per batch (
force()sets anAtomicBoolean; the processor decides when to run), and theSynchronizationUtil.waitForStableSynchronization()quiet period already exists to absorb trailing watcher events.Pre-existing flake noticed, not touched:
RecursiveFolderWatcherTest.a_file_added_deep_in_the_tree_afterwards_schedules_another_passfails intermittently (1 in ~3 full-module runs, green in isolation).registryChanged()also fires for theENTRY_MODIFYon the containing directory, whose mtime moves when the new file is created, so the test'satLeast(2)).force()can return beforesecond.txthas been copied and theFiles.existsassertion that follows races. The production ordering is correct (copy, then report); the race is in the assertion. Left alone as out of scope - happy to fix it in a follow-up.Fixes #7303
🤖 Generated with Claude Code