level-2pc
Version:
A two-phase-commit protocol for leveldb.
573 lines (439 loc) • 14.6 kB
JavaScript
var level = require('level')
var net = require('net')
var after = require('after')
var Replicator = require('./index')
var rmrf = require('rimraf')
var test = require('tape')
function createOpts(localport, ports, min, failAfter) {
var peers = ports.map(function(port) {
return { host: 'localhost', port: port }
})
return {
peers: peers,
port: localport,
minConsensus: min,
failAfter: failAfter || 16,
host: 'localhost'
}
}
function createData(prefix, size, type) {
var map = function() {
if (type == 'json') {
return {
key: prefix + Math.random().toString(15).slice(-256),
value: { string: Math.random().toString(15) }
}
} else {
return {
key: prefix + Math.random().toString(15).slice(-256),
value: Math.random().toString(15)
}
}
}
var sort = function (a, b) {
if (a.key > b.key) {
return 1
}
if (a.key < b.key) {
return -1
}
return 0
}
return Array.apply(null, new Array(size)).map(map).sort(sort)
}
function run(t, dbOpts) {
var i = 16
var dbs = {}
var rs = {}
var servers = {}
while(i > 0) {
rmrf.sync('./test/db' + i)
dbs['db' + i] = level('./test/db' + i, dbOpts)
i--
}
t.on('end', function () {
for (var name in dbs) {
dbs[name].close()
}
})
//
// create three databases and start them.
//
t.test('setup', function(t) {
rs.r1 = Replicator(dbs.db1, createOpts(3001, [3002, 3003]))
rs.r2 = Replicator(dbs.db2, createOpts(3002, [3003, 3001]))
rs.r3 = Replicator(dbs.db3, createOpts(3003, [3001, 3002]))
servers.r1 = net.createServer(function(con) {
var server = rs.r1.createServer()
server.pipe(con).pipe(server)
}).listen(3001)
servers.r2 = net.createServer(function(con) {
var server = rs.r2.createServer()
server.pipe(con).pipe(server)
}).listen(3002)
servers.r3 = net.createServer(function(con) {
var server = rs.r3.createServer()
server.pipe(con).pipe(server)
}).listen(3003)
//
// peers should be ready for replication right away because
// writes that are made before all connections are established
// are queued.
//
t.end()
})
t.test('that a random number of records put to one peer are replicated to all other peers', function(t) {
var size = 2
var records = createData('A_', size)
var done = after(records.length, function (err) {
t.ifError(err)
var databases = [dbs.db1, dbs.db2, dbs.db3]
var done2 = after(databases.length, t.end.bind(t))
databases.forEach(function (db) {
verifyRecords(db, done2)
})
})
records.forEach(function(record) {
dbs.db1.put(record.key, record.value, { valueEncoding: 'utf8' }, done)
})
function verifyRecords(db, cb) {
var results = []
var count = 0
db.createReadStream({ gte: 'A_', lte: 'A_~' })
.on('data', function(r) {
++count
r.key = r.key.toString('binary')
results.push(r)
})
.on('end', function() {
t.equal(count, records.length, 'equal #1')
t.equal(
JSON.stringify(results), JSON.stringify(records), 'equal #2'
)
cb()
})
}
})
t.test('that records batched to one peer are replicated to all other peers', function(t) {
function chunk(arr, n) {
return !arr.length ? [] : [arr.slice(0, n)].concat(chunk(arr.slice(n), n))
}
var size = 10
var records = createData('B_', size)
var groups = chunk(records, 2)
var done = after(groups.length, function (err) {
t.ifError(err)
var databases = [dbs.db2, dbs.db3]
var done2 = after(databases.length, t.end.bind(t))
databases.forEach(function (db) {
verifyRecords(db, done2)
})
})
groups.forEach(function(group) {
var newgroup = []
group.forEach(function(op) {
newgroup.push({ type: 'put', key: op.key, value: op.value })
})
dbs.db1.batch(newgroup, done)
})
function verifyRecords(db, cb) {
var results = []
db.createReadStream({ gte: 'B_', lte: 'B_~' })
.on('data', function(r) {
r.key = r.key.toString('binary')
results.push(r)
})
.on('end', function() {
t.equal(
JSON.stringify(results), JSON.stringify(records), 'equal #3'
)
cb()
})
}
})
t.test('that a single record is added to all peers before returning the callback', function(t) {
dbs.db1.put('test1key', 'test1value', function(err) {
t.ifError(err, 'key added to the coordinator and all other peers')
dbs.db2.get('test1key', function(err) {
t.ifError(err, 'key was found in dbs.db2')
dbs.db3.get('test1key', function(err) {
t.ifError(err, 'key was found in dbs.db3')
t.end()
})
})
})
})
t.test('that a record can be deleted from one database and it is removed from all others', function(t) {
dbs.db1.del('test1key', function(err) {
t.ifError(err, 'key added to the coordinator and all other peers')
dbs.db2.get('test1key', function(err) {
t.ok(err, 'key was not found in dbs.db2')
dbs.db3.get('test1key', function(err) {
t.ok(err, 'key was not found in dbs.db3')
t.end()
})
})
})
})
t.test('writes should not be made if the minimum number of required peers for concensus can not be reached', function(t) {
//
// only retry 4 times in the interests of keeping the test run-time short
//
rs.r4 = Replicator(dbs.db4, createOpts(3004, [3005, 3006], 2, 2))
rs.r5 = Replicator(dbs.db5, createOpts(3005, [3004, 3004], 2, 2))
rs.r4.on('error', function(err) {})
rs.r5.on('error', function(err) {})
servers.r4 = net.createServer(function(con) {
var server = rs.r4.createServer()
server.pipe(con).pipe(server)
}).listen(3004)
servers.r5 = net.createServer(function(con) {
var server = rs.r5.createServer()
server.pipe(con).pipe(server)
}).listen(3005)
var failures = 0
function done() {
if (++failures == 2) {
dbs.db4.get('test1key', function(err) {
t.ok(err, 'test1key should not be in dbs.db4')
dbs.db5.get('test1key', function(err) {
t.ok(err, 'test1key should not be in dbs.db5')
t.end()
})
})
}
}
rs.r4.on('fail', done)
rs.r5.on('fail', done)
dbs.db4.put('test1key', 'test1value', function(err) {
t.ok(err)
})
dbs.db5.put('test1key', 'test1value', function(err) {
t.ok(err)
})
})
t.test('joining peers in random orders at different times', function(t) {
var base = 1000
setTimeout(function() {
rs.r6 = Replicator(dbs.db6, createOpts(3006, [3007, 3008], 2))
rs.r6.on('error', function() {})
servers.r6 = net.createServer(function(con) {
var server = rs.r6.createServer()
server.pipe(con).pipe(server)
}).listen(3006)
}, Math.ceil(Math.random()*base))
setTimeout(function() {
rs.r7 = Replicator(dbs.db7, createOpts(3007, [3006, 3008], 2))
rs.r7.on('error', function() {})
servers.r7 = net.createServer(function(con) {
var server = rs.r7.createServer()
server.pipe(con).pipe(server)
}).listen(3007)
}, Math.ceil(Math.random()*base))
setTimeout(function() {
rs.r8 = Replicator(dbs.db8, createOpts(3008, [3006, 3007], 2))
rs.r8.on('error', function() {})
servers.r8 = net.createServer(function(con) {
var server = rs.r8.createServer()
server.pipe(con).pipe(server)
}).listen(3008)
dbs.db8.put('test1key', 'test1value', function(err) {
t.ifError(err)
//
// after it's been put into dbs.db8 it should be everywhere
//
dbs.db6.get('test1key', function(err) {
t.ifError(err)
dbs.db7.get('test1key', function(err) {
t.ifError(err)
dbs.db8.get('test1key', function(err) {
t.ifError(err)
t.end()
})
})
})
})
}, Math.ceil(Math.random()*base))
})
t.test('peer discovery -- peer joins later on', function(t) {
rs.r9 = Replicator(dbs.db9, createOpts(3009, [3010], 1))
rs.r9.on('error', function() {})
servers.r9 = net.createServer(function(con) {
var server = rs.r9.createServer()
server.pipe(con).pipe(server)
}).listen(3009)
rs.r10 = Replicator(dbs.db10, createOpts(3010, [3009], 1))
rs.r10.on('error', function() {})
servers.r10 = net.createServer(function(con) {
var server = rs.r10.createServer()
server.pipe(con).pipe(server)
}).listen(3010)
dbs.db9.put('test1key', 'test1value', function(err) {
t.ifError(err)
dbs.db10.get('test1key', function(err) {
t.ifError(err);
})
})
setTimeout(function() {
rs.r11 = Replicator(dbs.db11, createOpts(3011, [3009, 3010], 1))
servers.r11 = net.createServer(function(con) {
var server = rs.r11.createServer()
server.on('error', function() {})
server.pipe(con).pipe(server)
}).listen(3011)
dbs.db11.put('test2key', 'test2value', function(err) {
t.ifError(err)
//
// after it's been put into dbs.db8 it should be everywhere
//
dbs.db9.get('test2key', function(err) {
t.ifError(err)
dbs.db10.get('test2key', function(err) {
t.ifError(err)
t.end();
})
})
})
}, Math.ceil(Math.random()*1000))
})
t.test('random set of json encoded records pass', function(t) {
var size = 2
var records = createData('C_', size, 'json')
var done = after(records.length, function (err) {
t.ifError(err)
var databases = [dbs.db1, dbs.db2, dbs.db3]
var done2 = after(databases.length, t.end.bind(t))
databases.forEach(function (db) {
verifyRecords(db, done2)
})
})
records.forEach(function(record) {
dbs.db1.put(record.key, record.value, { valueEncoding: 'json' }, done)
})
function verifyRecords(db, cb) {
var results = []
var count = 0
db.createReadStream({ gte: 'C_', lte: 'C_~', valueEncoding: 'json' })
.on('data', function(r) {
++count
r.key = r.key.toString('binary')
results.push(r)
})
.on('end', function() {
t.equal(count, records.length, 'equal #4')
t.equal(
JSON.stringify(results), JSON.stringify(records), 'equal #5'
)
cb()
})
}
})
t.test('closed peers should not try to reconnect if missing failAfter', function(t) {
var opts12 = createOpts(3012, [3013], 1)
delete opts12.failAfter
var r12 = Replicator(dbs.db12, opts12)
r12.on('error', function() {})
var opts13 = createOpts(3013, [3012], 1)
delete opts13.failAfter
var r13 = Replicator(dbs.db13, opts13)
r13.on('error', function() {})
var s12 = net.createServer(function(con) {
var server = r12.createServer()
server.on('error', function() {})
server.pipe(con).pipe(server)
}).listen(3012)
var s13 = net.createServer(function(con) {
var server = r13.createServer()
server.on('error', function() {})
server.pipe(con).pipe(server)
}).listen(3013)
var ready = after(2, thirdPeer)
function thirdPeer(err) {
t.ifError(err)
var opts14 = createOpts(3014, [3012, 3013], 2)
delete opts14.failAfter
var r14 = Replicator(dbs.db14, opts14)
r14.on('error', function() {})
var s14 = net.createServer(function(con) {
var server = r14.createServer()
server.on('error', function() {})
server.pipe(con).pipe(server)
}).listen(3014)
var reconnectAttempts = 0
function attemptedReconnect() { ++reconnectAttempts }
r12.on('reconnect', attemptedReconnect)
r13.on('reconnect', attemptedReconnect)
r14.on('reconnect', attemptedReconnect)
r14.once('ready', function () {
var notready = after(3, function (err) {
t.ifError(err, 'no error')
setTimeout(function () {
t.equal(reconnectAttempts, 0, 'no attempts to reconnect')
t.end()
}, 500)
})
r12.once('notready', notready)
r13.once('notready', notready)
r14.once('notready', notready)
s12.close()
s13.close()
s14.close()
r12.close()
r13.close()
r14.close()
})
}
r12.once('ready', ready)
r13.once('ready', ready)
})
t.test('peer with minConsensus 0 should always be in ready state', function(t) {
var peer1, peer2
var servers = []
var ready = after(2, function() {
peer1.close()
peer2.close()
servers.forEach(function(server) { server.close() })
})
peer1 = Replicator(dbs.db15, createOpts(3015, [3016], 0))
peer1.on('error', function() {})
servers.push(net.createServer(function(con) {
var server = peer1.createServer()
server.pipe(con).pipe(server)
}).listen(3015))
peer1.on('ready', ready)
peer1.on('notready', function () {
t.fail('should never happen')
})
peer2 = Replicator(dbs.db16, createOpts(3016, [3015], 1))
peer2.on('error', function() {})
servers.push(net.createServer(function(con) {
var server = peer2.createServer()
server.pipe(con).pipe(server)
}).listen(3016))
peer2.on('ready', ready)
peer2.on('notready', function() {
t.ok(true, 'only one notready called')
t.end()
})
})
t.test('when the databases closes, the replicator disconnects from its peers', function(t) {
var done = after(Object.keys(servers).length, function (err) {
t.ifError(err, 'no error')
t.end()
})
for (var r in rs)
rs[r].close()
for(var s in servers)
servers[s].close(done)
})
t.end()
}
test('keyEncoding: utf8', function (t) {
run(t, { keyEncoding: 'utf8', valueEncoding: 'utf8' })
})
test('keyEncoding: binary', function (t) {
run(t, { keyEncoding: 'binary', valueEncoding: 'utf8' })
})
test('keyEncoding: bytewise', function (t) {
run(t, { keyEncoding: require('bytewise-core'), valueEncoding: 'utf8' })
})