Skip to content

Commit

Permalink
Merge branch 'main' into explore-more-fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
MadameSheema authored Mar 21, 2024
2 parents 30f65b6 + c81301b commit b5f49ab
Show file tree
Hide file tree
Showing 50 changed files with 1,155 additions and 263 deletions.
5 changes: 5 additions & 0 deletions docs/user/discover.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,11 @@ For more about this and other rules provided in {alert-features}, go to <<alerti

* <<document-explorer, Configure the chart and document table>> to better meet your needs.

[float]
=== Troubleshooting

* {blog-ref}troubleshooting-guide-common-issues-kibana-discover-load[Learn how to resolve common issues with Discover.]


--
include::{kibana-root}/docs/discover/document-explorer.asciidoc[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ test('getPlugins returns the list of plugins', () => {
expect(pluginsSystem.getPlugins()).toEqual([pluginA, pluginB]);
});

test('getPluginDependencies returns dependency tree of symbols', () => {
test('getPluginDependencies returns dependency tree with keys topologically sorted', () => {
pluginsSystem.addPlugin(createPlugin('plugin-a', { required: ['no-dep'] }));
pluginsSystem.addPlugin(
createPlugin('plugin-b', { required: ['plugin-a'], optional: ['no-dep', 'other'] })
Expand All @@ -138,24 +138,24 @@ test('getPluginDependencies returns dependency tree of symbols', () => {
expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(`
Object {
"asNames": Map {
"no-dep" => Array [],
"plugin-a" => Array [
"no-dep",
],
"plugin-b" => Array [
"plugin-a",
"no-dep",
],
"no-dep" => Array [],
},
"asOpaqueIds": Map {
Symbol(no-dep) => Array [],
Symbol(plugin-a) => Array [
Symbol(no-dep),
],
Symbol(plugin-b) => Array [
Symbol(plugin-a),
Symbol(no-dep),
],
Symbol(no-dep) => Array [],
},
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class PluginsSystem<T extends PluginType> {
private readonly log: Logger;
// `satup`, the past-tense version of the noun `setup`.
private readonly satupPlugins: PluginName[] = [];
private sortedPluginNames?: Set<string>;

constructor(private readonly coreContext: CoreContext, public readonly type: T) {
this.log = coreContext.logger.get('plugins-system', this.type);
Expand All @@ -47,39 +48,41 @@ export class PluginsSystem<T extends PluginType> {
}

this.plugins.set(plugin.name, plugin);

// clear sorted plugin name cache on addition
this.sortedPluginNames = undefined;
}

public getPlugins() {
return [...this.plugins.values()];
}

/**
* @returns a ReadonlyMap of each plugin and an Array of its available dependencies
* @returns a Map of each plugin and an Array of its available dependencies
* @internal
*/
public getPluginDependencies(): PluginDependencies {
const asNames = new Map(
[...this.plugins].map(([name, plugin]) => [
const asNames = new Map<string, string[]>();
const asOpaqueIds = new Map<symbol, symbol[]>();

for (const pluginName of this.getTopologicallySortedPluginNames()) {
const plugin = this.plugins.get(pluginName)!;
const dependencies = [
...new Set([
...plugin.requiredPlugins,
...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)),
]),
];

asNames.set(
plugin.name,
[
...new Set([
...plugin.requiredPlugins,
...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)),
]),
].map((depId) => this.plugins.get(depId)!.name),
])
);
const asOpaqueIds = new Map(
[...this.plugins].map(([name, plugin]) => [
dependencies.map((depId) => this.plugins.get(depId)!.name)
);
asOpaqueIds.set(
plugin.opaqueId,
[
...new Set([
...plugin.requiredPlugins,
...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)),
]),
].map((depId) => this.plugins.get(depId)!.opaqueId),
])
);
dependencies.map((depId) => this.plugins.get(depId)!.opaqueId)
);
}

return { asNames, asOpaqueIds };
}
Expand Down Expand Up @@ -298,67 +301,74 @@ export class PluginsSystem<T extends PluginType> {
return publicPlugins;
}

/**
* Gets topologically sorted plugin names that are registered with the plugin system.
* Ordering is possible if and only if the plugins graph has no directed cycles,
* that is, if it is a directed acyclic graph (DAG). If plugins cannot be ordered
* an error is thrown.
*
* Uses Kahn's Algorithm to sort the graph.
*/
private getTopologicallySortedPluginNames() {
// We clone plugins so we can remove handled nodes while we perform the
// topological ordering. If the cloned graph is _not_ empty at the end, we
// know we were not able to topologically order the graph. We exclude optional
// dependencies that are not present in the plugins graph.
const pluginsDependenciesGraph = new Map(
[...this.plugins.entries()].map(([pluginName, plugin]) => {
return [
pluginName,
new Set([
...plugin.requiredPlugins,
...plugin.optionalPlugins.filter((dependency) => this.plugins.has(dependency)),
]),
] as [PluginName, Set<PluginName>];
})
);

// First, find a list of "start nodes" which have no outgoing edges. At least
// one such node must exist in a non-empty acyclic graph.
const pluginsWithAllDependenciesSorted = [...pluginsDependenciesGraph.keys()].filter(
(pluginName) => pluginsDependenciesGraph.get(pluginName)!.size === 0
);

const sortedPluginNames = new Set<PluginName>();
while (pluginsWithAllDependenciesSorted.length > 0) {
const sortedPluginName = pluginsWithAllDependenciesSorted.pop()!;

// We know this plugin has all its dependencies sorted, so we can remove it
// and include into the final result.
pluginsDependenciesGraph.delete(sortedPluginName);
sortedPluginNames.add(sortedPluginName);

// Go through the rest of the plugins and remove `sortedPluginName` from their
// unsorted dependencies.
for (const [pluginName, dependencies] of pluginsDependenciesGraph) {
// If we managed delete `sortedPluginName` from dependencies let's check
// whether it was the last one and we can mark plugin as sorted.
if (dependencies.delete(sortedPluginName) && dependencies.size === 0) {
pluginsWithAllDependenciesSorted.push(pluginName);
}
}
if (!this.sortedPluginNames) {
this.sortedPluginNames = getTopologicallySortedPluginNames(this.plugins);
}
return this.sortedPluginNames;
}
}

if (pluginsDependenciesGraph.size > 0) {
const edgesLeft = JSON.stringify([...pluginsDependenciesGraph.keys()]);
throw new Error(
`Topological ordering of plugins did not complete, these plugins have cyclic or missing dependencies: ${edgesLeft}`
);
/**
* Gets topologically sorted plugin names that are registered with the plugin system.
* Ordering is possible if and only if the plugins graph has no directed cycles,
* that is, if it is a directed acyclic graph (DAG). If plugins cannot be ordered
* an error is thrown.
*
* Uses Kahn's Algorithm to sort the graph.
*/
const getTopologicallySortedPluginNames = (plugins: Map<PluginName, PluginWrapper>) => {
// We clone plugins so we can remove handled nodes while we perform the
// topological ordering. If the cloned graph is _not_ empty at the end, we
// know we were not able to topologically order the graph. We exclude optional
// dependencies that are not present in the plugins graph.
const pluginsDependenciesGraph = new Map(
[...plugins.entries()].map(([pluginName, plugin]) => {
return [
pluginName,
new Set([
...plugin.requiredPlugins,
...plugin.optionalPlugins.filter((dependency) => plugins.has(dependency)),
]),
] as [PluginName, Set<PluginName>];
})
);

// First, find a list of "start nodes" which have no outgoing edges. At least
// one such node must exist in a non-empty acyclic graph.
const pluginsWithAllDependenciesSorted = [...pluginsDependenciesGraph.keys()].filter(
(pluginName) => pluginsDependenciesGraph.get(pluginName)!.size === 0
);

const sortedPluginNames = new Set<PluginName>();
while (pluginsWithAllDependenciesSorted.length > 0) {
const sortedPluginName = pluginsWithAllDependenciesSorted.pop()!;

// We know this plugin has all its dependencies sorted, so we can remove it
// and include into the final result.
pluginsDependenciesGraph.delete(sortedPluginName);
sortedPluginNames.add(sortedPluginName);

// Go through the rest of the plugins and remove `sortedPluginName` from their
// unsorted dependencies.
for (const [pluginName, dependencies] of pluginsDependenciesGraph) {
// If we managed delete `sortedPluginName` from dependencies let's check
// whether it was the last one and we can mark plugin as sorted.
if (dependencies.delete(sortedPluginName) && dependencies.size === 0) {
pluginsWithAllDependenciesSorted.push(pluginName);
}
}
}

return sortedPluginNames;
if (pluginsDependenciesGraph.size > 0) {
const edgesLeft = JSON.stringify([...pluginsDependenciesGraph.keys()]);
throw new Error(
`Topological ordering of plugins did not complete, these plugins have cyclic or missing dependencies: ${edgesLeft}`
);
}
}

return sortedPluginNames;
};

const buildReverseDependencyMap = (
pluginMap: Map<PluginName, PluginWrapper>
Expand Down
10 changes: 10 additions & 0 deletions packages/core/plugins/core-plugins-server-internal/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import type { PluginName, PluginOpaqueId } from '@kbn/core-base-common';

/** @internal */
export interface PluginDependencies {
/**
* Plugin to dependencies map with plugin names as key/values.
*
* Keys sorted by plugin topological order (root plugins first, leaf plugins last).
*/
asNames: ReadonlyMap<PluginName, PluginName[]>;
/**
* Plugin to dependencies map with plugin opaque ids as key/values.
*
* Keys sorted by plugin topological order (root plugins first, leaf plugins last).
*/
asOpaqueIds: ReadonlyMap<PluginOpaqueId, PluginOpaqueId[]>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
takeUntil,
delay,
} from 'rxjs/operators';
import { sortBy } from 'lodash';
import { isDeepStrictEqual } from 'util';
import type { PluginName } from '@kbn/core-base-common';
import { ServiceStatusLevels, type CoreStatus, type ServiceStatus } from '@kbn/core-status-common';
Expand All @@ -45,7 +44,6 @@ export interface Deps {
interface PluginData {
[name: PluginName]: {
name: PluginName;
depth: number; // depth of this plugin in the dependency tree (root plugins will have depth = 1)
dependencies: PluginName[];
reverseDependencies: PluginName[];
reportedStatus?: PluginStatus;
Expand Down Expand Up @@ -81,7 +79,8 @@ export class PluginsStatusService {
constructor(deps: Deps, private readonly statusTimeoutMs: number = STATUS_TIMEOUT_MS) {
this.pluginData = this.initPluginData(deps.pluginDependencies);
this.rootPlugins = this.getRootPlugins();
this.orderedPluginNames = this.getOrderedPluginNames();
// plugin dependencies keys are already sorted
this.orderedPluginNames = [...deps.pluginDependencies.keys()];

this.coreSubscription = deps.core$
.pipe(
Expand Down Expand Up @@ -216,23 +215,20 @@ export class PluginsStatusService {
private initPluginData(pluginDependencies: ReadonlyMap<PluginName, PluginName[]>): PluginData {
const pluginData: PluginData = {};

if (pluginDependencies) {
pluginDependencies.forEach((dependencies, name) => {
pluginData[name] = {
name,
depth: 0,
dependencies,
reverseDependencies: [],
derivedStatus: defaultStatus,
};
});
pluginDependencies.forEach((dependencies, name) => {
pluginData[name] = {
name,
dependencies,
reverseDependencies: [],
derivedStatus: defaultStatus,
};
});

pluginDependencies.forEach((dependencies, name) => {
dependencies.forEach((dependency) => {
pluginData[dependency].reverseDependencies.push(name);
});
pluginDependencies.forEach((dependencies, name) => {
dependencies.forEach((dependency) => {
pluginData[dependency].reverseDependencies.push(name);
});
}
});

return pluginData;
}
Expand All @@ -248,36 +244,6 @@ export class PluginsStatusService {
);
}

/**
* Obtain a list of plugins names, ordered by depth.
* @see {calculateDepthRecursive}
* @returns {PluginName[]} a list of plugins, ordered by depth + name
*/
private getOrderedPluginNames(): PluginName[] {
this.rootPlugins.forEach((plugin) => {
this.calculateDepthRecursive(plugin, 1);
});

return sortBy(Object.values(this.pluginData), ['depth', 'name']).map(({ name }) => name);
}

/**
* Calculate the depth of the given plugin, knowing that it's has at least the specified depth
* The depth of a plugin is determined by how many levels of dependencies the plugin has above it.
* We define root plugins as depth = 1, plugins that only depend on root plugins will have depth = 2
* and so on so forth
* @param {PluginName} plugin the name of the plugin whose depth must be calculated
* @param {number} depth the minimum depth that we know for sure this plugin has
*/
private calculateDepthRecursive(plugin: PluginName, depth: number): void {
const pluginData = this.pluginData[plugin];
pluginData.depth = Math.max(pluginData.depth, depth);
const newDepth = depth + 1;
pluginData.reverseDependencies.forEach((revDep) =>
this.calculateDepthRecursive(revDep, newDepth)
);
}

/**
* Updates the root plugins statuses according to the current core services status
*/
Expand Down
Loading

0 comments on commit b5f49ab

Please sign in to comment.