You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
LocalRegistryWatcher (components/core/core-registry/.../watcher/LocalRegistryWatcher.java) watches /registry/publicrecursively, registers directories created after startup, and fans every change
out to List<LocalRegistryWatcherHandler>. That interface has zero implementations — in the whole
repository, production and test alike:
$ grep -rn "LocalRegistryWatcherHandler" --include="*.java" . | grep -v /target/
components/core/core-registry/.../LocalRegistryWatcherHandler.java # the interface
components/core/core-registry/.../LocalRegistryWatcher.java # the field, the ctor, six fan-out loops
So every instance starts a thread, opens a WatchService, walks the entire registry once
(Performing initial sync... / Initial sync complete.), and keeps polling it — to hand every event
to an empty list. It has been like that since it was added in #5666 (2026-02-09), together with a
configuration key, DIRIGIBLE_REGISTRY_LOCAL_IGNORED_FOLDERS, that consequently cannot affect anything
observable either.
Why this is worth an issue rather than a deletion PR
Because the capability it would provide is exactly the one the platform is missing, and its absence
has already cost a production defect.
SynchronizationWatcher (core-base) — the thing that decides whether a synchronization pass runs at
all — registers only the registry root, non-recursively:
A file written several folders deep therefore produces no event on Linux, isSynchronizationNeeded()
keeps answering false, and no pass ever runs. A publish is fine (SynchronizationWatcherPublisherHandler
forces one), but anything else that writes the registry is invisible. That is the root cause of #7192:
the external-folder copy wrote deep into the registry, nothing was scheduled, and a client-Java
generation that lost the race against the copy stayed incomplete for the life of the process —
controllers answering 404 with all their sources on disk. PR #7299 fixes it at the writer
(RecursiveFolderWatcher now brackets RegistryMutationTracker and calls SynchronizationWatcher.force()),
deliberately not at the watcher, to keep the blast radius on the feature that had the defect.
So we have a recursive registry watcher that notifies nobody, sitting next to a shallow registry watcher
that everything depends on. The two facts belong together.
What to decide
Three options, in increasing order of ambition:
Delete LocalRegistryWatcher + LocalRegistryWatcherHandler + DIRIGIBLE_REGISTRY_LOCAL_IGNORED_FOLDERS.
Nothing consumes them; this reclaims a thread, a watch service and a full registry walk per boot. The
class also carries real hard-won machinery (the macOS PollingWatchService close deadlock handled in closeOffThread, guarded by LocalRegistryWatcherShutdownTest) which would have to be kept — PR engine-java, core-registry: an external-folder publish schedules a pass, and a broken source no longer zeroes the batch (#7192) #7299
already reuses closeOffThread from RecursiveFolderWatcher, so it cannot simply go with the class.
Make SynchronizationWatcher itself recursive and drop LocalRegistryWatcher — one watcher instead
of two. Broadest, and the riskiest: it needs dynamic registration of new directories, and on a platform
with no native file-event source it is a polling watcher over the whole registry.
My preference is (2) or (1); (3) puts a polling walk of the entire registry on the critical path for the
one platform where the JDK has no native watcher.
Notes for whoever takes it
Whatever is chosen, the trap should be written down: a component that writes the registry outside the
publisher pipeline must bracket the write with RegistryMutationTracker and mark the registry modified,
or its writes are invisible. RegistryMutationTracker's javadoc states half of it already ("If you add
another way to write the registry, bracket the write - do not add a mapping"); engine-java, core-registry: an external-folder publish schedules a pass, and a broken source no longer zeroes the batch (#7192) #7299 adds the other half
to .claude/docs/synchronizer-model.md.
ClasspathExpander also writes deep into registry/public, but at boot before the first pass, so it is
not affected today — it would become a second silent writer the moment that ordering changed.
Summary
LocalRegistryWatcher(components/core/core-registry/.../watcher/LocalRegistryWatcher.java) watches/registry/publicrecursively, registers directories created after startup, and fans every changeout to
List<LocalRegistryWatcherHandler>. That interface has zero implementations — in the wholerepository, production and test alike:
So every instance starts a thread, opens a
WatchService, walks the entire registry once(
Performing initial sync.../Initial sync complete.), and keeps polling it — to hand every eventto an empty list. It has been like that since it was added in #5666 (2026-02-09), together with a
configuration key,
DIRIGIBLE_REGISTRY_LOCAL_IGNORED_FOLDERS, that consequently cannot affect anythingobservable either.
Why this is worth an issue rather than a deletion PR
Because the capability it would provide is exactly the one the platform is missing, and its absence
has already cost a production defect.
SynchronizationWatcher(core-base) — the thing that decides whether a synchronization pass runs atall — registers only the registry root, non-recursively:
A file written several folders deep therefore produces no event on Linux,
isSynchronizationNeeded()keeps answering false, and no pass ever runs. A publish is fine (
SynchronizationWatcherPublisherHandlerforces one), but anything else that writes the registry is invisible. That is the root cause of #7192:
the external-folder copy wrote deep into the registry, nothing was scheduled, and a client-Java
generation that lost the race against the copy stayed incomplete for the life of the process —
controllers answering 404 with all their sources on disk. PR #7299 fixes it at the writer
(
RecursiveFolderWatchernow bracketsRegistryMutationTrackerand callsSynchronizationWatcher.force()),deliberately not at the watcher, to keep the blast radius on the feature that had the defect.
So we have a recursive registry watcher that notifies nobody, sitting next to a shallow registry watcher
that everything depends on. The two facts belong together.
What to decide
Three options, in increasing order of ambition:
Delete
LocalRegistryWatcher+LocalRegistryWatcherHandler+DIRIGIBLE_REGISTRY_LOCAL_IGNORED_FOLDERS.Nothing consumes them; this reclaims a thread, a watch service and a full registry walk per boot. The
class also carries real hard-won machinery (the macOS
PollingWatchServiceclose deadlock handled incloseOffThread, guarded byLocalRegistryWatcherShutdownTest) which would have to be kept — PR engine-java, core-registry: an external-folder publish schedules a pass, and a broken source no longer zeroes the batch (#7192) #7299already reuses
closeOffThreadfromRecursiveFolderWatcher, so it cannot simply go with the class.Wire one handler that forces a synchronization pass, making any deep registry write self-healing
rather than only the external-folder one. This is what the SPI looks designed for. It changes behaviour
on every deployment and every integration test, which is why engine-java, core-registry: an external-folder publish schedules a pass, and a broken source no longer zeroes the batch (#7192) #7299 did not do it — it needs its own
measurement (how often does a pass get scheduled that would not have been?) and its own review.
Make
SynchronizationWatcheritself recursive and dropLocalRegistryWatcher— one watcher insteadof two. Broadest, and the riskiest: it needs dynamic registration of new directories, and on a platform
with no native file-event source it is a polling watcher over the whole registry.
My preference is (2) or (1); (3) puts a polling walk of the entire registry on the critical path for the
one platform where the JDK has no native watcher.
Notes for whoever takes it
publisher pipeline must bracket the write with
RegistryMutationTrackerand mark the registry modified,or its writes are invisible.
RegistryMutationTracker's javadoc states half of it already ("If you addanother way to write the registry, bracket the write - do not add a mapping"); engine-java, core-registry: an external-folder publish schedules a pass, and a broken source no longer zeroes the batch (#7192) #7299 adds the other half
to
.claude/docs/synchronizer-model.md.ClasspathExpanderalso writes deep intoregistry/public, but at boot before the first pass, so it isnot affected today — it would become a second silent writer the moment that ordering changed.
LocalRegistryWatcher.closeOffThreadalong with the class:RecursiveFolderWatcher.destroy()now uses it (PR engine-java, core-registry: an external-folder publish schedules a pass, and a broken source no longer zeroes the batch (#7192) #7299), and it is the fix for a teardown deadlock that only reproduces on macOS.