diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts index c07624826ff83..8130698379eda 100644 --- a/src/core/server/status/plugins_status.test.ts +++ b/src/core/server/status/plugins_status.test.ts @@ -285,6 +285,32 @@ describe('PluginStatusService', () => { ]); }); + it('updates when a plugin status observable emits with the same level but a different summary', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies: new Map([['a', []]]), + }); + const statusUpdates: Array> = []; + const subscription = service + .getAll$() + // the first emission happens right after core services emit (see explanation above) + .pipe(skip(1)) + .subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses)); + + const aStatus$ = new BehaviorSubject({ + level: ServiceStatusLevels.available, + summary: 'summary initial', + }); + service.set('a', aStatus$); + aStatus$.next({ level: ServiceStatusLevels.available, summary: 'summary updated' }); + subscription.unsubscribe(); + + expect(statusUpdates).toEqual([ + { a: { level: ServiceStatusLevels.available, summary: 'summary initial' } }, + { a: { level: ServiceStatusLevels.available, summary: 'summary updated' } }, + ]); + }); + it('emits an unavailable status if first emission times out, then continues future emissions', async () => { const service = new PluginsStatusService( { diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts index 8d042d4cba3f9..d77529f06ddec 100644 --- a/src/core/server/status/plugins_status.ts +++ b/src/core/server/status/plugins_status.ts @@ -71,7 +71,12 @@ export class PluginsStatusService { this.coreSubscription = deps.core$ .pipe(debounceTime(10)) - .subscribe((coreStatus: CoreStatus) => this.updateCoreAndPluginStatuses(coreStatus)); + .subscribe((coreStatus: CoreStatus) => { + this.coreStatus = coreStatus; + this.updateRootPluginsStatuses(); + this.updateDependantStatuses(this.rootPlugins); + this.emitCurrentStatus(); + }); } /** @@ -96,8 +101,19 @@ export class PluginsStatusService { this.reportedStatusSubscriptions[plugin] = status$ // Set a timeout for externally-defined status Observables - .pipe(timeoutWith(this.statusTimeoutMs, status$.pipe(startWith(defaultStatus)))) - .subscribe((status) => this.updatePluginReportedStatus(plugin, status)); + .pipe( + timeoutWith(this.statusTimeoutMs, status$.pipe(startWith(defaultStatus))), + distinctUntilChanged() + ) + .subscribe((status) => { + const levelChanged = this.updatePluginReportedStatus(plugin, status); + + if (levelChanged) { + this.updateDependantStatuses([plugin]); + } + + this.emitCurrentStatus(); + }); } /** @@ -233,16 +249,14 @@ export class PluginsStatusService { } /** - * Updates the core services statuses and plugins' statuses - * according to the latest status reported by core services. - * @param {CoreStatus} coreStatus the latest status of core services + * Updates the root plugins statuses according to the current core services status */ - private updateCoreAndPluginStatuses(coreStatus: CoreStatus): void { - this.coreStatus = coreStatus!; + private updateRootPluginsStatuses(): void { const derivedStatus = getSummaryStatus(Object.entries(this.coreStatus), { allAvailableSummary: `All dependencies are available`, }); + // note that the derived status is the same for all root plugins this.rootPlugins.forEach((plugin) => { this.pluginData[plugin].derivedStatus = derivedStatus; if (!this.isReportingStatus[plugin]) { @@ -250,18 +264,18 @@ export class PluginsStatusService { this.pluginStatus[plugin] = derivedStatus; } }); - - this.updatePluginsStatuses(this.rootPlugins); } /** - * Determine the derived statuses of the specified plugins and their dependencies, - * updating them on the pluginData structure - * Optionally, if the plugins have not registered a custom status Observable, update their "current" status as well. - * @param {PluginName[]} plugins The names of the plugins to be updated + * Update the derived statuses of the specified plugins' dependant plugins, + * If impacted plugins have not registered a custom status Observable, update their "current" status as well. + * @param {PluginName[]} plugins The names of the plugins whose dependant plugins must be updated */ - private updatePluginsStatuses(plugins: PluginName[]): void { - const toCheck = new Set(plugins); + private updateDependantStatuses(plugins: PluginName[]): void { + const toCheck = new Set(); + plugins.forEach((plugin) => + this.pluginData[plugin].reverseDependencies.forEach((revDep) => toCheck.add(revDep)) + ); // Note that we are updating the plugins in an ordered fashion. // This way, when updating plugin X (at depth = N), @@ -276,9 +290,6 @@ export class PluginsStatusService { this.pluginData[current].reverseDependencies.forEach((revDep) => toCheck.add(revDep)); } } - - this.pluginData$.next(this.pluginData); - this.pluginStatus$.next({ ...this.pluginStatus }); } /** @@ -328,15 +339,23 @@ export class PluginsStatusService { * Updates the reported status for the given plugin, along with the status of its dependencies tree. * @param {PluginName} plugin The name of the plugin whose reported status must be updated * @param {ServiceStatus} reportedStatus The newly reported status for that plugin + * @return {boolean} true if the level of the reported status changed */ - private updatePluginReportedStatus(plugin: PluginName, reportedStatus: ServiceStatus): void { - const previousReportedLevel = this.pluginData[plugin].reportedStatus?.level; + private updatePluginReportedStatus(plugin: PluginName, reportedStatus: ServiceStatus): boolean { + const previousReportedStatus = this.pluginData[plugin].reportedStatus; this.pluginData[plugin].reportedStatus = reportedStatus; this.pluginStatus[plugin] = reportedStatus; - if (reportedStatus.level !== previousReportedLevel) { - this.updatePluginsStatuses([plugin]); - } + return previousReportedStatus?.level !== reportedStatus.level; + } + + /** + * Emit the current status to internal Subjects, effectively propagating it to observers. + */ + private emitCurrentStatus(): void { + this.pluginData$.next(this.pluginData); + // we must clone the plugin status to prevent future modifications from updating current emission + this.pluginStatus$.next({ ...this.pluginStatus }); } }