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

@@ -1,3 +1,4 @@
import type { ClusterManager } from './cluster/ClusterManager';
import type { Finalizable } from './final/Finalizable';
import type { Initializer } from './Initializer';
@@ -7,10 +8,12 @@ import type { Initializer } from './Initializer';
export class App {
private readonly initializer: Initializer;
private readonly finalizer: Finalizable;
public readonly clusterManager: ClusterManager;
public constructor(initializer: Initializer, finalizer: Finalizable) {
public constructor(initializer: Initializer, finalizer: Finalizable, clusterManager: ClusterManager) {
this.initializer = initializer;
this.finalizer = finalizer;
this.clusterManager = clusterManager;
}
/**

View File

@@ -6,9 +6,11 @@ import yargs from 'yargs';
import { LOG_LEVELS } from '../logging/LogLevel';
import { getLoggerFor } from '../logging/LogUtil';
import { createErrorMessage, isError } from '../util/errors/ErrorUtil';
import { InternalServerError } from '../util/errors/InternalServerError';
import { resolveModulePath, resolveAssetPath } from '../util/PathUtil';
import type { App } from './App';
import type { CliResolver } from './CliResolver';
import { listSingleThreadedComponents } from './cluster/SingleThreaded';
import type { CliArgv, VariableBindings } from './variables/Types';
const DEFAULT_CONFIG = resolveModulePath('config/default.json');
@@ -65,7 +67,7 @@ export class AppRunner {
const componentsManager = await this.createComponentsManager<App>(loaderProperties, configFile);
// Create the application using the translated variable values
return componentsManager.instantiate(DEFAULT_APP, { variables: variableBindings });
return await this.createApp(componentsManager, variableBindings);
}
/**
@@ -177,12 +179,26 @@ export class AppRunner {
* where the App is created and started using the variable mappings.
*/
private async createApp(componentsManager: ComponentsManager<App>, variables: Record<string, unknown>): Promise<App> {
let app: App;
// Create the app
try {
// Create the app
return await componentsManager.instantiate(DEFAULT_APP, { variables });
app = await componentsManager.instantiate(DEFAULT_APP, { variables });
} catch (error: unknown) {
this.resolveError(`Could not create the server`, error);
}
// Ensure thread safety
if (!app.clusterManager.isSingleThreaded()) {
const violatingClasses = await listSingleThreadedComponents(componentsManager);
if (violatingClasses.length > 0) {
const verb = violatingClasses.length > 1 ? 'are' : 'is';
const detailedError = new InternalServerError(
`[${violatingClasses.join(', ')}] ${verb} not threadsafe and should not be run in multithreaded setups!`,
);
this.resolveError('Cannot run a singlethreaded-only component in a multithreaded setup!', detailedError);
}
}
return app;
}
/**

View File

@@ -1,10 +1,7 @@
import { readJson } from 'fs-extra';
import type { KeyValueStorage } from '../storage/keyvalue/KeyValueStorage';
import { resolveModulePath } from '../util/PathUtil';
import { readPackageJson } from '../util/PathUtil';
import { Initializer } from './Initializer';
const PACKAGE_JSON_PATH = resolveModulePath('package.json');
/**
* This initializer simply writes the version number of the server to the storage.
* This will be relevant in the future when we look into migration initializers.
@@ -22,7 +19,7 @@ export class ModuleVersionVerifier extends Initializer {
}
public async handle(): Promise<void> {
const pkg = await readJson(PACKAGE_JSON_PATH);
const pkg = await readPackageJson();
await this.storage.set(this.storageKey, pkg.version);
}
}

View File

@@ -0,0 +1,120 @@
import type { Worker } from 'cluster';
import cluster from 'cluster';
import { cpus } from 'os';
import { getLoggerFor } from '../../logging/LogUtil';
import { InternalServerError } from '../../util/errors/InternalServerError';
/**
* Different cluster modes.
*/
enum ClusterMode {
/** Scales in relation to `core_count`. */
autoScale,
/** Single threaded mode, no clustering */
singleThreaded,
/** Fixed amount of workers being forked. (limited to core_count) */
fixed
}
/**
* Convert workers amount to {@link ClusterMode}
* @param workers - Amount of workers
* @returns ClusterMode enum value
*/
function toClusterMode(workers: number): ClusterMode {
if (workers <= 0) {
return ClusterMode.autoScale;
}
if (workers === 1) {
return ClusterMode.singleThreaded;
}
return ClusterMode.fixed;
}
/**
* This class is responsible for deciding how many affective workers are needed.
* It also contains the logic for respawning workers when they are killed by the os.
*
* The workers values are interpreted as follows:
* value | actual workers |
* ------|--------------|
* `-m` | `num_cores - m` workers _(autoscale)_ (`m < num_cores`) |
* `-1` | `num_cores - 1` workers _(autoscale)_ |
* `0` | `num_cores` workers _(autoscale)_ |
* `1` | `single threaded mode` _(default)_ |
* `n` | `n` workers |
*/
export class ClusterManager {
private readonly logger = getLoggerFor(this);
private readonly workers: number;
private readonly clusterMode: ClusterMode;
public constructor(workers: number | string) {
const cores = cpus().length;
// Workaround for https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1182
if (typeof workers === 'string') {
workers = Number.parseInt(workers, 10);
}
if (workers <= -cores) {
throw new InternalServerError('Invalid workers value (should be in the interval ]-num_cores, +∞).');
}
this.workers = toClusterMode(workers) === ClusterMode.autoScale ? cores + workers : workers;
this.clusterMode = toClusterMode(this.workers);
}
/**
* Spawn all required workers.
*/
public spawnWorkers(): void {
let counter = 0;
this.logger.info(`Setting up ${this.workers} workers`);
for (let i = 0; i < this.workers; i++) {
cluster.fork().on('message', (msg: string): void => {
this.logger.info(msg);
});
}
cluster.on('online', (worker: Worker): void => {
this.logger.info(`Worker ${worker.process.pid} is listening`);
counter += 1;
if (counter === this.workers) {
this.logger.info(`All ${this.workers} requested workers have been started.`);
}
});
cluster.on('exit', (worker: Worker, code: number, signal: string): void => {
this.logger.warn(`Worker ${worker.process.pid} died with code ${code} and signal ${signal}`);
this.logger.warn('Starting a new worker');
cluster.fork().on('message', (msg: string): void => {
this.logger.info(msg);
});
});
}
/**
* Check whether the CSS server was booted in single threaded mode.
* @returns True is single threaded.
*/
public isSingleThreaded(): boolean {
return this.clusterMode === ClusterMode.singleThreaded;
}
/**
* Whether the calling process is the primary process.
* @returns True if primary
*/
public isPrimary(): boolean {
return cluster.isMaster;
}
/**
* Whether the calling process is a worker process.
* @returns True if worker
*/
public isWorker(): boolean {
return cluster.isWorker;
}
}

View File

@@ -0,0 +1,57 @@
import type { ComponentsManager } from 'componentsjs';
import { PrefetchedDocumentLoader } from 'componentsjs';
import { ContextParser } from 'jsonld-context-parser';
import { InternalServerError } from '../../util/errors/InternalServerError';
import { readPackageJson } from '../../util/PathUtil';
/**
* Indicates a class is only meant to work in singlethreaded setups and is thus not threadsafe.
*/
export interface SingleThreaded {}
/**
* Convert an exported interface name to the properly expected Components.js type URI.
* @param componentsManager - The currently used ComponentsManager
* @param interfaceName - An interface name
* @returns A Components.js type URI
*/
export async function toComponentsJsType<T>(componentsManager: ComponentsManager<T>, interfaceName: string):
Promise<string> {
const pkg = await readPackageJson();
const contextParser = new ContextParser({
documentLoader: new PrefetchedDocumentLoader({ contexts: componentsManager.moduleState.contexts }),
skipValidation: true,
});
// The keys of the package.json `lsd:contexts` array contains all the IRIs of the relevant contexts;
const lsdContexts = Object.keys(pkg['lsd:contexts']);
// Feed the lsd:context IRIs to the ContextParser
const cssContext = await contextParser.parse(lsdContexts);
// We can now expand a simple interface name, to its full Components.js type identifier.
const interfaceIRI = cssContext.expandTerm(interfaceName, true);
if (!interfaceIRI) {
throw new InternalServerError(`Could not expand ${interfaceName} to IRI!`);
}
return interfaceIRI;
}
/**
* Will list class names of components instantiated implementing the {@link SingleThreaded}
* interface while the application is being run in multithreaded mode.
* @param componentsManager - The componentsManager being used to set up the application
*/
export async function listSingleThreadedComponents<T>(componentsManager: ComponentsManager<T>): Promise<string[]> {
const interfaceType = await toComponentsJsType(componentsManager, 'SingleThreaded');
const violatingClasses: string[] = [];
// Loop through all instantiated Resources
for (const resource of componentsManager.getInstantiatedResources()) {
// If implementing interfaceType, while not being the interfaceType itself.
if (resource?.isA(interfaceType) && resource.value !== interfaceType) {
// Part after the # in an IRI is the actual class name
const name = resource.property?.type?.value?.split('#')?.[1];
violatingClasses.push(name);
}
}
return violatingClasses;
}

View File

@@ -0,0 +1,20 @@
import { Initializer } from '../Initializer';
import type { ClusterManager } from './ClusterManager';
/**
* Spawns the necessary workers when starting in multithreaded mode.
*/
export class WorkerManager extends Initializer {
private readonly clusterManager: ClusterManager;
public constructor(clusterManager: ClusterManager) {
super();
this.clusterManager = clusterManager;
}
public async handle(): Promise<void> {
if (!this.clusterManager.isSingleThreaded()) {
this.clusterManager.spawnWorkers();
}
}
}