elector
Version:
simple zookeeper based leader election
195 lines (171 loc) • 4.9 kB
JavaScript
'use strict'
const debug = require('debug')('elector')
const zookeeper = require('node-zookeeper-client')
const async = require('async')
const _ = require('lodash')
const EventEmitter = require('events')
class Elector extends EventEmitter {
constructor (zkOptions, electionPath = '/election') {
super()
this.id = null
this.electionPath = electionPath
if (this._isZkClient(zkOptions)) {
this.client = zkOptions
} else {
this.client = zookeeper.createClient(zkOptions.host)
this.ownsClient = true
}
this.client.once('connected', () => this._onConnect())
}
connect () {
this.client.connect()
}
_isZkClient (client) {
return (
_.isObject(client) && 'connectionManager' in client && 'close' in client
)
}
_onConnect () {
debug('connected!')
async.waterfall(
[
callback => {
debug('creating election path "%s"', this.electionPath)
this.client.mkdirp(this.electionPath, callback)
},
(path, callback) => {
debug('created path: "%s"', path)
debug('creating EPHEMERAL_SEQUENTIAL node')
this.client.create(
`${this.electionPath}/p_`,
zookeeper.CreateMode.EPHEMERAL_SEQUENTIAL,
callback
)
},
(path, callback) => {
debug('newly created znode is "%s"', path)
this.id = _.last(path.split('/'))
debug('my candidateId is', this.id)
this.emit('candidateId', this.id)
this._listCandidates(callback)
},
(candidates, callback) =>
this._watchSiblingCandidateIf(candidates, callback)
],
(error, candidates) => {
if (error) {
return console.error(error)
}
debug('%s received candidates %j', this.id, candidates)
this._pickLeader(candidates)
}
)
}
_watchSiblingCandidateIf (candidates, callback) {
this.candidates = candidates.sort()
this.watchingCandidate = candidates[candidates.indexOf(this.id) - 1]
if (this.watchingCandidate) {
const watchPath = `${this.electionPath}/${this.watchingCandidate}`
debug(`${this.id} watching for changes to "${watchPath}"`)
this.client.exists(
watchPath,
event => this._onCandidateChange(event),
(error, stat) => {
if (error) {
return callback(error)
}
if (!stat) {
return callback(
new Error(
`${this.id} sibling node "${watchPath}" does not exist!`
)
)
}
callback(null, this.candidates)
}
)
return
}
debug('%s I got no sibling candidates to watch. Am I the leader?', this.id)
callback(null, this.candidates)
}
_pickLeader (candidates) {
const previousLeadershipState = this.isLeader
this.isLeader = _.first(candidates) === this.id
if (previousLeadershipState !== this.isLeader) {
if (this.isLeader) {
debug('%s I am the leader!', this.id)
this.emit('leader')
} else {
this.emit('follower')
debug('%s I am a follower', this.id)
}
} else {
debug('%s state is the same', this.id)
}
}
_onCandidateChange (event) {
if (this.disconnecting) {
debug(
'%s is disconnecting ignoring watch event from %s',
this.id,
this.watchingCandidate
)
return
}
debug('%s received change event %j', this.id, event)
this._listCandidates((error, candidates) => {
if (this.disconnecting) {
debug('%s in disconnecting state after _listCandidates', this.id)
return
}
if (error) {
debug(error)
this.emit('error', error)
return
}
debug('%s received new candidates %j', this.id, candidates)
this._watchSiblingCandidateIf(candidates, (error, candidates) => {
if (this.disconnecting) {
debug(
'%s in disconnecting state after _watchSiblingCandidateIf',
this.id
)
return
}
if (error) {
debug(error)
return this.emit(error)
}
this._pickLeader(candidates)
})
})
}
_listCandidates (callback) {
this.client.getChildren(this.electionPath, function (
error,
candidates,
stats
) {
if (error) {
return callback(error)
}
callback(null, candidates)
})
}
disconnect (callback) {
debug('%s disconnecting', this.id)
this.disconnecting = true
this.client.remove(`${this.electionPath}/${this.id}`, error => {
if (this.ownsClient) {
this.client.close()
}
if (error) {
debug(error)
return callback(error)
}
callback(null)
})
}
}
module.exports = Elector