Move Modules from DB to Node

This commit is contained in:
Braydon Fuller 2015-08-27 16:09:27 -04:00
parent aa6b03ae58
commit 56ebf42403
11 changed files with 309 additions and 170 deletions

View File

@ -364,9 +364,7 @@ The module can then be used when running a node:
```js
var configuration = {
datadir: process.env.BITCORENODE_DIR || '~/.bitcoin',
db: {
modules: [MyModule]
}
modules: [MyModule]
};
var node = new Node(configuration);

View File

@ -12,8 +12,6 @@ var index = require('./');
var errors = index.errors;
var log = index.log;
var Transaction = require('./transaction');
var BaseModule = require('./module');
var AddressModule = require('./modules/address');
function DB(options) {
/* jshint maxstatements: 30 */
@ -52,12 +50,6 @@ function DB(options) {
this.node = options.node;
// Modules to be loaded when ready
this._modules = options.modules || [];
this._modules.push(AddressModule);
this.modules = [];
this.subscriptions = {
transaction: [],
block: []
@ -79,12 +71,6 @@ DB.prototype.initialize = function() {
};
DB.prototype.start = function(callback) {
// Add all db option modules
if(this._modules && this._modules.length) {
for(var i = 0; i < this._modules.length; i++) {
this.addModule(this._modules[i]);
}
}
this.node.bitcoind.on('tx', this.transactionHandler.bind(this));
this.emit('ready');
setImmediate(callback);
@ -278,9 +264,9 @@ DB.prototype.blockHandler = function(block, add, callback) {
}
async.eachSeries(
this.modules,
function(module, next) {
module['blockHandler'].call(module, block, add, function(err, ops) {
this.node.modules,
function(bitcoreNodeModule, next) {
bitcoreNodeModule.blockHandler.call(bitcoreNodeModule, block, add, function(err, ops) {
if(err) {
return next(err);
}
@ -308,11 +294,6 @@ DB.prototype.getAPIMethods = function() {
['sendTransaction', this, this.sendTransaction, 1],
['estimateFee', this, this.estimateFee, 1]
];
for(var i = 0; i < this.modules.length; i++) {
methods = methods.concat(this.modules[i]['getAPIMethods'].call(this.modules[i]));
}
return methods;
};
@ -333,14 +314,6 @@ DB.prototype.getPublishEvents = function() {
];
};
DB.prototype.addModule = function(Module) {
var module = new Module({
node: this.node
});
$.checkArgumentType(module, BaseModule);
this.modules.push(module);
};
DB.prototype.subscribe = function(name, emitter) {
this.subscriptions[name].push(emitter);
};

View File

@ -4,6 +4,11 @@ var Module = function(options) {
this.node = options.node;
};
/**
* Describes the dependencies that should be loaded before this module.
*/
Module.dependencies = [];
/**
* blockHandler
* @param {Block} block - the block being added or removed from the chain
@ -45,12 +50,12 @@ Module.prototype.getAPIMethods = function() {
//
// };
Module.prototype.start = function() {
Module.prototype.start = function(done) {
setImmediate(done);
};
Module.prototype.stop = function() {
Module.prototype.stop = function(done) {
setImmediate(done);
};
module.exports = Module;

View File

@ -27,6 +27,11 @@ var AddressModule = function(options) {
inherits(AddressModule, BaseModule);
AddressModule.dependencies = [
'bitcoind',
'db'
];
AddressModule.PREFIXES = {
OUTPUTS: 'outs',
SPENTS: 'sp'

View File

@ -17,6 +17,7 @@ var index = require('./');
var log = index.log;
var daemon = require('./daemon');
var Bus = require('./bus');
var BaseModule = require('./module');
function Node(config) {
if(!(this instanceof Node)) {
@ -27,10 +28,17 @@ function Node(config) {
this.chain = null;
this.network = null;
this.modules = {};
this._unloadedModules = [];
// TODO type check the arguments of config.modules
if (config.modules) {
$.checkArgument(Array.isArray(config.modules));
this._unloadedModules = config.modules;
}
this._loadConfiguration(config);
this._initialize();
this.testnet = config.testnet;
}
util.inherits(Node, EventEmitter);
@ -39,10 +47,41 @@ Node.prototype.openBus = function() {
return new Bus({db: this.db});
};
Node.prototype.addModule = function(service) {
var self = this;
var mod = new service.module({
node: this
});
$.checkState(
mod instanceof BaseModule,
'Unexpected module instance type for module:' + service.name
);
// include in loaded modules
this.modules[service.name] = mod;
// add API methods
var methodData = mod.getAPIMethods();
methodData.forEach(function(data) {
var name = data[0];
var instance = data[1];
var method = data[2];
if (self[name]) {
throw new Error('Existing API method exists:' + name);
} else {
self[name] = function() {
return method.apply(instance, arguments);
};
}
});
};
Node.prototype.getAllAPIMethods = function() {
var methods = this.db.getAPIMethods();
for (var i = 0; i < this.db.modules.length; i++) {
var mod = this.db.modules[i];
for(var i in this.modules) {
var mod = this.modules[i];
methods = methods.concat(mod.getAPIMethods());
}
return methods;
@ -50,8 +89,8 @@ Node.prototype.getAllAPIMethods = function() {
Node.prototype.getAllPublishEvents = function() {
var events = this.db.getPublishEvents();
for (var i = 0; i < this.db.modules.length; i++) {
var mod = this.db.modules[i];
for (var i in this.modules) {
var mod = this.modules[i];
events = events.concat(mod.getPublishEvents());
}
return events;
@ -379,7 +418,6 @@ Node.prototype._loadConsensus = function(config) {
Node.prototype._loadAPI = function() {
var self = this;
var methodData = self.db.getAPIMethods();
methodData.forEach(function(data) {
var name = data[0];
@ -456,32 +494,62 @@ Node.prototype._initializeChain = function() {
};
Node.prototype.getServices = function() {
var defaultServices = {
'bitcoind': [],
'db': ['bitcoind'],
'chain': ['db']
};
return defaultServices;
var services = [
{
name: 'bitcoind',
dependencies: []
},
{
name: 'db',
dependencies: ['bitcoind'],
},
{
name: 'chain',
dependencies: ['db']
}
];
services = services.concat(this._unloadedModules);
return services;
};
Node.prototype.getServiceOrder = function(keys, stack) {
Node.prototype.getServiceOrder = function() {
var services = this.getServices();
if(!keys) {
keys = Object.keys(services);
// organize data for sorting
var names = [];
var servicesByName = {};
for (var i = 0; i < services.length; i++) {
var service = services[i];
names.push(service.name);
servicesByName[service.name] = service;
}
if(!stack) {
stack = [];
}
var stackNames = {};
var stack = [];
function addToStack(names) {
for(var i = 0; i < names.length; i++) {
var name = names[i];
var service = servicesByName[name];
// first add the dependencies
addToStack(service.dependencies);
// add to the stack if it hasn't been added
if(!stackNames[name]) {
stack.push(service);
stackNames[name] = true;
}
for(var i = 0; i < keys.length; i++) {
this.getServiceOrder(services[keys[i]], stack);
if(stack.indexOf(keys[i]) === -1) {
stack.push(keys[i]);
}
}
addToStack(names);
return stack;
};
@ -492,8 +560,15 @@ Node.prototype.start = function(callback) {
async.eachSeries(
servicesOrder,
function(service, next) {
log.info('Starting ' + service);
self[service].start(next);
log.info('Starting ' + service.name);
if (service.module) {
self.addModule(service);
self.modules[service.name].start(next);
} else {
// TODO: implement bitcoind, chain and db as modules
self[service.name].start(next);
}
},
callback
);
@ -510,12 +585,16 @@ Node.prototype.stop = function(callback) {
async.eachSeries(
services,
function(service, next) {
log.info('Stopping ' + service);
self[service].stop(next);
log.info('Stopping ' + service.name);
if (service.module) {
self.modules[service.name].stop(next);
} else {
self[service.name].stop(next);
}
},
callback
);
};
module.exports = Node;

View File

@ -12,7 +12,8 @@ function getDefaultConfig() {
config: {
datadir: process.env.BITCORENODE_DIR || path.resolve(process.env.HOME, '.bitcoin'),
network: process.env.BITCORENODE_NETWORK || 'livenet',
port: process.env.BITCORENODE_PORT || 3001
port: process.env.BITCORENODE_PORT || 3001,
modules: ['address']
}
};
}

View File

@ -36,18 +36,23 @@ function start(options) {
bitcoreNodeModule = moduleName + '/' + modulePackage.bitcoreNode;
}
bitcoreModule = require(bitcoreNodeModule);
}
// check that the module supports expected methods
if (!bitcoreModule.prototype ||
!bitcoreModule.dependencies ||
!bitcoreModule.prototype.start ||
!bitcoreModule.prototype.stop) {
throw new Error(
'Could not load module "' + moduleName + '" as it does not support necessary methods.'
);
}
bitcoreModules.push(bitcoreModule);
bitcoreModules.push({
name: moduleName,
module: bitcoreModule,
dependencies: bitcoreModule.dependencies
});
}
}
@ -56,13 +61,8 @@ function start(options) {
// expand to the full path
fullConfig.datadir = path.resolve(configPath, config.datadir);
// delete until modules move to the node
delete fullConfig.modules;
// load the modules
fullConfig.db = {
modules: bitcoreModules
};
fullConfig.modules = bitcoreModules;
var node = new BitcoreNode(fullConfig);

View File

@ -249,26 +249,16 @@ describe('Bitcoin Chain', function() {
chain.tip = block2;
chain.on('ready', function() {
delete chain.cache.hashes[block1.hash];
// remove one of the cached hashes to force db call
delete chain.cache.hashes[block1.hash];
// the test
chain.getHashes(block2.hash, function(err, hashes) {
should.not.exist(err);
should.exist(hashes);
hashes.length.should.equal(3);
done();
});
});
chain.on('error', function(err) {
// the test
chain.getHashes(block2.hash, function(err, hashes) {
should.not.exist(err);
should.exist(hashes);
hashes.length.should.equal(3);
done();
});
chain.initialize();
});
});

View File

@ -16,7 +16,6 @@ var bitcore = require('bitcore');
var Transaction = bitcore.Transaction;
describe('Bitcoin DB', function() {
var coinbaseAmount = 50 * 1e8;
describe('#start', function() {
it('should emit ready', function(done) {
@ -336,7 +335,8 @@ describe('Bitcoin DB', function() {
Module1.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op1', 'op2', 'op3']);
var Module2 = function() {};
Module2.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op4', 'op5']);
db.modules = [
db.node = {};
db.node.modules = [
new Module1(),
new Module2()
];
@ -355,7 +355,7 @@ describe('Bitcoin DB', function() {
it('should give an error if one of the modules gives an error', function(done) {
var Module3 = function() {};
Module3.prototype.blockHandler = sinon.stub().callsArgWith(2, new Error('error'));
db.modules.push(new Module3());
db.node.modules.push(new Module3());
db.blockHandler('block', true, function(err) {
should.exist(err);
@ -367,62 +367,11 @@ describe('Bitcoin DB', function() {
describe('#getAPIMethods', function() {
it('should return the correct db methods', function() {
var db = new DB({store: memdown});
db.modules = [];
db.node = {};
db.node.modules = [];
var methods = db.getAPIMethods();
methods.length.should.equal(4);
});
it('should also return modules API methods', function() {
var module1 = {
getAPIMethods: function() {
return [
['module1-one', module1, module1, 2],
['module1-two', module1, module1, 2]
];
}
};
var module2 = {
getAPIMethods: function() {
return [
['moudle2-one', module2, module2, 1]
];
}
};
var db = new DB({store: memdown});
db.modules = [module1, module2];
var methods = db.getAPIMethods();
methods.length.should.equal(7);
});
});
describe('#addModule', function() {
it('instantiate module and add to db.modules', function() {
var Module1 = function(options) {
BaseModule.call(this, options);
};
inherits(Module1, BaseModule);
var db = new DB({store: memdown});
var node = {};
db.node = node;
db.modules = [];
db.addModule(Module1);
db.modules.length.should.equal(1);
should.exist(db.modules[0].node);
db.modules[0].node.should.equal(node);
});
it('should throw an error if module is not an instance of BaseModule', function() {
var Module2 = function(options) {};
var db = new DB({store: memdown});
db.modules = [];
(function() {
db.addModule(Module2);
}).should.throw('bitcore.ErrorInvalidArgumentType');
});
});
});

View File

@ -13,8 +13,10 @@ var index = require('..');
var fs = require('fs');
var bitcoinConfBuffer = fs.readFileSync(__dirname + '/data/bitcoin.conf');
var chainHashes = require('./data/hashes.json');
var util = require('util');
var BaseModule = require('../lib/module');
describe('Bitcoind Node', function() {
describe('Bitcore Node', function() {
var Node;
var BadNode;
@ -47,6 +49,36 @@ describe('Bitcoind Node', function() {
});
describe('@constructor', function() {
it('will set properties', function() {
function TestModule() {}
util.inherits(TestModule, BaseModule);
TestModule.prototype.getData = function() {};
TestModule.prototype.getAPIMethods = function() {
return [
['getData', this, this.getData, 1]
];
};
var config = {
modules: [
{
name: 'test1',
module: TestModule
}
],
};
var TestNode = proxyquire('../lib/node', {});
TestNode.prototype._loadConfiguration = sinon.spy();
TestNode.prototype._initialize = sinon.spy();
var node = new TestNode(config);
TestNode.prototype._loadConfiguration.callCount.should.equal(1);
TestNode.prototype._initialize.callCount.should.equal(1);
node._unloadedModules.length.should.equal(1);
node._unloadedModules[0].name.should.equal('test1');
node._unloadedModules[0].module.should.equal(TestModule);
});
});
describe('#openBus', function() {
it('will create a new bus', function() {
var node = new Node({});
@ -56,19 +88,41 @@ describe('Bitcoind Node', function() {
bus.db.should.equal(db);
});
});
describe('#addModule', function() {
it('will instantiate an instance and load api methods', function() {
var node = new Node({});
function TestModule() {}
util.inherits(TestModule, BaseModule);
TestModule.prototype.getData = function() {};
TestModule.prototype.getAPIMethods = function() {
return [
['getData', this, this.getData, 1]
];
};
var service = {
name: 'testmodule',
module: TestModule
};
node.addModule(service);
should.exist(node.modules.testmodule);
should.exist(node.getData);
});
});
describe('#getAllAPIMethods', function() {
it('should return db methods and modules methods', function() {
var node = new Node({});
node.modules = [
{
getAPIMethods: sinon.stub().returns(['mda1', 'mda2'])
},
{
getAPIMethods: sinon.stub().returns(['mdb1', 'mdb2'])
}
];
var db = {
getAPIMethods: sinon.stub().returns(['db1', 'db2']),
modules: [
{
getAPIMethods: sinon.stub().returns(['mda1', 'mda2'])
},
{
getAPIMethods: sinon.stub().returns(['mdb1', 'mdb2'])
}
]
};
node.db = db;
@ -79,16 +133,16 @@ describe('Bitcoind Node', function() {
describe('#getAllPublishEvents', function() {
it('should return modules publish events', function() {
var node = new Node({});
node.modules = [
{
getPublishEvents: sinon.stub().returns(['mda1', 'mda2'])
},
{
getPublishEvents: sinon.stub().returns(['mdb1', 'mdb2'])
}
];
var db = {
getPublishEvents: sinon.stub().returns(['db1', 'db2']),
modules: [
{
getPublishEvents: sinon.stub().returns(['mda1', 'mda2'])
},
{
getPublishEvents: sinon.stub().returns(['mdb1', 'mdb2'])
}
]
};
node.db = db;
@ -648,15 +702,96 @@ describe('Bitcoind Node', function() {
it('should return the services in the correct order', function() {
var node = new Node({});
node.getServices = function() {
return {
'chain': ['db'],
'db': ['daemon', 'p2p'],
'daemon': [],
'p2p': []
};
return [
{
name: 'chain',
dependencies: ['db']
},
{
name: 'db',
dependencies: ['daemon', 'p2p']
},
{
name:'daemon',
dependencies: []
},
{
name: 'p2p',
dependencies: []
}
];
};
var order = node.getServiceOrder();
order.should.deep.equal(['daemon', 'p2p', 'db', 'chain']);
order[0].name.should.equal('daemon');
order[1].name.should.equal('p2p');
order[2].name.should.equal('db');
order[3].name.should.equal('chain');
});
});
describe('#start', function() {
it('will call start for each module', function(done) {
var node = new Node({});
function TestModule() {}
util.inherits(TestModule, BaseModule);
TestModule.prototype.start = sinon.stub().callsArg(0);
TestModule.prototype.getData = function() {};
TestModule.prototype.getAPIMethods = function() {
return [
['getData', this, this.getData, 1]
];
};
node.test2 = {};
node.test2.start = sinon.stub().callsArg(0);
node.getServiceOrder = sinon.stub().returns([
{
name: 'test1',
module: TestModule
},
{
name: 'test2'
}
]);
node.start(function() {
node.test2.start.callCount.should.equal(1);
TestModule.prototype.start.callCount.should.equal(1);
done();
});
});
});
describe('#stop', function() {
it('will call stop for each module', function(done) {
var node = new Node({});
function TestModule() {}
util.inherits(TestModule, BaseModule);
TestModule.prototype.stop = sinon.stub().callsArg(0);
TestModule.prototype.getData = function() {};
TestModule.prototype.getAPIMethods = function() {
return [
['getData', this, this.getData, 1]
];
};
node.modules = {
'test1': new TestModule({node: node})
};
node.test2 = {};
node.test2.stop = sinon.stub().callsArg(0);
node.getServiceOrder = sinon.stub().returns([
{
name: 'test2'
},
{
name: 'test1',
module: TestModule
}
]);
node.stop(function() {
node.test2.stop.callCount.should.equal(1);
TestModule.prototype.stop.callCount.should.equal(1);
done();
});
});
});
});

View File

@ -12,7 +12,11 @@ describe('#start', function() {
it('require each bitcore-node module', function(done) {
var node;
var TestNode = function(options) {
options.db.modules.should.deep.equal([AddressModule]);
options.modules[0].should.deep.equal({
name: 'address',
module: AddressModule,
dependencies: ['bitcoind', 'db']
});
};
TestNode.prototype.on = sinon.stub();
TestNode.prototype.chain = {