mongo-lead
Version:
Leader election backed by MongoDB
153 lines • 4.95 kB
JavaScript
import { Db } from 'mongodb';
import { EventEmitter } from 'events';
import crypto from 'crypto';
export default class Leader extends EventEmitter {
id;
db;
options;
paused;
initiated;
constructor(db, options = {}) {
super();
this.options = {
collectionName: 'leader',
groupName: 'default',
ttl: 1000,
wait: 500,
...options,
};
this.id = crypto.randomUUID();
this.db = db;
this.paused = false;
this.initiated = false;
}
async isLeader() {
if (this.paused)
return false;
if (!this.initiated) {
await this.start();
}
const item = await this.getCollection().findOne({ 'leader-id': this.id });
return item != null && item['leader-id'] === this.id;
}
async elect() {
if (this.paused)
return;
try {
const collection = this.getCollection();
const exists = (await collection.countDocuments()) > 0;
const result = exists
? null
: await collection.findOneAndUpdate({
groupName: this.options.groupName,
}, {
$set: {
'leader-id': this.id,
createdAt: new Date(),
},
$setOnInsert: {
groupName: this.options.groupName,
},
}, {
upsert: true,
returnDocument: 'after',
});
const isElected = result && result['leader-id'] === this.id;
if (!isElected) {
setTimeout(() => this.elect(), this.options.wait);
}
else {
this.emit('elected');
setTimeout(() => this.renew(), Math.floor(this.options.ttl / 4));
}
}
catch (error) {
console.error('Election error:', error);
setTimeout(() => this.elect(), this.options.wait);
}
}
async renew() {
if (this.paused)
return;
try {
const expiresAt = new Date(Date.now() - this.options.ttl);
const result = await this.getCollection().findOneAndUpdate({
'leader-id': this.id,
groupName: this.options.groupName,
createdAt: { $gt: expiresAt },
}, {
$set: {
createdAt: new Date(),
},
}, {
returnDocument: 'after',
});
if (result) {
setTimeout(() => this.renew(), Math.floor(this.options.ttl / 4));
}
else {
this.emit('revoked');
setTimeout(() => this.elect(), this.options.wait);
}
}
catch (error) {
console.error('Renewal error:', error);
this.emit('revoked');
setTimeout(() => this.elect(), this.options.wait);
}
}
pause() {
if (!this.paused)
this.paused = true;
}
async resume() {
if (this.paused) {
this.paused = false;
await this.elect();
}
}
async start() {
if (!this.initiated) {
this.initiated = true;
await this.initDatabase();
await this.elect();
}
}
async initDatabase() {
await this.db.command({ ping: 1 });
if (this.options.ttl < 1000) {
try {
await this.db
.admin()
.command({ setParameter: 1, ttlMonitorSleepSecs: 1 });
}
catch (err) {
console.warn('Unable to set TTL monitor sleep time. This is not critical, but TTL precision may be reduced.', err);
}
}
const collection = await this.createCollection();
const ttlSeconds = Math.max(Math.floor(this.options.ttl / 1000), 1);
try {
await collection.dropIndex('createdAt_1');
await collection.dropIndex('groupName_1_createdAt_1');
}
catch (err) {
}
await collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: ttlSeconds });
await collection.createIndex({ groupName: 1 });
}
async createCollection() {
const cursor = this.db.listCollections({
name: this.options.collectionName,
});
const exists = await cursor.hasNext();
const collection = exists
? this.db.collection(this.options.collectionName)
: await this.db.createCollection(this.options.collectionName);
return collection;
}
getCollection() {
return this.db.collection(this.options.collectionName);
}
}
//# sourceMappingURL=index.js.map