feat(server): add cluster mode for starting app (#4895)

* feat(server): add cluster mode for starting app

* chore(env): adjust order of env item

* chore: sync in main data source

* chore: onSync in plugin

* refactor(server): change onSync to plugin method

---------

Co-authored-by: Chareice <chareice@live.com>
This commit is contained in:
Junyi 2024-07-18 23:23:23 +08:00 committed by GitHub
parent c1dba8ac91
commit 1c9e71dbf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 55 additions and 14 deletions

View File

@ -29,6 +29,12 @@ LOGGER_MAX_SIZE=
# console | json | logfmt | delimiter # console | json | logfmt | delimiter
LOGGER_FORMAT= LOGGER_FORMAT=
# Start application in cluster mode when the value is set (same as pm2 -i <cluster_mode>).
# Cluster mode will only work properly when plugins related to distributed architecture are enabled.
# Otherwise, the application's functionality may encounter unexpected issues.
# The cluster mode will not work in development mode either.
CLUSTER_MODE=
################# DATABASE ################# ################# DATABASE #################
# postgres | msysql | mariadb | sqlite # postgres | msysql | mariadb | sqlite

View File

@ -33,6 +33,7 @@ module.exports = (cli) => {
.command('start') .command('start')
.option('-p, --port [port]') .option('-p, --port [port]')
.option('-d, --daemon') .option('-d, --daemon')
.option('-i, --instances [instances]')
.option('--db-sync') .option('--db-sync')
.option('--quickstart') .option('--quickstart')
.allowUnknownOption() .allowUnknownOption()
@ -61,13 +62,16 @@ module.exports = (cli) => {
} }
await postCheck(opts); await postCheck(opts);
deleteSockFiles(); deleteSockFiles();
const instances = opts.instances || process.env.CLUSTER_MODE;
const instancesArgs = instances ? ['-i', instances] : [];
if (opts.daemon) { if (opts.daemon) {
run('pm2', ['start', `${APP_PACKAGE_ROOT}/lib/index.js`, '--', ...process.argv.slice(2)]); run('pm2', ['start', ...instancesArgs, `${APP_PACKAGE_ROOT}/lib/index.js`, '--', ...process.argv.slice(2)]);
} else { } else {
run( run(
'pm2-runtime', 'pm2-runtime',
[ [
'start', 'start',
...instancesArgs,
`${APP_PACKAGE_ROOT}/lib/index.js`, `${APP_PACKAGE_ROOT}/lib/index.js`,
NODE_ARGS ? `--node-args="${NODE_ARGS}"` : undefined, NODE_ARGS ? `--node-args="${NODE_ARGS}"` : undefined,
'--', '--',

View File

@ -18,6 +18,7 @@ import { resolve } from 'path';
import { Application } from './application'; import { Application } from './application';
import { InstallOptions, getExposeChangelogUrl, getExposeReadmeUrl } from './plugin-manager'; import { InstallOptions, getExposeChangelogUrl, getExposeReadmeUrl } from './plugin-manager';
import { checkAndGetCompatible, getPluginBasePath } from './plugin-manager/utils'; import { checkAndGetCompatible, getPluginBasePath } from './plugin-manager/utils';
import { SyncMessageData } from './sync-manager';
export interface PluginInterface { export interface PluginInterface {
beforeLoad?: () => void; beforeLoad?: () => void;
@ -133,6 +134,12 @@ export abstract class Plugin<O = any> implements PluginInterface {
async afterRemove() {} async afterRemove() {}
/**
* Fired when a sync message is received.
* @experimental
*/
onSync(message: SyncMessageData): Promise<void> | void {}
/** /**
* @deprecated * @deprecated
*/ */

View File

@ -34,7 +34,7 @@ export class SyncManager {
private nodeId: string; private nodeId: string;
private app: Application; private app: Application;
private eventEmitter = new EventEmitter(); private eventEmitter = new EventEmitter();
private adapter = null; private adapter: SyncAdapter = null;
private incomingBuffer: SyncMessageData[] = []; private incomingBuffer: SyncMessageData[] = [];
private outgoingBuffer: [string, SyncMessageData][] = []; private outgoingBuffer: [string, SyncMessageData][] = [];
private flushTimer: NodeJS.Timeout = null; private flushTimer: NodeJS.Timeout = null;
@ -43,12 +43,21 @@ export class SyncManager {
return this.adapter ? this.adapter.ready : false; return this.adapter ? this.adapter.ready : false;
} }
private onMessage(namespace, message) {
this.app.logger.info(`emit sync event in namespace ${namespace}`);
this.eventEmitter.emit(namespace, message);
const pluginInstance = this.app.pm.get(namespace);
pluginInstance.onSync(message);
}
private onSync = (messages: SyncMessage[]) => { private onSync = (messages: SyncMessage[]) => {
this.app.logger.info('sync messages received into buffer:', messages); this.app.logger.info('sync messages received, save into buffer:', messages);
if (this.flushTimer) { if (this.flushTimer) {
clearTimeout(this.flushTimer); clearTimeout(this.flushTimer);
this.flushTimer = null; this.flushTimer = null;
} }
this.incomingBuffer = uniqWith( this.incomingBuffer = uniqWith(
this.incomingBuffer.concat( this.incomingBuffer.concat(
messages messages
@ -60,9 +69,9 @@ export class SyncManager {
this.flushTimer = setTimeout(() => { this.flushTimer = setTimeout(() => {
this.incomingBuffer.forEach(({ namespace, ...message }) => { this.incomingBuffer.forEach(({ namespace, ...message }) => {
this.app.logger.info(`emit sync event in namespace ${namespace}`); this.onMessage(namespace, message);
this.eventEmitter.emit(namespace, message);
}); });
this.incomingBuffer = [];
}, 1000); }, 1000);
}; };
@ -82,9 +91,11 @@ export class SyncManager {
if (this.adapter) { if (this.adapter) {
throw new Error('sync adapter is already exists'); throw new Error('sync adapter is already exists');
} }
if (!adapter) { if (!adapter) {
return; return;
} }
this.adapter = adapter; this.adapter = adapter;
this.adapter.on('message', this.onSync); this.adapter.on('message', this.onSync);
this.adapter.on('ready', this.onReady); this.adapter.on('ready', this.onReady);

View File

@ -40,6 +40,19 @@ export class PluginDataSourceMainServer extends Plugin {
this.loadFilter = filter; this.loadFilter = filter;
} }
async onSync(message) {
const { type, collectionName } = message;
if (type === 'newCollection') {
const collectionModel: CollectionModel = await this.app.db.getCollection('collections').repository.findOne({
filter: {
name: collectionName,
},
});
await collectionModel.load();
}
}
async beforeLoad() { async beforeLoad() {
if (this.app.db.inDialect('postgres')) { if (this.app.db.inDialect('postgres')) {
this.schema = process.env.COLLECTION_MANAGER_SCHEMA || this.db.options.schema || 'public'; this.schema = process.env.COLLECTION_MANAGER_SCHEMA || this.db.options.schema || 'public';
@ -77,6 +90,11 @@ export class PluginDataSourceMainServer extends Plugin {
await model.migrate({ await model.migrate({
transaction, transaction,
}); });
this.app.syncManager.publish(this.name, {
type: 'newCollection',
collectionName: model.get('name'),
});
} }
}, },
); );

View File

@ -66,7 +66,7 @@ export default class PluginFileManagerServer extends Plugin {
} }
} }
private onSync = async (message) => { async onSync(message) {
if (message.type === 'storageChange') { if (message.type === 'storageChange') {
const storage = await this.db.getRepository('storages').findOne({ const storage = await this.db.getRepository('storages').findOne({
filterByTk: message.storageId, filterByTk: message.storageId,
@ -79,7 +79,7 @@ export default class PluginFileManagerServer extends Plugin {
const id = Number.parseInt(message.storageId, 10); const id = Number.parseInt(message.storageId, 10);
this.storagesCache.delete(id); this.storagesCache.delete(id);
} }
}; }
async beforeLoad() { async beforeLoad() {
this.db.registerModels({ FileModel }); this.db.registerModels({ FileModel });
@ -90,8 +90,6 @@ export default class PluginFileManagerServer extends Plugin {
}); });
this.app.on('afterStart', async () => { this.app.on('afterStart', async () => {
await this.loadStorages(); await this.loadStorages();
this.app.syncManager.subscribe(this.name, this.onSync);
}); });
} }

View File

@ -110,7 +110,7 @@ export default class PluginWorkflowServer extends Plugin {
} }
}; };
private onSync = async (message) => { async onSync(message) {
if (message.type === 'statusChange') { if (message.type === 'statusChange') {
const workflowId = Number.parseInt(message.workflowId, 10); const workflowId = Number.parseInt(message.workflowId, 10);
const enabled = Number.parseInt(message.enabled, 10); const enabled = Number.parseInt(message.enabled, 10);
@ -133,7 +133,7 @@ export default class PluginWorkflowServer extends Plugin {
} }
} }
} }
}; }
/** /**
* @experimental * @experimental
@ -284,7 +284,6 @@ export default class PluginWorkflowServer extends Plugin {
this.app.on('afterStart', async () => { this.app.on('afterStart', async () => {
this.app.setMaintainingMessage('check for not started executions'); this.app.setMaintainingMessage('check for not started executions');
this.ready = true; this.ready = true;
this.app.syncManager.subscribe(this.name, this.onSync);
const collection = db.getCollection('workflows'); const collection = db.getCollection('workflows');
const workflows = await collection.repository.find({ const workflows = await collection.repository.find({
@ -308,8 +307,6 @@ export default class PluginWorkflowServer extends Plugin {
this.toggle(workflow, false); this.toggle(workflow, false);
} }
this.app.syncManager.unsubscribe('workflow', this.onSync);
this.ready = false; this.ready = false;
if (this.events.length) { if (this.events.length) {
await this.prepare(); await this.prepare();