This commit is contained in:
mike k
2021-01-24 17:03:09 -05:00
parent bf4b2bb113
commit a338d6158a
3 changed files with 117 additions and 86 deletions

116
README.md
View File

@@ -1 +1,115 @@
README.md
# Safe Redis Leader
## Goal
The Safe Redis Leader JS module is designed to provide a leader election implementation that provides tested gaurentees that there is only a single leader elected from a group of clients at one time.
The implementation is a port of the stale [Redis Leader npm package](https://github.com/pierreinglebert/redis-leader) that implements a solution to the [known race condition](https://github.com/pierreinglebert/redis-leader/blob/c3b4db5df9802908728ad0ae4310a52e74acb462/index.js#L81). Additionally, this rewritten package:
1. Removes the usage of `.bind` and `this`, as well as prototype inheritance (Without introducing classes in the main impl)
2. Only exposes public api functions that should be exposed (no more public-but-should-be-private `_elect` fn)
3. has a test suite within docker-compose using a real redis instance, which allows anyone to run the tests with no heavy dependency setup
4. Has tests to assert the known race condition can no longer occur
5. removes the need for `new`, by providing a simple `createSafeRedisLeader(...)` public fn
6. Replace callback-hell with async/await
## Usage
Install the package:
```bash
npm install --save safe-redis-leader
```
in one terminal, run the follow index.js:
```javascript
const {createSafeRedisLeader} = require('safe-redis-leader')
const Redis = require('ioredis')
async function main(){
const asyncRedis = new Redis({
host: "locahost",
port: 6379,
password: "some-password"
})
const leaderElectionKey = 'the-election'
const safeLeader = await createSafeRedisLeader({
asyncRedis: asyncRedis,
ttl: 1500,
wait: 3000,
key: leaderElectionKey
})
safeLeader.on("elected", ()=>{
console.log("I'm the leader - 1")
})
await safeLeader.elect()
}
main().catch((e)=>{
console.error(e)
process.exit(1)
})
```
In a seperate terminal/tab, run the following index.js:
```javascript
const {createSafeRedisLeader} = require('safe-redis-leader')
const Redis = require('ioredis')
async function main(){
const asyncRedis = new Redis({
host: "locahost",
port: 6379,
password: "some-password"
})
const leaderElectionKey = 'the-election'
const safeLeader = await createSafeRedisLeader({
asyncRedis: asyncRedis,
ttl: 1500,
wait: 3000,
key: leaderElectionKey
})
safeLeader.on("elected", ()=>{
console.log("I'm the leader - 2")
})
await safeLeader.elect()
}
main().catch((e)=>{
console.error(e)
process.exit(1)
})
```
## Run Library Tests
npm run docker:test
# License
MIT

View File

@@ -2,7 +2,7 @@
"name": "safe-redis-leader",
"version": "0.0.1",
"description": "Redis leader election implementation that does not have any race conditions",
"main": "index.js",
"main": "src/src/index.js",
"scripts": {
"test": "npm install && node ./docker/scripts/runner.js"
},

View File

@@ -107,87 +107,4 @@ async function createSafeRedisLeader({
}
module.exports.createSafeRedisLeader = createSafeRedisLeader
// function Leader(redis, options) {
// options = options || {};
// this.id = uuid.v4();
// this.redis = redis;
// this.options = {};
// this.options.ttl = options.ttl || 10000; // Lock time to live in milliseconds
// this.options.wait = options.wait || 1000; // time between 2 tries to get lock
// this.key = hashKey(options.key || 'default');
// }
// util.inherits(Leader, EventEmitter);
// /**
// * Renew leader as elected
// */
// Leader.prototype._renew = function _renew() {
// // it is safer to check we are still leader
// this.isLeader(function(err, isLeader) {
// if(isLeader) {
// this.redis.pexpire(this.key, this.options.ttl, function(err) {
// if(err) {
// this.emit('error', err);
// }
// }.bind(this));
// } else {
// clearInterval(this.renewId);
// this.electId = setTimeout(Leader.prototype.elect.bind(this), this.options.wait);
// this.emit('revoked');
// }
// }.bind(this));
// };
// /**
// * Try to get elected as leader
// */
// Leader.prototype.elect = function elect() {
// // atomic redis set
// this.redis.set(this.key, this.id, 'PX', this.options.ttl, 'NX', function(err, res) {
// if(err) {
// return this.emit('error', err);
// }
// if(res !== null) {
// this.emit('elected');
// this.renewId = setInterval(Leader.prototype._renew.bind(this), this.options.ttl / 2);
// } else {
// // use setTimeout to avoid max call stack error
// this.electId = setTimeout(Leader.prototype.elect.bind(this), this.options.wait);
// }
// }.bind(this));
// };
// Leader.prototype.isLeader = function isLeader(done) {
// this.redis.get(this.key, function(err, id) {
// if(err) {
// return done(err);
// }
// done(null, (id === this.id));
// }.bind(this));
// };
// /**
// * if leader, stop being a leader
// * stop trying to be a leader
// */
// Leader.prototype.stop = function stop() {
// this.isLeader(function(err, isLeader) {
// if(isLeader) {
// // possible race condition, cause we need atomicity on get -> isEqual -> delete
// this.redis.del(this.key, function(err) {
// if(err) {
// return this.emit('error', err);
// }
// this.emit('revoked');
// }.bind(this));
// }
// clearInterval(this.renewId);
// clearTimeout(this.electId);
// }.bind(this));
// };
// module.exports = Leader;
module.exports.createSafeRedisLeader = createSafeRedisLeader