DB Service: Include a version number for upgrading purposes

This commit is contained in:
Braydon Fuller 2016-01-28 11:14:04 -05:00
parent b0a0f629e2
commit 98bd8ee560
2 changed files with 199 additions and 5 deletions

View File

@ -38,6 +38,10 @@ function DB(options) {
Service.call(this, options);
// Used to keep track of the version of the indexes
// to determine during an upgrade if a reindex is required
this.version = 2;
this.tip = null;
this.genesis = null;
@ -66,6 +70,7 @@ util.inherits(DB, Service);
DB.dependencies = ['bitcoind'];
DB.PREFIXES = {
VERSION: new Buffer('ff', 'hex'),
BLOCKS: new Buffer('01', 'hex'),
TIP: new Buffer('04', 'hex')
};
@ -90,6 +95,48 @@ DB.prototype._setDataPath = function() {
}
};
DB.prototype._checkVersion = function(callback) {
var self = this;
var options = {
keyEncoding: 'binary',
valueEncoding: 'binary'
};
self.store.get(DB.PREFIXES.TIP, options, function(err) {
if (err instanceof levelup.errors.NotFoundError) {
// The database is brand new and doesn't have a tip stored
// we can skip version checking
return callback();
} else if (err) {
return callback(err);
}
self.store.get(DB.PREFIXES.VERSION, options, function(err, buffer) {
var version;
if (err instanceof levelup.errors.NotFoundError) {
// The initial version (1) of the database didn't store the version number
version = 1;
} else if (err) {
return callback(err);
} else {
version = buffer.readUInt32BE();
}
if (self.version !== version) {
return callback(new Error(
'The version of the database "' + version + '" does not match the expected version "' +
self.version + '". A reindex (can take several hours) is required or to switch ' +
'versions of software to match.'
));
}
callback();
});
});
};
DB.prototype._setVersion = function(callback) {
var versionBuffer = new Buffer(new Array(4));
versionBuffer.writeUInt32BE(this.version);
this.store.put(DB.PREFIXES.VERSION, versionBuffer, callback);
};
/**
* Called by Node to start the service.
* @param {Function} callback
@ -116,14 +163,26 @@ DB.prototype.start = function(callback) {
});
});
self.loadTip(function(err) {
if(err) {
async.series([
function(next) {
self._checkVersion(next);
},
function(next) {
self._setVersion(next);
}
], function(err) {
if (err) {
return callback(err);
}
self.loadTip(function(err) {
if (err) {
return callback(err);
}
self.sync();
self.emit('ready');
setImmediate(callback);
self.sync();
self.emit('ready');
setImmediate(callback);
});
});
};

View File

@ -104,6 +104,135 @@ describe('DB Service', function() {
});
});
describe('#_checkVersion', function() {
var config = {
node: {
network: Networks.get('testnet'),
datadir: 'testdir'
},
store: memdown
};
it('will handle an error while retrieving the tip', function() {
var db = new DB(config);
db.store = {};
db.store.get = sinon.stub().callsArgWith(2, new Error('test'));
db._checkVersion(function(err) {
should.exist(err);
err.message.should.equal('test');
});
});
it('will handle an error while retrieving the version', function() {
var db = new DB(config);
db.store = {};
db.store.get = function() {};
var callCount = 0;
sinon.stub(db.store, 'get', function(key, options, callback) {
if (callCount === 1) {
return callback(new Error('test'));
}
callCount++;
setImmediate(callback);
});
db._checkVersion(function(err) {
should.exist(err);
err.message.should.equal('test');
});
});
it('will NOT check the version if a tip is not found', function(done) {
var db = new DB(config);
db.store = {};
db.store.get = sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError());
db._checkVersion(done);
});
it('will NOT give an error if the versions match', function(done) {
var db = new DB(config);
db.store = {};
db.store.get = function() {};
var callCount = 0;
sinon.stub(db.store, 'get', function(key, options, callback) {
if (callCount === 1) {
var versionBuffer = new Buffer(new Array(4));
versionBuffer.writeUInt32BE(2);
return callback(null, versionBuffer);
}
callCount++;
setImmediate(callback);
});
db.version = 2;
db._checkVersion(done);
});
it('will give an error if the versions do NOT match', function(done) {
var db = new DB(config);
db.store = {};
db.store.get = function() {};
var callCount = 0;
sinon.stub(db.store, 'get', function(key, options, callback) {
if (callCount === 1) {
var versionBuffer = new Buffer(new Array(4));
versionBuffer.writeUInt32BE(2);
return callback(null, versionBuffer);
}
callCount++;
setImmediate(callback);
});
db.version = 3;
db._checkVersion(function(err) {
should.exist(err);
err.message.should.match(/^The version of the database/);
done();
});
});
it('will default to version 1 if the version is NOT found', function(done) {
var db = new DB(config);
db.store = {};
db.store.get = function() {};
var callCount = 0;
sinon.stub(db.store, 'get', function(key, options, callback) {
if (callCount === 1) {
return callback(new levelup.errors.NotFoundError());
}
callCount++;
setImmediate(callback);
});
db.version = 1;
db._checkVersion(done);
});
});
describe('#_setVersion', function() {
var config = {
node: {
network: Networks.get('testnet'),
datadir: 'testdir'
},
store: memdown
};
it('will give an error from the store', function(done) {
var db = new DB(config);
db.store = {};
db.store.put = sinon.stub().callsArgWith(2, new Error('test'));
db._setVersion(function(err) {
should.exist(err);
err.message.should.equal('test');
done();
});
});
it('will set the version', function(done) {
var db = new DB(config);
db.store = {};
db.store.put = sinon.stub().callsArgWith(2, null);
db.version = 5;
db._setVersion(function(err) {
if (err) {
return done(err);
}
db.store.put.args[0][0].should.deep.equal(new Buffer('ff', 'hex'));
db.store.put.args[0][1].should.deep.equal(new Buffer('00000005', 'hex'));
done();
});
});
});
describe('#start', function() {
var TestDB;
@ -126,6 +255,8 @@ describe('DB Service', function() {
};
db.loadTip = sinon.stub().callsArg(0);
db.connectBlock = sinon.stub().callsArg(1);
db._checkVersion = sinon.stub().callsArg(0);
db._setVersion = sinon.stub().callsArg(0);
db.sync = sinon.stub();
var readyFired = false;
db.on('ready', function() {
@ -144,6 +275,8 @@ describe('DB Service', function() {
db.node.services.bitcoind.genesisBuffer = genesisBuffer;
db.loadTip = sinon.stub().callsArg(0);
db.connectBlock = sinon.stub().callsArg(1);
db._checkVersion = sinon.stub().callsArg(0);
db._setVersion = sinon.stub().callsArg(0);
db.sync = sinon.stub();
db.start(function() {
db.sync = function() {
@ -161,6 +294,8 @@ describe('DB Service', function() {
db.node.services.bitcoind.genesisBuffer = genesisBuffer;
db.loadTip = sinon.stub().callsArg(0);
db.connectBlock = sinon.stub().callsArg(1);
db._checkVersion = sinon.stub().callsArg(0);
db._setVersion = sinon.stub().callsArg(0);
db.node.stopping = true;
db.sync = sinon.stub();
db.start(function() {