feat: initial proposal for multithreaded execution

This commit is contained in:
Thomas Dupont
2022-05-13 11:27:31 +02:00
committed by Joachim Van Herwegen
parent 32245fc604
commit 236bbc6e5d
40 changed files with 880 additions and 97 deletions

View File

@@ -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,
};
}

View File

@@ -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);
});
});

View File

@@ -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> => {

View 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);
});
});

View 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);
});
});

View 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();
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});
});

View File

@@ -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),

View 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);
});
});
});

View File

@@ -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();