level-2pc
Version:
A two-phase-commit protocol for leveldb.
365 lines (272 loc) • 7.94 kB
JavaScript
var rpc = require('rpc-stream')
var createClient = require('./client')
var xtend = require('xtend')
var debug = require('debug')('level2pc')
var inherits = require('inherits')
var Emitter = require('events').EventEmitter
var Codec = require('level-codec')
var prefix = '\xffxxl\xff'
function Replicator(db, repl_opts) {
if (!(this instanceof Replicator)) return new Replicator(db, repl_opts)
Emitter.call(this)
this._isReady = false
var that = this
var id = repl_opts.host + ':' + repl_opts.port
var peers = {}
var connections = {}
var _db = {}
_db.del = db.del.bind(db)
_db.put = db.put.bind(db)
_db.batch = db.batch.bind(db)
_db.close = db.close.bind(db)
// encode keys and values before sending pushing them through the rpc stream
var codec = new Codec(db.options)
var DEL = {}
function encodeOp(key, value, opts) {
var op = {}
op.key = codec.encodeKey(key, opts)
op.keyEncoding = codec.keyAsBuffer(opts) ? 'binary' : 'utf8'
op.type = value === DEL ? 'del' : 'put'
if (op.type == 'put') {
op.value = codec.encodeValue(value, opts)
op.valueEncoding = codec.valueAsBuffer(opts) ? 'binary' : 'utf8'
}
return op
}
function encodeOps(arr, opts) {
// force utf8 or binary value encoding
return codec.encodeBatch(arr, opts).map(function (op) {
if (op.keyEncoding != 'binary') {
op.keyEncoding = 'utf8'
}
if ('value' in op && op.valueEncoding != 'binary') {
op.valueEncoding = 'utf8'
}
return op
})
}
function encodePrefix(op) {
if (typeof op.key === 'string') return prefix + op
return new Buffer(prefix + op.key.toString('binary'), 'binary')
}
function prefixRemoveOp(op) {
return {
type: 'del',
key: encodePrefix(op),
keyEncoding: op.keyEncoding
}
}
db.addPeer = function addPeer(peer) {
connect(peer)
}
db.quorum = function quorum(cb) {
debug('QUORUM @%d [%j]', repl_opts.port)
//
// the purpose of the quorum step is to determine if the
// database is capable of reposonding successfully to operations.
//
var testkey = prefix + prefix + 'q'
_db.put(testkey, 1, cb)
}
db.commit = function commit(op, cb) {
var arr
var opts
if (op.type == 'batch') {
opts = op.opts
arr = op.value
arr.forEach(function(o) {
arr.push(prefixRemoveOp(o))
})
}
else {
opts = {}
arr = [ prefixRemoveOp(op), op ]
}
debug('COMMIT @%d [%j]', repl_opts.port, arr)
_db.batch(arr, opts, cb)
}
db.del = function del(key, opts, cb) {
// TODO: is this really necessary?
if (key.toString('binary').indexOf(prefix) == 0)
return _db.del.apply(_db, arguments)
if ('function' == typeof opts) {
cb = opts
opts = {}
}
var op
queue(function() {
try {
op = encodeOp(key, DEL, opts)
}
catch (err) {
return cb(err)
}
var _opts = { keyEncoding: op.keyEncoding }
_db.del(encodePrefix(op), _opts, function(err) {
if (err) return cb(err)
replicate(op, cb)
})
})
}
db.put = function put(key, value, opts, cb) {
if ('function' == typeof opts) {
cb = opts
opts = {}
}
var op
queue(function() {
try {
op = encodeOp(key, value, opts)
}
catch (err) {
return cb(err)
}
var _opts = { keyEncoding: op.keyEncoding }
_db.put(encodePrefix(op), op.value, _opts, function (err) {
if (err) return cb(err)
replicate(op, cb)
})
})
}
db.batch = function batch(arr, opts, cb) {
// TODO: is this really necessary?
if (arr[0] && arr[0].key.toString('binary').indexOf(prefix) == 0)
return _db.batch.apply(_db, arguments)
if ('function' == typeof opts) {
cb = opts
opts = {}
}
var ops
var prefixes
queue(function() {
try {
ops = encodeOps(arr, opts)
prefixed = ops.map(function (op) {
return xtend(op, { key: encodePrefix(op) })
})
}
catch (err) {
return cb(err)
}
_db.batch(prefixed, opts, function (err) {
replicate({ type: 'batch', value: ops, opts: opts }, cb)
})
})
}
function quorumPhase(op, len, cb) {
var index = 0
var failures = 0
for (var peer in peers) {
peers[peer].quorum(function quorumCallback(err) {
if (err && err.message == 'Database is not open') {
debug('FAILURE EVENT @%s', peer)
if (repl_opts.minConsensus && ++failures == repl_opts.minConsensus) {
return cb(new Error('minimum consensus failed'))
}
}
else if (err) {
return cb(err)
}
if (++index == len) {
commitPhase(op, len, cb)
}
})
}
}
function commitPhase(op, len, cb) {
var index = 0
for (var peer in peers) {
peers[peer].commit(op, function quorumCallback(err) {
if (err && err.message != 'Database is not open') {
return cb(err)
}
if (++index == len) {
cb(null)
}
})
}
}
function queue(fn) {
if (that._isReady) process.nextTick(fn)
else that.once('ready', fn)
}
function replicate(op, cb) {
debug('REPLICATION EVENT @%s', id)
var len = repl_opts.minConsensus || Object.keys(peers).length
if (!len || len == 0) return db.commit(op, cb)
quorumPhase(op, len, function quorumPhaseCallback(err) {
if (err) return cb(err)
db.commit(op, cb)
})
}
function ready(isReady) {
if (isReady) debug('READY EVENT %s', id)
else debug('NOT READY EVENT %s', id)
that._isReady = isReady
that.emit(isReady ? 'ready' : 'notready')
}
function connect(peer) {
var peername = peer.host + ':' + peer.port
if (connections === null || typeof connections[peername] != 'undefined')
return
var min = repl_opts.minConsensus || repl_opts.peers.length
var client = createClient(repl_opts)
var failures = 0
client.connect(peer.port, peer.host)
connections[peername] = client
client.on('fail', function() {
that.emit('fail', peer.host, peer.port)
})
client.on('error', function(err) {
that.emit('error', err)
if (failures++ == repl_opts.failAfter) {
client.emit('fail')
}
})
client.on('reconnect', function() {
debug('RECONNECT EVENT %s -> %s', id, peername)
that.emit('reconnect', peer.host, peer.port)
})
client.on('connect', function onConnect(con) {
debug('CONNECT EVENT %s -> %s', id, peername)
var client = rpc()
var remote = client.wrap(db)
client.pipe(con).pipe(client)
peers[peername] = remote
Object.keys(peers).forEach(function(p) {
peers[p].addPeer({host: repl_opts.host, port: repl_opts.port})
})
that.emit('connect', peer.host, peer.port)
if (!that._isReady && Object.keys(peers).length >= min) {
ready(true)
}
})
client.on('disconnect', function onDisconnect() {
debug('DISCONNECT EVENT %s -> %s', id, peername)
that.emit('disconnect', peer.host, peer.port)
delete peers[peername]
if (repl_opts.minConsensus > 0
&& that._isReady
&& Object.keys(peers).length < min) {
ready(false)
}
})
}
if (repl_opts.minConsensus == 0) {
process.nextTick(ready.bind(null, true))
}
repl_opts.peers.forEach(connect)
this.close = function closeServer() {
var _connections = connections
connections = null
for (var client in _connections) {
_connections[client].disconnect()
}
peers = {}
}
this.createServer = function createServer() {
return rpc(db)
}
}
inherits(Replicator, Emitter)
module.exports = Replicator