Skip to content

Commit

Permalink
feat: add plugin system to workit (#216)
Browse files Browse the repository at this point in the history
* feat: add plugin system to workit

Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca>

* fix: lint issue

Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca>

* refactor(hookstate): address Sylvain comment

Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca>
  • Loading branch information
OlivierAlbertini committed Nov 27, 2020
1 parent 55b08bd commit 3d28cab
Show file tree
Hide file tree
Showing 30 changed files with 601 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
!packages/workit-core/tests/units/node_modules
lerna-debug.log
packages/*/lib
.idea
Expand Down
14 changes: 14 additions & 0 deletions packages/workit-bpm-client/src/camundaBpmClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ICreateWorkflowInstanceResponse,
IDeployWorkflowResponse,
IHttpResponse,
ILogger,
IMessage,
IPagination,
IPaginationOptions,
Expand All @@ -30,6 +31,7 @@ import {
IWorkflowOptions,
IWorkflowProcessIdDefinition,
} from 'workit-types';
import { IoC, PluginLoader, SERVICE_IDENTIFIER, NOOP_LOGGER } from 'workit-core';
import { PaginationUtils } from './utils/paginationUtils';

import { CamundaMessage } from './camundaMessage';
Expand All @@ -56,6 +58,10 @@ export class CamundaBpmClient implements IClient<ICamundaService>, IWorkflowClie
this._client = client;
this._config = config;
this._repo = new CamundaRepository(config);
const pluginLoader = new PluginLoader(IoC, this._getLogger());
if (config.plugins) {
pluginLoader.load(config.plugins);
}
}

public subscribe(onMessageReceived: (message: IMessage, service: ICamundaService) => Promise<void>): Promise<void> {
Expand Down Expand Up @@ -196,4 +202,12 @@ export class CamundaBpmClient implements IClient<ICamundaService>, IWorkflowClie
private _hasBpmnProcessId(request: IWorkflowDefinitionRequest): request is IWorkflowProcessIdDefinition {
return (request as IWorkflowProcessIdDefinition).bpmnProcessId !== undefined;
}

private _getLogger(): ILogger {
try {
return IoC.get(SERVICE_IDENTIFIER.logger);
} catch (error) {
return NOOP_LOGGER;
}
}
}
2 changes: 2 additions & 0 deletions packages/workit-core/src/config/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SuccessStrategySimple } from '../strategies/SuccessStrategySimple';
import { NoopTracerPropagator } from '../tracer/noopTracerPropagator';
import { SERVICE_IDENTIFIER } from './constants/identifiers';
import { IOC } from '../IoC';
import { NOOP_LOGGER } from '../common/noopLogger';

try {
decorate(injectable(), EventEmitter);
Expand All @@ -24,6 +25,7 @@ try {
const kernel = new Container();
const container = new Container();

kernel.bind(SERVICE_IDENTIFIER.logger).toConstantValue(NOOP_LOGGER);
kernel.bind(SERVICE_IDENTIFIER.tracer_propagator).toConstantValue(new NoopTracerPropagator());
kernel.bind(SERVICE_IDENTIFIER.tracer).toConstantValue(NOOP_TRACER);
kernel.bind(SERVICE_IDENTIFIER.success_strategy).toConstantValue(new SuccessStrategySimple());
Expand Down
3 changes: 3 additions & 0 deletions packages/workit-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
export * from './config/container';
export * from './config/constants/identifiers';
export * from './config/constants';
export * from './common/noopLogger';

export * from './processHandler/simpleCamundaProcessHandler';
export * from './interceptors';
Expand All @@ -24,4 +25,6 @@ export * from './proxyObserver';

export * from './worker';

export * from './plugin';

export * from './utils/utils';
40 changes: 40 additions & 0 deletions packages/workit-core/src/plugin/basePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 Ville de Montreal. All rights reserved.
* Licensed under the MIT license.
* See LICENSE file in the project root for full license information.
*/

import { IPluginConfig, IPlugin, ILogger, IIoC } from 'workit-types';

// TODO: add bpmn files and failures/success strategies as well

export abstract class BasePlugin implements IPlugin {
public supportedVersions?: string[];

public abstract readonly moduleName: string;

public readonly version?: string;

protected _ioc!: IIoC;

protected _logger!: ILogger;

protected _config!: IPluginConfig;

constructor(protected readonly packageName: string) {}

public enable(ioc: IIoC, logger: ILogger, config?: IPluginConfig): void {
this._ioc = ioc;
this._logger = logger;
if (config) this._config = config;
this.bind();
}

public disable(): void {
this.unbind();
}

protected abstract bind(): void;

protected abstract unbind(): void;
}
7 changes: 7 additions & 0 deletions packages/workit-core/src/plugin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright (c) 2020 Ville de Montreal. All rights reserved.
* Licensed under the MIT license.
* See LICENSE file in the project root for full license information.
*/
export * from './basePlugin';
export * from './pluginLoader';
138 changes: 138 additions & 0 deletions packages/workit-core/src/plugin/pluginLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright (c) 2020 Ville de Montreal. All rights reserved.
* Licensed under the MIT license.
* See LICENSE file in the project root for full license information.
*/

/* eslint @typescript-eslint/no-unsafe-assignment: 0 */
/* eslint @typescript-eslint/no-unsafe-call: 0 */
/* eslint @typescript-eslint/no-unsafe-member-access: 0 */
/* eslint @typescript-eslint/no-var-requires: 0 */
/* eslint @typescript-eslint/no-unsafe-return: 0 */
/* eslint @typescript-eslint/explicit-member-accessibility: 0 */
/* eslint @typescript-eslint/restrict-template-expressions: 0 */
/* eslint import/no-dynamic-require: 0 */
/* eslint global-require: 0 */
/* eslint no-restricted-syntax: 0 */

import { IPlugin, IPlugins, HookState, ILogger, IIoC } from 'workit-types';

/**
* Returns the Plugins object that meet the below conditions.
* Valid criteria: 1. It should be enabled. 2. Should have non-empty path.
*/
function filterPlugins(plugins: IPlugins): IPlugins {
const keys = Object.keys(plugins);
return keys.reduce((acc: IPlugins, key: string) => {
if (plugins[key].enabled && plugins[key].path) acc[key] = plugins[key];
return acc;
}, {});
}

/**
* The PluginLoader class can load instrumentation plugins that use a patch
* mechanism to enable automatic tracing for specific target modules.
*/
export class PluginLoader {
/** A list of loaded plugins. */
private _plugins: IPlugin[] = [];

/**
* A field that tracks whether the plugin has been loaded
* for the first time, as well as whether the plugin is activated or not.
*/
private _hookState = HookState.UNINITIALIZED;

/** Constructs a new PluginLoader instance. */
constructor(readonly ioc: IIoC, readonly logger: ILogger) {}

/**
* Loads a list of plugins. Each plugin module should implement the core
* {@link Plugin} interface and export an instance named as 'plugin'.
* @param Plugins an object whose keys are plugin names and whose
* {@link PluginConfig} values indicate several configuration options.
*/
load(plugins: IPlugins): void {
if (this._hookState === HookState.UNINITIALIZED) {
const pluginsToLoad = filterPlugins(plugins);
const modulesToHook = Object.keys(pluginsToLoad);

if (modulesToHook.length === 0) {
this._hookState = HookState.UNLOADED;
return;
}

const alreadyRequiredModules = Object.keys(require.cache);
const requiredModulesToHook = modulesToHook.filter(
(name) =>
alreadyRequiredModules.find((cached) => {
try {
return require.resolve(name) === cached;
} catch (err) {
return false;
}
}) !== undefined
);

if (requiredModulesToHook.length > 0) {
this.logger.info(
`Some modules (${requiredModulesToHook.join(
', '
)}) were already required when their respective plugin was loaded, some plugins might not work. Make sure Workit is setup before you require in other modules.`
);
}

modulesToHook.forEach((name) => {
const config = pluginsToLoad[name];
const modulePath = config.path!;
const version = null;

this.logger.info(`PluginLoader#load: trying loading ${name}@${version}`);
this.logger.debug(`PluginLoader#load: applying binding to ${name}@${version} using ${modulePath} module`);

// Expecting a plugin from module;
try {
const { plugin } = require(modulePath);

if (plugin.moduleName !== name) {
this.logger.error(`PluginLoader#load: Entry ${name} use a plugin that instruments ${plugin.moduleName}`);
return exports;
}

this._plugins.push(plugin);
// Enable each supported plugin.
return plugin.enable(this.ioc, this.logger, config);
} catch (e) {
this.logger.error(
`PluginLoader#load: could not load plugin ${modulePath} of module ${name}. Error: ${e.message}`
);
return exports;
}
});
this._hookState = HookState.LOADED;
} else if (this._hookState === HookState.UNLOADED) {
this.logger.error('PluginLoader#load: Currently cannot re-enable plugin loader.');
} else {
this.logger.error('PluginLoader#load: Plugin loader already enabled.');
}
}

/** Unloads plugins. */
unload(): void {
if (this._hookState === HookState.LOADED) {
for (const plugin of this._plugins) {
plugin.disable();
}
this._plugins = [];
this._hookState = HookState.UNLOADED;
}
}
}

/**
* Adds a search path for plugin modules. Intended for testing purposes only.
* @param searchPath The path to add.
*/
export function searchPathForTest(searchPath: string) {
module.paths.push(searchPath);
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3d28cab

Please sign in to comment.