112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import { EventEmitter } from 'node:events';
|
|
import config from '@/config';
|
|
import { Service } from 'typedi';
|
|
import { TIME } from '@/constants';
|
|
import { getRedisPrefix } from '@/services/redis/RedisServiceHelper';
|
|
import { ErrorReporterProxy as EventReporter } from 'n8n-workflow';
|
|
import { Logger } from '@/Logger';
|
|
import { RedisServicePubSubPublisher } from '@/services/redis/RedisServicePubSubPublisher';
|
|
|
|
@Service()
|
|
export class MultiMainSetup extends EventEmitter {
|
|
constructor(
|
|
private readonly logger: Logger,
|
|
private readonly redisPublisher: RedisServicePubSubPublisher,
|
|
) {
|
|
super();
|
|
}
|
|
|
|
get instanceId() {
|
|
return config.getEnv('redis.queueModeId');
|
|
}
|
|
|
|
private readonly leaderKey = getRedisPrefix() + ':main_instance_leader';
|
|
|
|
private readonly leaderKeyTtl = config.getEnv('multiMainSetup.ttl');
|
|
|
|
private leaderCheckInterval: NodeJS.Timer | undefined;
|
|
|
|
async init() {
|
|
await this.tryBecomeLeader(); // prevent initial wait
|
|
|
|
this.leaderCheckInterval = setInterval(
|
|
async () => {
|
|
await this.checkLeader();
|
|
},
|
|
config.getEnv('multiMainSetup.interval') * TIME.SECOND,
|
|
);
|
|
}
|
|
|
|
async shutdown() {
|
|
clearInterval(this.leaderCheckInterval);
|
|
|
|
const isLeader = config.getEnv('multiMainSetup.instanceType') === 'leader';
|
|
|
|
if (isLeader) await this.redisPublisher.clear(this.leaderKey);
|
|
}
|
|
|
|
private async checkLeader() {
|
|
const leaderId = await this.redisPublisher.get(this.leaderKey);
|
|
|
|
if (leaderId === this.instanceId) {
|
|
this.logger.debug(`[Instance ID ${this.instanceId}] Leader is this instance`);
|
|
|
|
await this.redisPublisher.setExpiration(this.leaderKey, this.leaderKeyTtl);
|
|
|
|
return;
|
|
}
|
|
|
|
if (leaderId && leaderId !== this.instanceId) {
|
|
this.logger.debug(`[Instance ID ${this.instanceId}] Leader is other instance "${leaderId}"`);
|
|
|
|
if (config.getEnv('multiMainSetup.instanceType') === 'leader') {
|
|
config.set('multiMainSetup.instanceType', 'follower');
|
|
|
|
this.emit('leadershipChange'); // stop triggers, pollers, pruning
|
|
|
|
EventReporter.report('[Multi-main setup] Leader failed to renew leader key', {
|
|
level: 'info',
|
|
});
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!leaderId) {
|
|
this.logger.debug(
|
|
`[Instance ID ${this.instanceId}] Leadership vacant, attempting to become leader...`,
|
|
);
|
|
|
|
config.set('multiMainSetup.instanceType', 'follower');
|
|
|
|
this.emit('leadershipVacant'); // stop triggers, pollers, pruning
|
|
|
|
await this.tryBecomeLeader();
|
|
}
|
|
}
|
|
|
|
private async tryBecomeLeader() {
|
|
// this can only succeed if leadership is currently vacant
|
|
const keySetSuccessfully = await this.redisPublisher.setIfNotExists(
|
|
this.leaderKey,
|
|
this.instanceId,
|
|
);
|
|
|
|
if (keySetSuccessfully) {
|
|
this.logger.debug(`[Instance ID ${this.instanceId}] Leader is now this instance`);
|
|
|
|
config.set('multiMainSetup.instanceType', 'leader');
|
|
|
|
await this.redisPublisher.setExpiration(this.leaderKey, this.leaderKeyTtl);
|
|
|
|
this.emit('leadershipChange'); // start triggers, pollers, pruning
|
|
} else {
|
|
config.set('multiMainSetup.instanceType', 'follower');
|
|
}
|
|
}
|
|
|
|
async fetchLeaderKey() {
|
|
return await this.redisPublisher.get(this.leaderKey);
|
|
}
|
|
}
|