mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(core): Add commands to workers to respond with current state (#7029)
This PR adds new endpoints to the REST API: `/orchestration/worker/status` and `/orchestration/worker/id` Currently these just trigger the return of status / ids from the workers via the redis back channel, this still needs to be handled and passed through to the frontend. It also adds the eventbus to each worker, and triggers a reload of those eventbus instances when the configuration changes on the main instances.
This commit is contained in:
committed by
GitHub
parent
0a35025e5e
commit
7b49cf2a2c
@@ -27,6 +27,11 @@ import { generateHostInstanceId } from '@/databases/utils/generators';
|
||||
import type { ICredentialsOverwrite } from '@/Interfaces';
|
||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||
import { rawBodyReader, bodyParser } from '@/middlewares';
|
||||
import { eventBus } from '../eventbus';
|
||||
import { RedisServicePubSubPublisher } from '../services/redis/RedisServicePubSubPublisher';
|
||||
import { RedisServicePubSubSubscriber } from '../services/redis/RedisServicePubSubSubscriber';
|
||||
import { EventMessageGeneric } from '../eventbus/EventMessageClasses/EventMessageGeneric';
|
||||
import { getWorkerCommandReceivedHandler } from './workerCommandHandler';
|
||||
|
||||
export class Worker extends BaseCommand {
|
||||
static description = '\nStarts a n8n worker';
|
||||
@@ -49,6 +54,10 @@ export class Worker extends BaseCommand {
|
||||
|
||||
readonly uniqueInstanceId = generateHostInstanceId('worker');
|
||||
|
||||
redisPublisher: RedisServicePubSubPublisher;
|
||||
|
||||
redisSubscriber: RedisServicePubSubSubscriber;
|
||||
|
||||
/**
|
||||
* Stop n8n in a graceful way.
|
||||
* Make for example sure that all the webhooks from third party services
|
||||
@@ -240,9 +249,48 @@ export class Worker extends BaseCommand {
|
||||
await this.initBinaryManager();
|
||||
await this.initExternalHooks();
|
||||
await this.initExternalSecrets();
|
||||
await this.initEventBus();
|
||||
await this.initRedis();
|
||||
await this.initQueue();
|
||||
}
|
||||
|
||||
async run() {
|
||||
async initEventBus() {
|
||||
await eventBus.initialize({
|
||||
workerId: this.uniqueInstanceId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the redis connection
|
||||
* A publishing connection to redis is created to publish events to the event log
|
||||
* A subscription connection to redis is created to subscribe to commands from the main process
|
||||
* The subscription connection adds a handler to handle the command messages
|
||||
*/
|
||||
async initRedis() {
|
||||
this.redisPublisher = Container.get(RedisServicePubSubPublisher);
|
||||
this.redisSubscriber = Container.get(RedisServicePubSubSubscriber);
|
||||
await this.redisPublisher.init();
|
||||
await this.redisPublisher.publishToEventLog(
|
||||
new EventMessageGeneric({
|
||||
eventName: 'n8n.worker.started',
|
||||
payload: {
|
||||
workerId: this.uniqueInstanceId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
await this.redisSubscriber.subscribeToCommandChannel();
|
||||
this.redisSubscriber.addMessageHandler(
|
||||
'WorkerCommandReceivedHandler',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
getWorkerCommandReceivedHandler({
|
||||
uniqueInstanceId: this.uniqueInstanceId,
|
||||
redisPublisher: this.redisPublisher,
|
||||
getRunningJobIds: () => Object.keys(Worker.runningJobs),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async initQueue() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const { flags } = this.parse(Worker);
|
||||
|
||||
@@ -255,11 +303,6 @@ export class Worker extends BaseCommand {
|
||||
this.runJob(job, this.nodeTypes),
|
||||
);
|
||||
|
||||
this.logger.info('\nn8n worker is now ready');
|
||||
this.logger.info(` * Version: ${N8N_VERSION}`);
|
||||
this.logger.info(` * Concurrency: ${flags.concurrency}`);
|
||||
this.logger.info('');
|
||||
|
||||
Worker.jobQueue.on('global:progress', (jobId: JobId, progress) => {
|
||||
// Progress of a job got updated which does get used
|
||||
// to communicate that a job got canceled.
|
||||
@@ -305,105 +348,116 @@ export class Worker extends BaseCommand {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (config.getEnv('queue.health.active')) {
|
||||
const port = config.getEnv('queue.health.port');
|
||||
async setupHealthMonitor() {
|
||||
const port = config.getEnv('queue.health.port');
|
||||
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
|
||||
const server = http.createServer(app);
|
||||
const server = http.createServer(app);
|
||||
|
||||
app.get(
|
||||
'/healthz',
|
||||
app.get(
|
||||
'/healthz',
|
||||
|
||||
async (req: express.Request, res: express.Response) => {
|
||||
LoggerProxy.debug('Health check started!');
|
||||
|
||||
const connection = Db.getConnection();
|
||||
|
||||
try {
|
||||
if (!connection.isInitialized) {
|
||||
// Connection is not active
|
||||
throw new Error('No active database connection!');
|
||||
}
|
||||
// DB ping
|
||||
await connection.query('SELECT 1');
|
||||
} catch (e) {
|
||||
LoggerProxy.error('No Database connection!', e as Error);
|
||||
const error = new ResponseHelper.ServiceUnavailableError('No Database connection!');
|
||||
return ResponseHelper.sendErrorResponse(res, error);
|
||||
}
|
||||
|
||||
// Just to be complete, generally will the worker stop automatically
|
||||
// if it loses the connection to redis
|
||||
try {
|
||||
// Redis ping
|
||||
await Worker.jobQueue.client.ping();
|
||||
} catch (e) {
|
||||
LoggerProxy.error('No Redis connection!', e as Error);
|
||||
const error = new ResponseHelper.ServiceUnavailableError('No Redis connection!');
|
||||
return ResponseHelper.sendErrorResponse(res, error);
|
||||
}
|
||||
|
||||
// Everything fine
|
||||
const responseData = {
|
||||
status: 'ok',
|
||||
};
|
||||
|
||||
LoggerProxy.debug('Health check completed successfully!');
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, responseData, true, 200);
|
||||
},
|
||||
);
|
||||
|
||||
let presetCredentialsLoaded = false;
|
||||
const endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
|
||||
if (endpointPresetCredentials !== '') {
|
||||
// POST endpoint to set preset credentials
|
||||
app.post(
|
||||
`/${endpointPresetCredentials}`,
|
||||
rawBodyReader,
|
||||
bodyParser,
|
||||
async (req: express.Request, res: express.Response) => {
|
||||
LoggerProxy.debug('Health check started!');
|
||||
if (!presetCredentialsLoaded) {
|
||||
const body = req.body as ICredentialsOverwrite;
|
||||
|
||||
const connection = Db.getConnection();
|
||||
|
||||
try {
|
||||
if (!connection.isInitialized) {
|
||||
// Connection is not active
|
||||
throw new Error('No active database connection!');
|
||||
}
|
||||
// DB ping
|
||||
await connection.query('SELECT 1');
|
||||
} catch (e) {
|
||||
LoggerProxy.error('No Database connection!', e as Error);
|
||||
const error = new ResponseHelper.ServiceUnavailableError('No Database connection!');
|
||||
return ResponseHelper.sendErrorResponse(res, error);
|
||||
}
|
||||
|
||||
// Just to be complete, generally will the worker stop automatically
|
||||
// if it loses the connection to redis
|
||||
try {
|
||||
// Redis ping
|
||||
await Worker.jobQueue.client.ping();
|
||||
} catch (e) {
|
||||
LoggerProxy.error('No Redis connection!', e as Error);
|
||||
const error = new ResponseHelper.ServiceUnavailableError('No Redis connection!');
|
||||
return ResponseHelper.sendErrorResponse(res, error);
|
||||
}
|
||||
|
||||
// Everything fine
|
||||
const responseData = {
|
||||
status: 'ok',
|
||||
};
|
||||
|
||||
LoggerProxy.debug('Health check completed successfully!');
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, responseData, true, 200);
|
||||
},
|
||||
);
|
||||
|
||||
let presetCredentialsLoaded = false;
|
||||
const endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
|
||||
if (endpointPresetCredentials !== '') {
|
||||
// POST endpoint to set preset credentials
|
||||
app.post(
|
||||
`/${endpointPresetCredentials}`,
|
||||
rawBodyReader,
|
||||
bodyParser,
|
||||
async (req: express.Request, res: express.Response) => {
|
||||
if (!presetCredentialsLoaded) {
|
||||
const body = req.body as ICredentialsOverwrite;
|
||||
|
||||
if (req.contentType !== 'application/json') {
|
||||
ResponseHelper.sendErrorResponse(
|
||||
res,
|
||||
new Error(
|
||||
'Body must be a valid JSON, make sure the content-type is application/json',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
CredentialsOverwrites().setData(body);
|
||||
presetCredentialsLoaded = true;
|
||||
ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200);
|
||||
} else {
|
||||
if (req.contentType !== 'application/json') {
|
||||
ResponseHelper.sendErrorResponse(
|
||||
res,
|
||||
new Error('Preset credentials can be set once'),
|
||||
new Error(
|
||||
'Body must be a valid JSON, make sure the content-type is application/json',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
CredentialsOverwrites().setData(body);
|
||||
presetCredentialsLoaded = true;
|
||||
ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200);
|
||||
} else {
|
||||
ResponseHelper.sendErrorResponse(res, new Error('Preset credentials can be set once'));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
server.on('error', (error: Error & { code: string }) => {
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
this.logger.error(
|
||||
`n8n's port ${port} is already in use. Do you have the n8n main process running on that port?`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
server.on('error', (error: Error & { code: string }) => {
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
this.logger.error(
|
||||
`n8n's port ${port} is already in use. Do you have the n8n main process running on that port?`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(port, () => resolve()));
|
||||
await this.externalHooks.run('worker.ready');
|
||||
this.logger.info(`\nn8n worker health check via, port ${port}`);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => server.listen(port, () => resolve()));
|
||||
await this.externalHooks.run('worker.ready');
|
||||
this.logger.info(`\nn8n worker health check via, port ${port}`);
|
||||
async run() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const { flags } = this.parse(Worker);
|
||||
|
||||
this.logger.info('\nn8n worker is now ready');
|
||||
this.logger.info(` * Version: ${N8N_VERSION}`);
|
||||
this.logger.info(` * Concurrency: ${flags.concurrency}`);
|
||||
this.logger.info('');
|
||||
|
||||
if (config.getEnv('queue.health.active')) {
|
||||
await this.setupHealthMonitor();
|
||||
}
|
||||
|
||||
// Make sure that the process does not close
|
||||
|
||||
Reference in New Issue
Block a user