Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add plugin system to workit #216

Merged
merged 3 commits into from
Nov 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
OlivierAlbertini marked this conversation as resolved.
Show resolved Hide resolved
} 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.

Loading