mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: initial proposal for multithreaded execution
This commit is contained in:
committed by
Joachim Van Herwegen
parent
32245fc604
commit
236bbc6e5d
@@ -54,5 +54,6 @@ export function getDefaultVariables(port: number, baseUrl?: string): Record<stri
|
||||
'urn:solid-server:default:variable:loggingLevel': 'off',
|
||||
'urn:solid-server:default:variable:showStackTrace': true,
|
||||
'urn:solid-server:default:variable:seededPodConfigJson': null,
|
||||
'urn:solid-server:default:variable:workers': 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ClusterManager } from '../../../src';
|
||||
import { App } from '../../../src/init/App';
|
||||
import type { Finalizable } from '../../../src/init/final/Finalizable';
|
||||
import type { Initializer } from '../../../src/init/Initializer';
|
||||
@@ -5,12 +6,14 @@ import type { Initializer } from '../../../src/init/Initializer';
|
||||
describe('An App', (): void => {
|
||||
let initializer: Initializer;
|
||||
let finalizer: Finalizable;
|
||||
let clusterManager: ClusterManager;
|
||||
let app: App;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
initializer = { handleSafe: jest.fn() } as any;
|
||||
finalizer = { finalize: jest.fn() };
|
||||
app = new App(initializer, finalizer);
|
||||
clusterManager = {} as any;
|
||||
app = new App(initializer, finalizer, clusterManager);
|
||||
});
|
||||
|
||||
it('can start with the initializer.', async(): Promise<void> => {
|
||||
@@ -22,4 +25,9 @@ describe('An App', (): void => {
|
||||
await expect(app.stop()).resolves.toBeUndefined();
|
||||
expect(finalizer.finalize).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('can check its clusterManager for the threading mode.', async(): Promise<void> => {
|
||||
await expect(app.start()).resolves.toBeUndefined();
|
||||
expect(app.clusterManager).toBe(clusterManager);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ComponentsManager } from 'componentsjs';
|
||||
import type { ClusterManager } from '../../../src';
|
||||
import type { App } from '../../../src/init/App';
|
||||
import { AppRunner } from '../../../src/init/AppRunner';
|
||||
import type { CliExtractor } from '../../../src/init/cli/CliExtractor';
|
||||
@@ -6,10 +7,6 @@ import type { SettingsResolver } from '../../../src/init/variables/SettingsResol
|
||||
import { joinFilePath } from '../../../src/util/PathUtil';
|
||||
import { flushPromises } from '../../util/Util';
|
||||
|
||||
const app: jest.Mocked<App> = {
|
||||
start: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const defaultParameters = {
|
||||
port: 3000,
|
||||
logLevel: 'info',
|
||||
@@ -26,6 +23,31 @@ const settingsResolver: jest.Mocked<SettingsResolver> = {
|
||||
handleSafe: jest.fn().mockResolvedValue(defaultVariables),
|
||||
} as any;
|
||||
|
||||
const mockLogger = {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
silly: jest.fn(),
|
||||
error: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
log: jest.fn(),
|
||||
};
|
||||
|
||||
const clusterManager: jest.Mocked<ClusterManager> = {
|
||||
isSingleThreaded: jest.fn().mockReturnValue(false),
|
||||
spawnWorkers: jest.fn(),
|
||||
isPrimary: jest.fn().mockReturnValue(true),
|
||||
isWorker: jest.fn().mockReturnValue(false),
|
||||
logger: mockLogger,
|
||||
workers: 1,
|
||||
clusterMode: 1,
|
||||
} as any;
|
||||
|
||||
const app: jest.Mocked<App> = {
|
||||
start: jest.fn(),
|
||||
clusterManager,
|
||||
} as any;
|
||||
|
||||
const manager: jest.Mocked<ComponentsManager<App>> = {
|
||||
instantiate: jest.fn(async(iri: string): Promise<any> => {
|
||||
switch (iri) {
|
||||
@@ -39,6 +61,12 @@ const manager: jest.Mocked<ComponentsManager<App>> = {
|
||||
},
|
||||
} as any;
|
||||
|
||||
const listSingleThreadedComponentsMock = jest.fn().mockResolvedValue([]);
|
||||
|
||||
jest.mock('../../../src/init/cluster/SingleThreaded', (): any => ({
|
||||
listSingleThreadedComponents: (): any => listSingleThreadedComponentsMock(),
|
||||
}));
|
||||
|
||||
jest.mock('componentsjs', (): any => ({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
ComponentsManager: {
|
||||
@@ -90,6 +118,75 @@ describe('AppRunner', (): void => {
|
||||
expect(cliExtractor.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(settingsResolver.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(app.start).toHaveBeenCalledTimes(0);
|
||||
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('throws an error if threading issues are detected with 1 class.', async(): Promise<void> => {
|
||||
listSingleThreadedComponentsMock.mockImplementationOnce((): string[] => [ 'ViolatingClass' ]);
|
||||
const variables = {
|
||||
'urn:solid-server:default:variable:port': 3000,
|
||||
'urn:solid-server:default:variable:loggingLevel': 'info',
|
||||
'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
|
||||
'urn:solid-server:default:variable:showStackTrace': false,
|
||||
'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json',
|
||||
'urn:solid-server:default:variable:seededPodConfigJson': '/var/cwd/seeded-pod-config.json',
|
||||
};
|
||||
|
||||
let caughtError: Error | undefined;
|
||||
try {
|
||||
await new AppRunner().create(
|
||||
{
|
||||
mainModulePath: joinFilePath(__dirname, '../../../'),
|
||||
dumpErrorState: true,
|
||||
logLevel: 'info',
|
||||
},
|
||||
joinFilePath(__dirname, '../../../config/default.json'),
|
||||
variables,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
caughtError = error as Error;
|
||||
}
|
||||
expect(caughtError?.message).toMatch(/^Cannot run a singlethreaded-only component in a multithreaded setup!/mu);
|
||||
expect(caughtError?.message).toMatch(
|
||||
/\[ViolatingClass\] is not threadsafe and should not be run in multithreaded setups!/mu,
|
||||
);
|
||||
|
||||
expect(write).toHaveBeenCalledTimes(0);
|
||||
expect(exit).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('throws an error if threading issues are detected with 2 class.', async(): Promise<void> => {
|
||||
listSingleThreadedComponentsMock.mockImplementationOnce((): string[] => [ 'ViolatingClass1', 'ViolatingClass2' ]);
|
||||
const variables = {
|
||||
'urn:solid-server:default:variable:port': 3000,
|
||||
'urn:solid-server:default:variable:loggingLevel': 'info',
|
||||
'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
|
||||
'urn:solid-server:default:variable:showStackTrace': false,
|
||||
'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json',
|
||||
'urn:solid-server:default:variable:seededPodConfigJson': '/var/cwd/seeded-pod-config.json',
|
||||
};
|
||||
|
||||
let caughtError: Error | undefined;
|
||||
try {
|
||||
await new AppRunner().create(
|
||||
{
|
||||
mainModulePath: joinFilePath(__dirname, '../../../'),
|
||||
dumpErrorState: true,
|
||||
logLevel: 'info',
|
||||
},
|
||||
joinFilePath(__dirname, '../../../config/default.json'),
|
||||
variables,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
caughtError = error as Error;
|
||||
}
|
||||
expect(caughtError?.message).toMatch(/^Cannot run a singlethreaded-only component in a multithreaded setup!/mu);
|
||||
expect(caughtError?.message).toMatch(
|
||||
/\[ViolatingClass1, ViolatingClass2\] are not threadsafe and should not be run in multithreaded setups!/mu,
|
||||
);
|
||||
|
||||
expect(write).toHaveBeenCalledTimes(0);
|
||||
expect(exit).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,6 +225,7 @@ describe('AppRunner', (): void => {
|
||||
expect(settingsResolver.handleSafe).toHaveBeenCalledTimes(0);
|
||||
expect(app.start).toHaveBeenCalledTimes(1);
|
||||
expect(app.start).toHaveBeenCalledWith();
|
||||
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -154,6 +252,7 @@ describe('AppRunner', (): void => {
|
||||
expect(manager.instantiate).toHaveBeenNthCalledWith(2,
|
||||
'urn:solid-server:default:App',
|
||||
{ variables: defaultVariables });
|
||||
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
|
||||
expect(app.start).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
@@ -171,6 +270,7 @@ describe('AppRunner', (): void => {
|
||||
'-t',
|
||||
'--podConfigJson', '/different-path.json',
|
||||
'--seededPodConfigJson', '/different-path.json',
|
||||
'-w', '1',
|
||||
];
|
||||
process.argv = argvParameters;
|
||||
|
||||
@@ -195,10 +295,35 @@ describe('AppRunner', (): void => {
|
||||
'urn:solid-server:default:App',
|
||||
{ variables: defaultVariables });
|
||||
expect(app.start).toHaveBeenCalledTimes(0);
|
||||
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
|
||||
|
||||
process.argv = argv;
|
||||
});
|
||||
|
||||
it('checks for threading issues when starting in multithreaded mode.', async(): Promise<void> => {
|
||||
const createdApp = await new AppRunner().createCli();
|
||||
expect(createdApp).toBe(app);
|
||||
expect(listSingleThreadedComponentsMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws an error if there are threading issues detected.', async(): Promise<void> => {
|
||||
listSingleThreadedComponentsMock.mockImplementationOnce((): string[] => [ 'ViolatingClass' ]);
|
||||
|
||||
let caughtError: Error = new Error('should disappear');
|
||||
try {
|
||||
await new AppRunner().createCli([ 'node', 'script' ]);
|
||||
} catch (error: unknown) {
|
||||
caughtError = error as Error;
|
||||
}
|
||||
expect(caughtError.message).toMatch(/^Cannot run a singlethreaded-only component in a multithreaded setup!/mu);
|
||||
expect(caughtError?.message).toMatch(
|
||||
/\[ViolatingClass\] is not threadsafe and should not be run in multithreaded setups!/mu,
|
||||
);
|
||||
|
||||
expect(write).toHaveBeenCalledTimes(0);
|
||||
expect(exit).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('throws an error if creating a ComponentsManager fails.', async(): Promise<void> => {
|
||||
(manager.configRegistry.register as jest.Mock).mockRejectedValueOnce(new Error('Fatal'));
|
||||
|
||||
@@ -291,6 +416,7 @@ describe('AppRunner', (): void => {
|
||||
{ variables: defaultVariables });
|
||||
expect(app.start).toHaveBeenCalledTimes(1);
|
||||
expect(app.start).toHaveBeenLastCalledWith();
|
||||
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('throws an error if the server could not start.', async(): Promise<void> => {
|
||||
@@ -342,6 +468,7 @@ describe('AppRunner', (): void => {
|
||||
{ variables: defaultVariables });
|
||||
expect(app.start).toHaveBeenCalledTimes(1);
|
||||
expect(app.start).toHaveBeenLastCalledWith();
|
||||
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('exits the process and writes to stderr if there was an error.', async(): Promise<void> => {
|
||||
|
||||
106
test/unit/init/cluster/ClusterManager.test.ts
Normal file
106
test/unit/init/cluster/ClusterManager.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import cluster from 'cluster';
|
||||
import EventEmitter from 'events';
|
||||
import { cpus } from 'os';
|
||||
import { ClusterManager } from '../../../../src';
|
||||
import * as LogUtil from '../../../../src/logging/LogUtil';
|
||||
|
||||
jest.mock('cluster');
|
||||
jest.mock('os', (): any => ({
|
||||
...jest.requireActual('os'),
|
||||
cpus: jest.fn().mockImplementation((): any => [{}, {}, {}, {}, {}, {}]),
|
||||
}));
|
||||
|
||||
const mockWorker = new EventEmitter() as any;
|
||||
mockWorker.process = { pid: 666 };
|
||||
|
||||
describe('A ClusterManager', (): void => {
|
||||
const emitter = new EventEmitter();
|
||||
const mockCluster = jest.requireMock('cluster');
|
||||
const mockLogger = { info: jest.fn(), warn: jest.fn() };
|
||||
jest.spyOn(LogUtil, 'getLoggerFor').mockImplementation((): any => mockLogger);
|
||||
|
||||
beforeAll((): void => {
|
||||
Object.assign(mockCluster, {
|
||||
fork: jest.fn().mockImplementation((): any => mockWorker),
|
||||
on: jest.fn().mockImplementation(emitter.on),
|
||||
emit: jest.fn().mockImplementation(emitter.emit),
|
||||
isMaster: true,
|
||||
isWorker: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('can handle workers input as string.', (): void => {
|
||||
const cm = new ClusterManager('4');
|
||||
expect(cm.isSingleThreaded()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can distinguish between ClusterModes.', (): void => {
|
||||
const cm1 = new ClusterManager(-1);
|
||||
const cm2 = new ClusterManager(0);
|
||||
const cm3 = new ClusterManager(1);
|
||||
const cm4 = new ClusterManager(2);
|
||||
expect(cm1.isSingleThreaded()).toBeFalsy();
|
||||
expect(cm2.isSingleThreaded()).toBeFalsy();
|
||||
expect(cm3.isSingleThreaded()).toBeTruthy();
|
||||
expect(cm4.isSingleThreaded()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('errors on invalid workers amount.', (): void => {
|
||||
expect((): ClusterManager => new ClusterManager('10')).toBeDefined();
|
||||
expect((): ClusterManager => new ClusterManager('2')).toBeDefined();
|
||||
expect((): ClusterManager => new ClusterManager('1')).toBeDefined();
|
||||
expect((): ClusterManager => new ClusterManager('0')).toBeDefined();
|
||||
expect((): ClusterManager => new ClusterManager('-1')).toBeDefined();
|
||||
expect((): ClusterManager => new ClusterManager('-5')).toBeDefined();
|
||||
expect((): ClusterManager => new ClusterManager('-6')).toThrow('Invalid workers value');
|
||||
expect((): ClusterManager => new ClusterManager('-10')).toThrow('Invalid workers value');
|
||||
});
|
||||
|
||||
it('has an isPrimary() that works.', (): void => {
|
||||
const cm = new ClusterManager(-1);
|
||||
expect(cm.isPrimary()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('has an isWorker() that works.', (): void => {
|
||||
const cm = new ClusterManager(-1);
|
||||
expect(cm.isWorker()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can autoscale to num_cpu and applies proper logging.', (): void => {
|
||||
const cm = new ClusterManager(-1);
|
||||
const workers = cpus().length - 1;
|
||||
expect(cpus()).toHaveLength(workers + 1);
|
||||
Object.assign(cm, { logger: mockLogger });
|
||||
|
||||
cm.spawnWorkers();
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(`Setting up ${workers} workers`);
|
||||
|
||||
for (let i = 0; i < workers; i++) {
|
||||
mockCluster.emit('online', mockWorker);
|
||||
}
|
||||
|
||||
expect(cluster.on).toHaveBeenCalledWith('online', expect.any(Function));
|
||||
expect(cluster.fork).toHaveBeenCalledTimes(workers);
|
||||
expect(mockLogger.info).toHaveBeenLastCalledWith(`All ${workers} requested workers have been started.`);
|
||||
|
||||
expect(cluster.on).toHaveBeenCalledWith('exit', expect.any(Function));
|
||||
const code = 333;
|
||||
const signal = 'exiting';
|
||||
mockCluster.emit('exit', mockWorker, code, signal);
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
`Worker ${mockWorker.process.pid} died with code ${code} and signal ${signal}`,
|
||||
);
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(`Starting a new worker`);
|
||||
});
|
||||
|
||||
it('can receive message from spawned workers.', (): void => {
|
||||
const cm = new ClusterManager(2);
|
||||
Object.assign(cm, { logger: mockLogger });
|
||||
|
||||
cm.spawnWorkers();
|
||||
const msg = 'Hi from worker!';
|
||||
mockWorker.emit('message', msg);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(msg);
|
||||
});
|
||||
});
|
||||
61
test/unit/init/cluster/SingleThreaded.test.ts
Normal file
61
test/unit/init/cluster/SingleThreaded.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { ComponentsManager } from 'componentsjs';
|
||||
import type { Resource } from 'rdf-object';
|
||||
import { listSingleThreadedComponents } from '../../../../src';
|
||||
|
||||
const moduleState = {
|
||||
contexts: {
|
||||
'https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld': 'dist/components/context.jsonld',
|
||||
},
|
||||
};
|
||||
|
||||
const mockResource: Resource = {
|
||||
isA: jest.fn().mockReturnValue(true),
|
||||
value: '#ViolatingClass',
|
||||
property: { type: { value: '#ViolatingClass' }},
|
||||
} as any;
|
||||
|
||||
const myExpandTerm = jest.fn().mockImplementation((): any => 'http://myFullIRI');
|
||||
|
||||
function mockComponentsManagerFn(length: number): jest.Mocked<ComponentsManager<any>> {
|
||||
const resources: Resource[] = Array.from<Resource>({ length }).fill(mockResource);
|
||||
return { moduleState, getInstantiatedResources: jest.fn((): any => resources) } as any;
|
||||
}
|
||||
|
||||
jest.mock('jsonld-context-parser/lib/ContextParser', (): any => ({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
ContextParser: jest.fn().mockImplementation((): any => ({
|
||||
parse: jest.fn(async(): Promise<any> => ({
|
||||
expandTerm: jest.fn((): any => myExpandTerm()),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('componentsjs', (): any => ({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
ComponentsManager: {
|
||||
build: jest.fn(async(props: any): Promise<ComponentsManager<any>> => mockComponentsManagerFn(props.length)),
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
PrefetchedDocumentLoader: jest.fn().mockImplementation((): any => ({
|
||||
load: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('A SingleThreaded', (): void => {
|
||||
it('has a listSingleThreadedComponents that works with 1 resource.', async(): Promise<void> => {
|
||||
const comp = await ComponentsManager.build({ length: 1 } as any);
|
||||
await expect(listSingleThreadedComponents(comp)).resolves.toEqual([ 'ViolatingClass' ]);
|
||||
});
|
||||
|
||||
it('has a listSingleThreadedComponents that works with multiple resources.', async(): Promise<void> => {
|
||||
const comp = await ComponentsManager.build({ length: 2 } as any);
|
||||
await expect(listSingleThreadedComponents(comp)).resolves.toEqual([ 'ViolatingClass', 'ViolatingClass' ]);
|
||||
});
|
||||
|
||||
it('errors when the interface IRI cannot be expanded.', async(): Promise<void> => {
|
||||
myExpandTerm.mockReturnValueOnce(null);
|
||||
const comp = await ComponentsManager.build({} as any);
|
||||
await expect(listSingleThreadedComponents(comp)).rejects
|
||||
.toThrow(/^Could not expand .* to IRI!/u);
|
||||
});
|
||||
});
|
||||
15
test/unit/init/cluster/WorkerManager.test.ts
Normal file
15
test/unit/init/cluster/WorkerManager.test.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ClusterManager, WorkerManager } from '../../../../src';
|
||||
|
||||
describe('A WorkerManager', (): void => {
|
||||
it('can be created from a ClusterManager.', (): void => {
|
||||
expect((): WorkerManager => new WorkerManager(new ClusterManager(4))).toBeDefined();
|
||||
});
|
||||
|
||||
it('can call handle.', async(): Promise<void> => {
|
||||
const cm = new ClusterManager(4);
|
||||
const wm = new WorkerManager(cm);
|
||||
Object.assign(cm, { spawnWorkers: jest.fn() });
|
||||
await wm.handle();
|
||||
expect(cm.spawnWorkers).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -53,13 +53,13 @@ describe('LazyLoggerFactory', (): void => {
|
||||
|
||||
const wrappedA = dummyLoggerFactory.createLogger.mock.results[0].value as jest.Mocked<Logger>;
|
||||
expect(wrappedA.log).toHaveBeenCalledTimes(2);
|
||||
expect(wrappedA.log).toHaveBeenNthCalledWith(1, 'warn', 'message1');
|
||||
expect(wrappedA.log).toHaveBeenNthCalledWith(2, 'error', 'message4');
|
||||
expect(wrappedA.log).toHaveBeenNthCalledWith(1, 'warn', 'message1', undefined);
|
||||
expect(wrappedA.log).toHaveBeenNthCalledWith(2, 'error', 'message4', undefined);
|
||||
|
||||
const wrappedB = dummyLoggerFactory.createLogger.mock.results[1].value as jest.Mocked<Logger>;
|
||||
expect(wrappedB.log).toHaveBeenCalledTimes(2);
|
||||
expect(wrappedB.log).toHaveBeenNthCalledWith(1, 'warn', 'message2');
|
||||
expect(wrappedB.log).toHaveBeenNthCalledWith(2, 'error', 'message3');
|
||||
expect(wrappedB.log).toHaveBeenNthCalledWith(1, 'warn', 'message2', undefined);
|
||||
expect(wrappedB.log).toHaveBeenNthCalledWith(2, 'error', 'message3', undefined);
|
||||
});
|
||||
|
||||
it('does not store more messages than the buffer limit.', (): void => {
|
||||
@@ -84,6 +84,6 @@ describe('LazyLoggerFactory', (): void => {
|
||||
expect(wrappedA.log).toHaveBeenCalledTimes(50);
|
||||
expect(wrappedB.log).toHaveBeenCalledTimes(49);
|
||||
expect(warningLogger.log).toHaveBeenCalledTimes(1);
|
||||
expect(warningLogger.log).toHaveBeenCalledWith('warn', 'Memory-buffered logging limit of 100 reached');
|
||||
expect(warningLogger.log).toHaveBeenCalledWith('warn', 'Memory-buffered logging limit of 100 reached', undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import process from 'process';
|
||||
import { BaseLogger, WrappingLogger } from '../../../src/logging/Logger';
|
||||
import type { SimpleLogger } from '../../../src/logging/Logger';
|
||||
import type { SimpleLogger, LogMetadata } from '../../../src/logging/Logger';
|
||||
|
||||
describe('Logger', (): void => {
|
||||
describe('a BaseLogger', (): void => {
|
||||
let logger: BaseLogger;
|
||||
const metadata: LogMetadata = {
|
||||
isPrimary: true,
|
||||
pid: process.pid,
|
||||
};
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
logger = new (BaseLogger as any)();
|
||||
@@ -13,43 +18,47 @@ describe('Logger', (): void => {
|
||||
it('delegates error to log.', async(): Promise<void> => {
|
||||
logger.error('my message');
|
||||
expect(logger.log).toHaveBeenCalledTimes(1);
|
||||
expect(logger.log).toHaveBeenCalledWith('error', 'my message');
|
||||
expect(logger.log).toHaveBeenCalledWith('error', 'my message', metadata);
|
||||
});
|
||||
|
||||
it('warn delegates to log.', async(): Promise<void> => {
|
||||
logger.warn('my message');
|
||||
expect(logger.log).toHaveBeenCalledTimes(1);
|
||||
expect(logger.log).toHaveBeenCalledWith('warn', 'my message');
|
||||
expect(logger.log).toHaveBeenCalledWith('warn', 'my message', metadata);
|
||||
});
|
||||
|
||||
it('info delegates to log.', async(): Promise<void> => {
|
||||
logger.info('my message');
|
||||
expect(logger.log).toHaveBeenCalledTimes(1);
|
||||
expect(logger.log).toHaveBeenCalledWith('info', 'my message');
|
||||
expect(logger.log).toHaveBeenCalledWith('info', 'my message', metadata);
|
||||
});
|
||||
|
||||
it('verbose delegates to log.', async(): Promise<void> => {
|
||||
logger.verbose('my message');
|
||||
expect(logger.log).toHaveBeenCalledTimes(1);
|
||||
expect(logger.log).toHaveBeenCalledWith('verbose', 'my message');
|
||||
expect(logger.log).toHaveBeenCalledWith('verbose', 'my message', metadata);
|
||||
});
|
||||
|
||||
it('debug delegates to log.', async(): Promise<void> => {
|
||||
logger.debug('my message');
|
||||
expect(logger.log).toHaveBeenCalledTimes(1);
|
||||
expect(logger.log).toHaveBeenCalledWith('debug', 'my message');
|
||||
expect(logger.log).toHaveBeenCalledWith('debug', 'my message', metadata);
|
||||
});
|
||||
|
||||
it('silly delegates to log.', async(): Promise<void> => {
|
||||
logger.silly('my message');
|
||||
expect(logger.log).toHaveBeenCalledTimes(1);
|
||||
expect(logger.log).toHaveBeenCalledWith('silly', 'my message');
|
||||
expect(logger.log).toHaveBeenCalledWith('silly', 'my message', metadata);
|
||||
});
|
||||
});
|
||||
|
||||
describe('a WrappingLogger', (): void => {
|
||||
let logger: SimpleLogger;
|
||||
let wrapper: WrappingLogger;
|
||||
const metadata: LogMetadata = {
|
||||
isPrimary: true,
|
||||
pid: process.pid,
|
||||
};
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
logger = { log: jest.fn() };
|
||||
@@ -59,37 +68,37 @@ describe('Logger', (): void => {
|
||||
it('error delegates to the internal logger.', async(): Promise<void> => {
|
||||
wrapper.error('my message');
|
||||
expect(logger.log).toHaveBeenCalledTimes(1);
|
||||
expect(logger.log).toHaveBeenCalledWith('error', 'my message');
|
||||
expect(logger.log).toHaveBeenCalledWith('error', 'my message', metadata);
|
||||
});
|
||||
|
||||
it('warn delegates to the internal logger.', async(): Promise<void> => {
|
||||
wrapper.warn('my message');
|
||||
expect(logger.log).toHaveBeenCalledTimes(1);
|
||||
expect(logger.log).toHaveBeenCalledWith('warn', 'my message');
|
||||
expect(logger.log).toHaveBeenCalledWith('warn', 'my message', metadata);
|
||||
});
|
||||
|
||||
it('info delegates to the internal logger.', async(): Promise<void> => {
|
||||
wrapper.info('my message');
|
||||
expect(logger.log).toHaveBeenCalledTimes(1);
|
||||
expect(logger.log).toHaveBeenCalledWith('info', 'my message');
|
||||
expect(logger.log).toHaveBeenCalledWith('info', 'my message', metadata);
|
||||
});
|
||||
|
||||
it('verbose delegates to the internal logger.', async(): Promise<void> => {
|
||||
wrapper.verbose('my message');
|
||||
expect(logger.log).toHaveBeenCalledTimes(1);
|
||||
expect(logger.log).toHaveBeenCalledWith('verbose', 'my message');
|
||||
expect(logger.log).toHaveBeenCalledWith('verbose', 'my message', metadata);
|
||||
});
|
||||
|
||||
it('debug delegates to the internal logger.', async(): Promise<void> => {
|
||||
wrapper.debug('my message');
|
||||
expect(logger.log).toHaveBeenCalledTimes(1);
|
||||
expect(logger.log).toHaveBeenCalledWith('debug', 'my message');
|
||||
expect(logger.log).toHaveBeenCalledWith('debug', 'my message', metadata);
|
||||
});
|
||||
|
||||
it('silly delegates to the internal logger.', async(): Promise<void> => {
|
||||
wrapper.silly('my message');
|
||||
expect(logger.log).toHaveBeenCalledTimes(1);
|
||||
expect(logger.log).toHaveBeenCalledWith('silly', 'my message');
|
||||
expect(logger.log).toHaveBeenCalledWith('silly', 'my message', metadata);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ describe('WinstonLoggerFactory', (): void => {
|
||||
level: expect.stringContaining('debug'),
|
||||
message: 'my message',
|
||||
timestamp: expect.any(String),
|
||||
metadata: expect.any(Object),
|
||||
[Symbol.for('level')]: 'debug',
|
||||
[Symbol.for('splat')]: [ undefined ],
|
||||
[Symbol.for('message')]: expect.any(String),
|
||||
|
||||
72
test/unit/util/handlers/ProcessHandler.test.ts
Normal file
72
test/unit/util/handlers/ProcessHandler.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { AsyncHandler, ClusterManager } from '../../../../src';
|
||||
import { NotImplementedHttpError, ProcessHandler } from '../../../../src';
|
||||
|
||||
function createClusterManager(workers: number, primary: boolean): jest.Mocked<ClusterManager> {
|
||||
return {
|
||||
isSingleThreaded: jest.fn().mockReturnValue(workers === 1),
|
||||
isWorker: jest.fn().mockReturnValue(!primary),
|
||||
isPrimary: jest.fn().mockReturnValue(primary),
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('A ProcessHandler', (): void => {
|
||||
const source: jest.Mocked<AsyncHandler<string, string>> = {
|
||||
canHandle: jest.fn(),
|
||||
handleSafe: jest.fn().mockResolvedValue('handledSafely'),
|
||||
handle: jest.fn().mockResolvedValue('handled'),
|
||||
};
|
||||
|
||||
describe('allowing only worker processes', (): void => {
|
||||
it('can create a ProcessHandler.', (): void => {
|
||||
expect((): ProcessHandler<string, string> =>
|
||||
new ProcessHandler(source, createClusterManager(1, true), false)).toBeDefined();
|
||||
});
|
||||
|
||||
it('can delegate to its source when run singlethreaded from worker.', async(): Promise<void> => {
|
||||
const ph = new ProcessHandler(source, createClusterManager(1, false), false);
|
||||
await expect(ph.handleSafe('test')).resolves.toBe('handled');
|
||||
});
|
||||
|
||||
it('can delegate to its source when run singlethreaded from primary.', async(): Promise<void> => {
|
||||
const ph = new ProcessHandler(source, createClusterManager(1, true), false);
|
||||
await expect(ph.handleSafe('test')).resolves.toBe('handled');
|
||||
});
|
||||
|
||||
it('can delegate to its source when run multithreaded from worker.', async(): Promise<void> => {
|
||||
const ph = new ProcessHandler(source, createClusterManager(2, false), false);
|
||||
await expect(ph.handleSafe('test')).resolves.toBe('handled');
|
||||
});
|
||||
|
||||
it('errors when run multithreaded from primary.', async(): Promise<void> => {
|
||||
const ph = new ProcessHandler(source, createClusterManager(2, true), false);
|
||||
await expect(ph.handleSafe('test')).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowing only the primary process', (): void => {
|
||||
it('can create a ProcessHandler.', (): void => {
|
||||
expect((): ProcessHandler<string, string> =>
|
||||
new ProcessHandler(source, createClusterManager(1, true), true)).toBeDefined();
|
||||
});
|
||||
|
||||
it('can delegate to its source when run singlethreaded from worker.', async(): Promise<void> => {
|
||||
const ph = new ProcessHandler(source, createClusterManager(1, false), true);
|
||||
await expect(ph.handleSafe('test')).resolves.toBe('handled');
|
||||
});
|
||||
|
||||
it('can delegate to its source when run singlethreaded from primary.', async(): Promise<void> => {
|
||||
const ph = new ProcessHandler(source, createClusterManager(1, true), true);
|
||||
await expect(ph.handleSafe('test')).resolves.toBe('handled');
|
||||
});
|
||||
|
||||
it('can delegate to its source when run multithreaded from primary.', async(): Promise<void> => {
|
||||
const ph = new ProcessHandler(source, createClusterManager(2, true), true);
|
||||
await expect(ph.handleSafe('test')).resolves.toBe('handled');
|
||||
});
|
||||
|
||||
it('errors when run multithreaded from worker.', async(): Promise<void> => {
|
||||
const ph = new ProcessHandler(source, createClusterManager(2, false), true);
|
||||
await expect(ph.handleSafe('test')).rejects.toThrow(NotImplementedHttpError);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,6 @@
|
||||
import type { Logger } from '../../../../src';
|
||||
import { getLoggerFor } from '../../../../src';
|
||||
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
||||
import { MemoryResourceLocker } from '../../../../src/util/locking/MemoryResourceLocker';
|
||||
|
||||
jest.mock('../../../../src/logging/LogUtil', (): any => {
|
||||
const logger: Logger =
|
||||
{ error: jest.fn(), debug: jest.fn(), warn: jest.fn(), info: jest.fn(), log: jest.fn() } as any;
|
||||
return { getLoggerFor: (): Logger => logger };
|
||||
});
|
||||
const logger: jest.Mocked<Logger> = getLoggerFor('MemoryResourceLocker') as any;
|
||||
|
||||
jest.mock('cluster', (): any => ({
|
||||
isWorker: true,
|
||||
}));
|
||||
|
||||
describe('A MemoryResourceLocker', (): void => {
|
||||
let locker: MemoryResourceLocker;
|
||||
const identifier = { path: 'http://test.com/foo' };
|
||||
@@ -21,11 +8,6 @@ describe('A MemoryResourceLocker', (): void => {
|
||||
locker = new MemoryResourceLocker();
|
||||
});
|
||||
|
||||
it('logs a warning when constructed on a worker process.', (): void => {
|
||||
expect((): MemoryResourceLocker => new MemoryResourceLocker()).toBeDefined();
|
||||
expect(logger.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can lock and unlock a resource.', async(): Promise<void> => {
|
||||
await expect(locker.acquire(identifier)).resolves.toBeUndefined();
|
||||
await expect(locker.release(identifier)).resolves.toBeUndefined();
|
||||
|
||||
Reference in New Issue
Block a user