mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
migrate pid file to core (#77037)
* migrate pid file to core * use correct log level + add comment * move `unhandledRejection` to service's setup * update comment
This commit is contained in:
parent
d190a2a2e3
commit
cdea019dfc
11 changed files with 278 additions and 86 deletions
|
@ -21,6 +21,7 @@ import { BehaviorSubject } from 'rxjs';
|
|||
import { EnvironmentService } from './environment_service';
|
||||
import { resolveInstanceUuid } from './resolve_uuid';
|
||||
import { createDataFolder } from './create_data_folder';
|
||||
import { writePidFile } from './write_pid_file';
|
||||
import { CoreContext } from '../core_context';
|
||||
|
||||
import { configServiceMock } from '../config/config_service.mock';
|
||||
|
@ -35,12 +36,20 @@ jest.mock('./create_data_folder', () => ({
|
|||
createDataFolder: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./write_pid_file', () => ({
|
||||
writePidFile: jest.fn(),
|
||||
}));
|
||||
|
||||
const pathConfig = {
|
||||
data: 'data-folder',
|
||||
};
|
||||
const serverConfig = {
|
||||
uuid: 'SOME_UUID',
|
||||
};
|
||||
const pidConfig = {
|
||||
file: '/pid/file',
|
||||
exclusive: 'false',
|
||||
};
|
||||
|
||||
const getConfigService = () => {
|
||||
const configService = configServiceMock.create();
|
||||
|
@ -51,6 +60,9 @@ const getConfigService = () => {
|
|||
if (path === 'server') {
|
||||
return new BehaviorSubject(serverConfig);
|
||||
}
|
||||
if (path === 'pid') {
|
||||
return new BehaviorSubject(pidConfig);
|
||||
}
|
||||
return new BehaviorSubject({});
|
||||
});
|
||||
return configService;
|
||||
|
@ -76,7 +88,7 @@ describe('UuidService', () => {
|
|||
expect(resolveInstanceUuid).toHaveBeenCalledWith({
|
||||
pathConfig,
|
||||
serverConfig,
|
||||
logger: logger.get('uuid'),
|
||||
logger: logger.get('environment'),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -86,7 +98,17 @@ describe('UuidService', () => {
|
|||
expect(createDataFolder).toHaveBeenCalledTimes(1);
|
||||
expect(createDataFolder).toHaveBeenCalledWith({
|
||||
pathConfig,
|
||||
logger: logger.get('uuid'),
|
||||
logger: logger.get('environment'),
|
||||
});
|
||||
});
|
||||
|
||||
it('calls writePidFile with correct parameters', async () => {
|
||||
const service = new EnvironmentService(coreContext);
|
||||
await service.setup();
|
||||
expect(writePidFile).toHaveBeenCalledTimes(1);
|
||||
expect(writePidFile).toHaveBeenCalledWith({
|
||||
pidConfig,
|
||||
logger: logger.get('environment'),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -23,8 +23,10 @@ import { Logger } from '../logging';
|
|||
import { IConfigService } from '../config';
|
||||
import { PathConfigType, config as pathConfigDef } from '../path';
|
||||
import { HttpConfigType, config as httpConfigDef } from '../http';
|
||||
import { PidConfigType, config as pidConfigDef } from './pid_config';
|
||||
import { resolveInstanceUuid } from './resolve_uuid';
|
||||
import { createDataFolder } from './create_data_folder';
|
||||
import { writePidFile } from './write_pid_file';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
@ -43,17 +45,24 @@ export class EnvironmentService {
|
|||
private uuid: string = '';
|
||||
|
||||
constructor(core: CoreContext) {
|
||||
this.log = core.logger.get('uuid');
|
||||
this.log = core.logger.get('environment');
|
||||
this.configService = core.configService;
|
||||
}
|
||||
|
||||
public async setup() {
|
||||
const [pathConfig, serverConfig] = await Promise.all([
|
||||
const [pathConfig, serverConfig, pidConfig] = await Promise.all([
|
||||
this.configService.atPath<PathConfigType>(pathConfigDef.path).pipe(take(1)).toPromise(),
|
||||
this.configService.atPath<HttpConfigType>(httpConfigDef.path).pipe(take(1)).toPromise(),
|
||||
this.configService.atPath<PidConfigType>(pidConfigDef.path).pipe(take(1)).toPromise(),
|
||||
]);
|
||||
|
||||
// was present in the legacy `pid` file.
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
this.log.warn(`Detected an unhandled Promise rejection.\n${reason}`);
|
||||
});
|
||||
|
||||
await createDataFolder({ pathConfig, logger: this.log });
|
||||
await writePidFile({ pidConfig, logger: this.log });
|
||||
|
||||
this.uuid = await resolveInstanceUuid({
|
||||
pathConfig,
|
||||
|
|
|
@ -23,3 +23,4 @@ import { promisify } from 'util';
|
|||
export const readFile = promisify(Fs.readFile);
|
||||
export const writeFile = promisify(Fs.writeFile);
|
||||
export const mkdir = promisify(Fs.mkdir);
|
||||
export const exists = promisify(Fs.exists);
|
||||
|
|
|
@ -18,3 +18,4 @@
|
|||
*/
|
||||
|
||||
export { EnvironmentService, InternalEnvironmentServiceSetup } from './environment_service';
|
||||
export { config, PidConfigType } from './pid_config';
|
||||
|
|
30
src/core/server/environment/pid_config.ts
Normal file
30
src/core/server/environment/pid_config.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { TypeOf, schema } from '@kbn/config-schema';
|
||||
|
||||
export const config = {
|
||||
path: 'pid',
|
||||
schema: schema.object({
|
||||
file: schema.maybe(schema.string()),
|
||||
exclusive: schema.boolean({ defaultValue: false }),
|
||||
}),
|
||||
};
|
||||
|
||||
export type PidConfigType = TypeOf<typeof config.schema>;
|
144
src/core/server/environment/write_pid_file.test.ts
Normal file
144
src/core/server/environment/write_pid_file.test.ts
Normal file
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { writeFile, exists } from './fs';
|
||||
import { writePidFile } from './write_pid_file';
|
||||
import { loggingSystemMock } from '../logging/logging_system.mock';
|
||||
|
||||
jest.mock('./fs', () => ({
|
||||
writeFile: jest.fn(),
|
||||
exists: jest.fn(),
|
||||
}));
|
||||
|
||||
const writeFileMock = writeFile as jest.MockedFunction<typeof writeFile>;
|
||||
const existsMock = exists as jest.MockedFunction<typeof exists>;
|
||||
|
||||
const pid = String(process.pid);
|
||||
|
||||
describe('writePidFile', () => {
|
||||
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = loggingSystemMock.createLogger();
|
||||
jest.spyOn(process, 'once');
|
||||
|
||||
writeFileMock.mockImplementation(() => Promise.resolve());
|
||||
existsMock.mockImplementation(() => Promise.resolve(false));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const allLogs = () =>
|
||||
Object.entries(loggingSystemMock.collect(logger)).reduce((messages, [key, value]) => {
|
||||
return [...messages, ...(key === 'log' ? [] : (value as any[]).map(([msg]) => [key, msg]))];
|
||||
}, [] as any[]);
|
||||
|
||||
it('does nothing if `pid.file` is not set', async () => {
|
||||
await writePidFile({
|
||||
pidConfig: {
|
||||
file: undefined,
|
||||
exclusive: false,
|
||||
},
|
||||
logger,
|
||||
});
|
||||
expect(writeFile).not.toHaveBeenCalled();
|
||||
expect(process.once).not.toHaveBeenCalled();
|
||||
expect(allLogs()).toMatchInlineSnapshot(`Array []`);
|
||||
});
|
||||
|
||||
it('writes the pid file to `pid.file`', async () => {
|
||||
existsMock.mockResolvedValue(false);
|
||||
|
||||
await writePidFile({
|
||||
pidConfig: {
|
||||
file: '/pid-file',
|
||||
exclusive: false,
|
||||
},
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(writeFile).toHaveBeenCalledTimes(1);
|
||||
expect(writeFile).toHaveBeenCalledWith('/pid-file', pid);
|
||||
|
||||
expect(process.once).toHaveBeenCalledTimes(2);
|
||||
expect(process.once).toHaveBeenCalledWith('exit', expect.any(Function));
|
||||
expect(process.once).toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
||||
|
||||
expect(allLogs()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"debug",
|
||||
"wrote pid file to /pid-file",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('throws an error if the file exists and `pid.exclusive is true`', async () => {
|
||||
existsMock.mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
writePidFile({
|
||||
pidConfig: {
|
||||
file: '/pid-file',
|
||||
exclusive: true,
|
||||
},
|
||||
logger,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"pid file already exists at /pid-file"`);
|
||||
|
||||
expect(writeFile).not.toHaveBeenCalled();
|
||||
expect(process.once).not.toHaveBeenCalled();
|
||||
expect(allLogs()).toMatchInlineSnapshot(`Array []`);
|
||||
});
|
||||
|
||||
it('logs a warning if the file exists and `pid.exclusive` is false', async () => {
|
||||
existsMock.mockResolvedValue(true);
|
||||
|
||||
await writePidFile({
|
||||
pidConfig: {
|
||||
file: '/pid-file',
|
||||
exclusive: false,
|
||||
},
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(writeFile).toHaveBeenCalledTimes(1);
|
||||
expect(writeFile).toHaveBeenCalledWith('/pid-file', pid);
|
||||
|
||||
expect(process.once).toHaveBeenCalledTimes(2);
|
||||
expect(process.once).toHaveBeenCalledWith('exit', expect.any(Function));
|
||||
expect(process.once).toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
||||
|
||||
expect(allLogs()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"debug",
|
||||
"wrote pid file to /pid-file",
|
||||
],
|
||||
Array [
|
||||
"warn",
|
||||
"pid file already exists at /pid-file",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
64
src/core/server/environment/write_pid_file.ts
Normal file
64
src/core/server/environment/write_pid_file.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { unlinkSync as unlink } from 'fs';
|
||||
import once from 'lodash/once';
|
||||
import { Logger } from '../logging';
|
||||
import { writeFile, exists } from './fs';
|
||||
import { PidConfigType } from './pid_config';
|
||||
|
||||
export const writePidFile = async ({
|
||||
pidConfig,
|
||||
logger,
|
||||
}: {
|
||||
pidConfig: PidConfigType;
|
||||
logger: Logger;
|
||||
}) => {
|
||||
const path = pidConfig.file;
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pid = String(process.pid);
|
||||
|
||||
if (await exists(path)) {
|
||||
const message = `pid file already exists at ${path}`;
|
||||
if (pidConfig.exclusive) {
|
||||
throw new Error(message);
|
||||
} else {
|
||||
logger.warn(message, { path, pid });
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(path, pid);
|
||||
|
||||
logger.debug(`wrote pid file to ${path}`, { path, pid });
|
||||
|
||||
const clean = once(() => {
|
||||
unlink(path);
|
||||
});
|
||||
|
||||
process.once('exit', clean); // for "natural" exits
|
||||
process.once('SIGINT', () => {
|
||||
// for Ctrl-C exits
|
||||
clean();
|
||||
// resend SIGINT
|
||||
process.kill(process.pid, 'SIGINT');
|
||||
});
|
||||
};
|
|
@ -31,7 +31,7 @@ import { PluginsService, config as pluginsConfig } from './plugins';
|
|||
import { SavedObjectsService } from '../server/saved_objects';
|
||||
import { MetricsService, opsConfig } from './metrics';
|
||||
import { CapabilitiesService } from './capabilities';
|
||||
import { EnvironmentService } from './environment';
|
||||
import { EnvironmentService, config as pidConfig } from './environment';
|
||||
import { StatusService } from './status/status_service';
|
||||
|
||||
import { config as cspConfig } from './csp';
|
||||
|
@ -310,6 +310,7 @@ export class Server {
|
|||
uiSettingsConfig,
|
||||
opsConfig,
|
||||
statusConfig,
|
||||
pidConfig,
|
||||
];
|
||||
|
||||
this.configService.addDeprecationProvider(rootConfigPath, coreDeprecationProvider);
|
||||
|
|
|
@ -42,10 +42,7 @@ export default () =>
|
|||
basePathProxyTarget: Joi.number().default(5603),
|
||||
}).default(),
|
||||
|
||||
pid: Joi.object({
|
||||
file: Joi.string(),
|
||||
exclusive: Joi.boolean().default(false),
|
||||
}).default(),
|
||||
pid: HANDLED_IN_NEW_PLATFORM,
|
||||
|
||||
csp: HANDLED_IN_NEW_PLATFORM,
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@ import { coreMixin } from './core';
|
|||
import { loggingMixin } from './logging';
|
||||
import warningsMixin from './warnings';
|
||||
import { statusMixin } from './status';
|
||||
import pidMixin from './pid';
|
||||
import configCompleteMixin from './config/complete';
|
||||
import { optimizeMixin } from '../../optimize';
|
||||
import * as Plugins from './plugins';
|
||||
|
@ -93,9 +92,6 @@ export default class KbnServer {
|
|||
warningsMixin,
|
||||
statusMixin,
|
||||
|
||||
// writes pid file
|
||||
pidMixin,
|
||||
|
||||
// scan translations dirs, register locale files and initialize i18n engine.
|
||||
i18nMixin,
|
||||
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import Boom from 'boom';
|
||||
import Bluebird from 'bluebird';
|
||||
import { unlinkSync as unlink } from 'fs';
|
||||
const writeFile = Bluebird.promisify(require('fs').writeFile);
|
||||
|
||||
export default Bluebird.method(function (kbnServer, server, config) {
|
||||
const path = config.get('pid.file');
|
||||
if (!path) return;
|
||||
|
||||
const pid = String(process.pid);
|
||||
|
||||
return writeFile(path, pid, { flag: 'wx' })
|
||||
.catch(function (err) {
|
||||
if (err.code !== 'EEXIST') throw err;
|
||||
|
||||
const message = `pid file already exists at ${path}`;
|
||||
const metadata = {
|
||||
path: path,
|
||||
pid: pid,
|
||||
};
|
||||
|
||||
if (config.get('pid.exclusive')) {
|
||||
throw Boom.internal(message, { message, ...metadata });
|
||||
} else {
|
||||
server.log(['pid', 'warning'], message, metadata);
|
||||
}
|
||||
|
||||
return writeFile(path, pid);
|
||||
})
|
||||
.then(function () {
|
||||
server.logWithMetadata(['pid', 'debug'], `wrote pid file to ${path}`, {
|
||||
path: path,
|
||||
pid: pid,
|
||||
});
|
||||
|
||||
const clean = _.once(function () {
|
||||
unlink(path);
|
||||
});
|
||||
|
||||
process.once('exit', clean); // for "natural" exits
|
||||
process.once('SIGINT', function () {
|
||||
// for Ctrl-C exits
|
||||
clean();
|
||||
|
||||
// resend SIGINT
|
||||
process.kill(process.pid, 'SIGINT');
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', function (reason) {
|
||||
server.log(['warning'], `Detected an unhandled Promise rejection.\n${reason}`);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue