[Server][Capability] Do not announce an externally loaded registry as changed (#490)
* [Capability] Do not announce an externally loaded registry as changed `Registry` suppresses its `*ListChangedEvent`s while it is loading — `dispatch()` returns early on the `loading` guard, so the elements a loader registers are the registry's initial contents rather than a change to them. `testListChangedEventsAreSuppressedDuringTheDeferredLoad` pins that. The guard only covers `Registry::load()`, which needs the loader the constructor took. A registry the caller built cannot be given one that way, so `Builder::resolve()` loads it from the outside instead — by calling `$chainLoader->load($registry)` directly, which never sets the guard. Every element then dispatches on the way in. With a notification bus configured that is not quiet. `PublishingEventDispatcher` turns each event into a published notification, so every `build()` puts one `list_changed` per element on the bus for a registry that did not change. Under PHP-FPM, where the server is built per request and `Psr16NotificationBus` is shared and persistent, every request broadcasts its whole element list to every open `subscriptions/listen` stream and consumes the 256-entry backlog — after which a reader that fell behind silently skips real notifications. Split the guarded body of `load()` into `loadFrom(LoaderInterface $loader)` and route the custom-registry branch through it. `loadFrom()` takes only the `loading` guard, not the `loaded` bookkeeping, which stays with `load()`. The loader it runs belongs to the caller, so it cannot stand in for the one the registry was constructed with: marking the registry loaded would retire a constructor loader that never ran, and would make a second `loadFrom()` — one registry handed to two builders — a silent no-op. Keeping `loaded` out of it also keeps `load()`'s promise that a transient failure is retried on the next read, including when the failing loader reads the registry during its own run. `RegistryInterface` declares no `load()`, so this stays on the concrete `Registry`, and a third-party implementation keeps the path it has today. * Apply batched suggestions from code review Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
This commit is contained in:
parent
46628fbe6a
commit
c5dbfb64e5
2 changed files with 134 additions and 4 deletions
|
|
@ -88,10 +88,27 @@ final class Registry implements RegistryInterface
|
|||
return;
|
||||
}
|
||||
|
||||
$this->loadFrom($this->loader);
|
||||
|
||||
$this->loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs $loader with the change events its registrations would dispatch suppressed, since they
|
||||
* describe the registry filling up rather than changing.
|
||||
*
|
||||
* Re-entrant-safe, and failure propagates. Does not mark the registry loaded: $loader is the
|
||||
* caller's, and the one the constructor took is still owed its run.
|
||||
*/
|
||||
public function loadFrom(LoaderInterface $loader): void
|
||||
{
|
||||
if ($this->loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->loading = true;
|
||||
try {
|
||||
$this->loader->load($this);
|
||||
$this->loaded = true;
|
||||
$loader->load($this);
|
||||
} finally {
|
||||
$this->loading = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -522,7 +522,8 @@ final class Builder
|
|||
*
|
||||
* Lazy (the default) defers loading to the first registry read so a persistent runtime does not
|
||||
* freeze the registry to a source not yet ready at build time. Disable to load eagerly at build.
|
||||
* A registry supplied via setRegistry() is always loaded eagerly.
|
||||
* A registry supplied via setRegistry() is always loaded eagerly; its own constructor loader,
|
||||
* if it has one, still runs on the first read.
|
||||
*/
|
||||
public function setLazyLoading(bool $lazyLoading = true): self
|
||||
{
|
||||
|
|
@ -1045,8 +1046,13 @@ final class Builder
|
|||
|
||||
if ($this->hasCustomRegistry) {
|
||||
// Builder can't inject the loader into an already-constructed instance, so load it eagerly.
|
||||
// Via loadFrom(), which suppresses the change events the load would otherwise dispatch.
|
||||
$registry = $this->registry;
|
||||
$chainLoader->load($registry);
|
||||
if ($registry instanceof Registry) {
|
||||
$registry->loadFrom($chainLoader);
|
||||
} else {
|
||||
$chainLoader->load($registry);
|
||||
}
|
||||
$eagerlyLoaded = true;
|
||||
} else {
|
||||
$registry = new Registry($eventDispatcher, $logger, loader: $chainLoader);
|
||||
|
|
|
|||
Loading…
Reference in a new issue