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
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
120
src/init/cluster/ClusterManager.ts
Normal file
120
src/init/cluster/ClusterManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
57
src/init/cluster/SingleThreaded.ts
Normal file
57
src/init/cluster/SingleThreaded.ts
Normal 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;
|
||||
}
|
||||
20
src/init/cluster/WorkerManager.ts
Normal file
20
src/init/cluster/WorkerManager.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user