Add an option to disable preboot phase (#179057)

## Summary

Fix https://github.com/elastic/kibana/issues/178180

- Add a new `core.lifecycle.disablePreboot` (internal) config option to
forcefully disable Core's `preboot` phase.
- Enable the option in the serverless configuration file

Gain is around 150/200ms on local developer machine, which translates to
~300/500ms on serverless environment
This commit is contained in:
Pierre Gayvallet 2024-03-22 15:23:48 +01:00 committed by GitHub
parent ca9c4bd3ea
commit dcba4d08f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 231 additions and 58 deletions

View file

@ -11,6 +11,9 @@ xpack.fleet.internal.retrySetupOnBoot: true
# Cloud links
xpack.cloud.base_url: 'https://cloud.elastic.co'
# Disable preboot phase for serverless
core.lifecycle.disablePreboot: true
# Enable ZDT migration algorithm
migrations.algorithm: zdt

View file

@ -95,20 +95,24 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
}
try {
const { preboot } = await root.preboot();
const prebootContract = await root.preboot();
let isSetupOnHold = false;
// If setup is on hold then preboot server is supposed to serve user requests and we can let
// dev parent process know that we are ready for dev mode.
const isSetupOnHold = preboot.isSetupOnHold();
if (process.send && isSetupOnHold) {
process.send(['SERVER_LISTENING']);
}
if (prebootContract) {
const { preboot } = prebootContract;
// If setup is on hold then preboot server is supposed to serve user requests and we can let
// dev parent process know that we are ready for dev mode.
isSetupOnHold = preboot.isSetupOnHold();
if (process.send && isSetupOnHold) {
process.send(['SERVER_LISTENING']);
}
if (isSetupOnHold) {
rootLogger.info('Holding setup until preboot stage is completed.');
const { shouldReloadConfig } = await preboot.waitUntilCanSetup();
if (shouldReloadConfig) {
await reloadConfiguration('configuration might have changed during preboot stage');
if (isSetupOnHold) {
rootLogger.info('Holding setup until preboot stage is completed.');
const { shouldReloadConfig } = await preboot.waitUntilCanSetup();
if (shouldReloadConfig) {
await reloadConfiguration('configuration might have changed during preboot stage');
}
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { schema, type TypeOf } from '@kbn/config-schema';
import type { ServiceConfigDescriptor } from '@kbn/core-base-server-internal';
const coreConfigSchema = schema.object({
lifecycle: schema.object({
disablePreboot: schema.boolean({ defaultValue: false }),
}),
});
export type CoreConfigType = TypeOf<typeof coreConfigSchema>;
export const coreConfig: ServiceConfigDescriptor<CoreConfigType> = {
path: 'core',
schema: coreConfigSchema,
};

View file

@ -26,15 +26,16 @@ import { config as i18nConfig } from '@kbn/core-i18n-server-internal';
import { config as deprecationConfig } from '@kbn/core-deprecations-server-internal';
import { statusConfig } from '@kbn/core-status-server-internal';
import { uiSettingsConfig } from '@kbn/core-ui-settings-server-internal';
import { config as pluginsConfig } from '@kbn/core-plugins-server-internal';
import { elasticApmConfig } from './root/elastic_config';
import { serverlessConfig } from './root/serverless_config';
import { coreConfig } from './core_config';
const rootConfigPath = '';
export function registerServiceConfig(configService: ConfigService) {
const configDescriptors: Array<ServiceConfigDescriptor<unknown>> = [
coreConfig,
cspConfig,
deprecationConfig,
elasticsearchConfig,

View file

@ -46,7 +46,10 @@ const logger = loggingSystemMock.create();
const rawConfigService = rawConfigServiceMock.create({});
beforeEach(() => {
mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
mockConfigService.atPath.mockReturnValue(
// config for `core` path, only one used with all the services being mocked
new BehaviorSubject({ lifecycle: { disablePreboot: false } })
);
mockPluginsService.discover.mockResolvedValue({
preboot: {
pluginTree: { asOpaqueIds: new Map(), asNames: new Map() },
@ -298,3 +301,32 @@ test('migrator-only node throws exception during start', async () => {
expect(migrationException!.processExitCode).toBe(0);
expect(migrationException!.cause).toBeUndefined();
});
describe('When preboot is disabled', () => {
beforeEach(() => {
mockConfigService.atPath.mockReturnValue(
// config for `core` path, only one used with all the services being mocked
new BehaviorSubject({ lifecycle: { disablePreboot: true } })
);
});
test('only preboots the mandatory services', async () => {
const server = new Server(rawConfigService, env, logger);
await server.preboot();
expect(mockNodeService.preboot).toHaveBeenCalledTimes(1);
expect(mockEnvironmentService.preboot).toHaveBeenCalledTimes(1);
expect(mockUiSettingsService.preboot).toHaveBeenCalledTimes(1);
expect(mockLoggingService.preboot).toHaveBeenCalledTimes(1);
expect(mockContextService.preboot).not.toHaveBeenCalled();
expect(mockHttpService.preboot).not.toHaveBeenCalled();
expect(mockI18nService.preboot).not.toHaveBeenCalled();
expect(mockElasticsearchService.preboot).not.toHaveBeenCalled();
expect(mockRenderingService.preboot).not.toHaveBeenCalled();
expect(mockPluginsService.preboot).not.toHaveBeenCalled();
expect(mockPrebootService.preboot).not.toHaveBeenCalled();
expect(mockStatusService.preboot).not.toHaveBeenCalled();
});
});

View file

@ -7,6 +7,7 @@
*/
import apm from 'elastic-apm-node';
import { firstValueFrom } from 'rxjs';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import type { Logger, LoggerFactory } from '@kbn/logging';
import type { NodeRoles } from '@kbn/core-node-server';
@ -55,6 +56,7 @@ import { CoreAppsService } from '@kbn/core-apps-server-internal';
import { SecurityService } from '@kbn/core-security-server-internal';
import { registerServiceConfig } from './register_service_config';
import { MIGRATION_EXCEPTION_CODE } from './constants';
import { coreConfig, type CoreConfigType } from './core_config';
const coreId = Symbol('core');
const KIBANA_STARTED_EVENT = 'kibana_started';
@ -158,16 +160,22 @@ export class Server {
this.uptimePerStep.constructor = { start: constructorStartUptime, end: performance.now() };
}
public async preboot() {
public async preboot(): Promise<InternalCorePreboot | undefined> {
this.log.debug('prebooting server');
const config = await firstValueFrom(this.configService.atPath<CoreConfigType>(coreConfig.path));
const { disablePreboot } = config.lifecycle;
if (disablePreboot) {
this.log.info('preboot phase is disabled - skipping');
}
const prebootStartUptime = performance.now();
const prebootTransaction = apm.startTransaction('server-preboot', 'kibana-platform');
// service required for plugin discovery
const analyticsPreboot = this.analytics.preboot();
const environmentPreboot = await this.environment.preboot({ analytics: analyticsPreboot });
const nodePreboot = await this.node.preboot({ loggingSystem: this.loggingSystem });
this.nodeRoles = nodePreboot.roles;
// Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph.
@ -176,57 +184,70 @@ export class Server {
node: nodePreboot,
});
// Immediately terminate in case of invalid configuration. This needs to be done after plugin discovery. We also
// silent deprecation warnings until `setup` stage where we'll validate config once again.
await ensureValidConfiguration(this.configService, { logDeprecations: false });
if (!disablePreboot) {
// Immediately terminate in case of invalid configuration. This needs to be done after plugin discovery. We also
// silent deprecation warnings until `setup` stage where we'll validate config once again.
await ensureValidConfiguration(this.configService, { logDeprecations: false });
}
const { uiPlugins, pluginTree, pluginPaths } = this.discoveredPlugins.preboot;
const contextServicePreboot = this.context.preboot({
pluginDependencies: new Map([...pluginTree.asOpaqueIds]),
});
const httpPreboot = await this.http.preboot({ context: contextServicePreboot });
// setup i18n prior to any other service, to have translations ready
await this.i18n.preboot({ http: httpPreboot, pluginPaths });
this.capabilities.preboot({ http: httpPreboot });
const elasticsearchServicePreboot = await this.elasticsearch.preboot();
// services we need to preboot even when preboot is disabled
const uiSettingsPreboot = await this.uiSettings.preboot();
await this.status.preboot({ http: httpPreboot });
const renderingPreboot = await this.rendering.preboot({ http: httpPreboot, uiPlugins });
const httpResourcesPreboot = this.httpResources.preboot({
http: httpPreboot,
rendering: renderingPreboot,
});
const loggingPreboot = this.logging.preboot({ loggingSystem: this.loggingSystem });
const corePreboot: InternalCorePreboot = {
analytics: analyticsPreboot,
context: contextServicePreboot,
elasticsearch: elasticsearchServicePreboot,
http: httpPreboot,
uiSettings: uiSettingsPreboot,
httpResources: httpResourcesPreboot,
logging: loggingPreboot,
preboot: this.prebootService.preboot(),
};
let corePreboot: InternalCorePreboot | undefined;
await this.plugins.preboot(corePreboot);
if (!disablePreboot) {
const { uiPlugins, pluginTree, pluginPaths } = this.discoveredPlugins.preboot;
httpPreboot.registerRouteHandlerContext<PrebootRequestHandlerContext, 'core'>(
coreId,
'core',
() => {
return new PrebootCoreRouteHandlerContext(corePreboot);
}
);
const contextServicePreboot = this.context.preboot({
pluginDependencies: new Map([...pluginTree.asOpaqueIds]),
});
this.coreApp.preboot(corePreboot, uiPlugins);
const httpPreboot = await this.http.preboot({ context: contextServicePreboot });
// setup i18n prior to any other service, to have translations ready
await this.i18n.preboot({ http: httpPreboot, pluginPaths });
this.capabilities.preboot({ http: httpPreboot });
const elasticsearchServicePreboot = await this.elasticsearch.preboot();
await this.status.preboot({ http: httpPreboot });
const renderingPreboot = await this.rendering.preboot({ http: httpPreboot, uiPlugins });
const httpResourcesPreboot = this.httpResources.preboot({
http: httpPreboot,
rendering: renderingPreboot,
});
corePreboot = {
analytics: analyticsPreboot,
context: contextServicePreboot,
elasticsearch: elasticsearchServicePreboot,
http: httpPreboot,
uiSettings: uiSettingsPreboot,
httpResources: httpResourcesPreboot,
logging: loggingPreboot,
preboot: this.prebootService.preboot(),
};
await this.plugins.preboot(corePreboot);
httpPreboot.registerRouteHandlerContext<PrebootRequestHandlerContext, 'core'>(
coreId,
'core',
() => {
return new PrebootCoreRouteHandlerContext(corePreboot!);
}
);
this.coreApp.preboot(corePreboot, uiPlugins);
}
prebootTransaction.end();
this.uptimePerStep.preboot = { start: prebootStartUptime, end: performance.now() };
return corePreboot;
}

View file

@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
createRootWithCorePlugins,
createTestServers,
TestElasticsearchUtils,
} from '@kbn/core-test-helpers-kbn-server';
function createRootWithDisabledPreboot() {
return createRootWithCorePlugins({
core: {
lifecycle: {
disablePreboot: true,
},
},
logging: {
appenders: {
'test-console': {
type: 'console',
layout: {
type: 'json',
},
},
},
root: {
appenders: ['test-console'],
level: 'info',
},
},
});
}
describe('Starting root with disabled preboot', () => {
let esServer: TestElasticsearchUtils;
let root: ReturnType<typeof createRootWithDisabledPreboot>;
beforeEach(async () => {
const { startES } = createTestServers({
adjustTimeout: (t: number) => jest.setTimeout(t),
settings: {
es: {
license: 'basic',
},
},
});
esServer = await startES();
root = createRootWithDisabledPreboot();
});
afterEach(async () => {
await root?.shutdown();
await esServer?.stop();
});
it('successfully boots', async () => {
const preboot = await root.preboot();
await root.setup();
await root.start();
// preboot contract is not returned when preboot is disabled
expect(preboot).toBeUndefined();
});
});

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
// TODO replace the line below with
// preset: '@kbn/test/jest_integration_node
// to do so, we must fix all integration tests first
// see https://github.com/elastic/kibana/pull/130255/
preset: '@kbn/test/jest_integration',
rootDir: '../../../../..',
roots: ['<rootDir>/src/core/server/integration_tests/root'],
// must override to match all test given there is no `integration_tests` subfolder
testMatch: ['**/*.test.{js,mjs,ts,tsx}'],
};