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 ```js
var configuration = { var configuration = {
datadir: process.env.BITCORENODE_DIR || '~/.bitcoin', datadir: process.env.BITCORENODE_DIR || '~/.bitcoin',
db: { modules: [MyModule]
modules: [MyModule]
}
}; };
var node = new Node(configuration); var node = new Node(configuration);

View File

@ -12,8 +12,6 @@ var index = require('./');
var errors = index.errors; var errors = index.errors;
var log = index.log; var log = index.log;
var Transaction = require('./transaction'); var Transaction = require('./transaction');
var BaseModule = require('./module');
var AddressModule = require('./modules/address');
function DB(options) { function DB(options) {
/* jshint maxstatements: 30 */ /* jshint maxstatements: 30 */
@ -52,12 +50,6 @@ function DB(options) {
this.node = options.node; this.node = options.node;
// Modules to be loaded when ready
this._modules = options.modules || [];
this._modules.push(AddressModule);
this.modules = [];
this.subscriptions = { this.subscriptions = {
transaction: [], transaction: [],
block: [] block: []
@ -79,12 +71,6 @@ DB.prototype.initialize = function() {
}; };
DB.prototype.start = function(callback) { 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.node.bitcoind.on('tx', this.transactionHandler.bind(this));
this.emit('ready'); this.emit('ready');
setImmediate(callback); setImmediate(callback);
@ -278,9 +264,9 @@ DB.prototype.blockHandler = function(block, add, callback) {
} }
async.eachSeries( async.eachSeries(
this.modules, this.node.modules,
function(module, next) { function(bitcoreNodeModule, next) {
module['blockHandler'].call(module, block, add, function(err, ops) { bitcoreNodeModule.blockHandler.call(bitcoreNodeModule, block, add, function(err, ops) {
if(err) { if(err) {
return next(err); return next(err);
} }
@ -308,11 +294,6 @@ DB.prototype.getAPIMethods = function() {
['sendTransaction', this, this.sendTransaction, 1], ['sendTransaction', this, this.sendTransaction, 1],
['estimateFee', this, this.estimateFee, 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; 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) { DB.prototype.subscribe = function(name, emitter) {
this.subscriptions[name].push(emitter); this.subscriptions[name].push(emitter);
}; };

View File

@ -4,6 +4,11 @@ var Module = function(options) {
this.node = options.node; this.node = options.node;
}; };
/**
* Describes the dependencies that should be loaded before this module.
*/
Module.dependencies = [];
/** /**
* blockHandler * blockHandler
* @param {Block} block - the block being added or removed from the chain * @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; module.exports = Module;

View File

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

View File

@ -17,6 +17,7 @@ var index = require('./');
var log = index.log; var log = index.log;
var daemon = require('./daemon'); var daemon = require('./daemon');
var Bus = require('./bus'); var Bus = require('./bus');
var BaseModule = require('./module');
function Node(config) { function Node(config) {
if(!(this instanceof Node)) { if(!(this instanceof Node)) {
@ -27,10 +28,17 @@ function Node(config) {
this.chain = null; this.chain = null;
this.network = 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._loadConfiguration(config);
this._initialize(); this._initialize();
this.testnet = config.testnet;
} }
util.inherits(Node, EventEmitter); util.inherits(Node, EventEmitter);
@ -39,10 +47,41 @@ Node.prototype.openBus = function() {
return new Bus({db: this.db}); 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() { Node.prototype.getAllAPIMethods = function() {
var methods = this.db.getAPIMethods(); var methods = this.db.getAPIMethods();
for (var i = 0; i < this.db.modules.length; i++) { for(var i in this.modules) {
var mod = this.db.modules[i]; var mod = this.modules[i];
methods = methods.concat(mod.getAPIMethods()); methods = methods.concat(mod.getAPIMethods());
} }
return methods; return methods;
@ -50,8 +89,8 @@ Node.prototype.getAllAPIMethods = function() {
Node.prototype.getAllPublishEvents = function() { Node.prototype.getAllPublishEvents = function() {
var events = this.db.getPublishEvents(); var events = this.db.getPublishEvents();
for (var i = 0; i < this.db.modules.length; i++) { for (var i in this.modules) {
var mod = this.db.modules[i]; var mod = this.modules[i];
events = events.concat(mod.getPublishEvents()); events = events.concat(mod.getPublishEvents());
} }
return events; return events;
@ -379,7 +418,6 @@ Node.prototype._loadConsensus = function(config) {
Node.prototype._loadAPI = function() { Node.prototype._loadAPI = function() {
var self = this; var self = this;
var methodData = self.db.getAPIMethods(); var methodData = self.db.getAPIMethods();
methodData.forEach(function(data) { methodData.forEach(function(data) {
var name = data[0]; var name = data[0];
@ -456,32 +494,62 @@ Node.prototype._initializeChain = function() {
}; };
Node.prototype.getServices = function() { Node.prototype.getServices = function() {
var defaultServices = { var services = [
'bitcoind': [], {
'db': ['bitcoind'], name: 'bitcoind',
'chain': ['db'] dependencies: []
}; },
return defaultServices; {
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(); var services = this.getServices();
if(!keys) { // organize data for sorting
keys = Object.keys(services); 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) { var stackNames = {};
stack = []; 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; return stack;
}; };
@ -492,8 +560,15 @@ Node.prototype.start = function(callback) {
async.eachSeries( async.eachSeries(
servicesOrder, servicesOrder,
function(service, next) { function(service, next) {
log.info('Starting ' + service); log.info('Starting ' + service.name);
self[service].start(next);
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 callback
); );
@ -510,12 +585,16 @@ Node.prototype.stop = function(callback) {
async.eachSeries( async.eachSeries(
services, services,
function(service, next) { function(service, next) {
log.info('Stopping ' + service); log.info('Stopping ' + service.name);
self[service].stop(next);
if (service.module) {
self.modules[service.name].stop(next);
} else {
self[service.name].stop(next);
}
}, },
callback callback
); );
}; };
module.exports = Node; module.exports = Node;

View File

@ -12,7 +12,8 @@ function getDefaultConfig() {
config: { config: {
datadir: process.env.BITCORENODE_DIR || path.resolve(process.env.HOME, '.bitcoin'), datadir: process.env.BITCORENODE_DIR || path.resolve(process.env.HOME, '.bitcoin'),
network: process.env.BITCORENODE_NETWORK || 'livenet', 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; bitcoreNodeModule = moduleName + '/' + modulePackage.bitcoreNode;
} }
bitcoreModule = require(bitcoreNodeModule); bitcoreModule = require(bitcoreNodeModule);
} }
// check that the module supports expected methods // check that the module supports expected methods
if (!bitcoreModule.prototype || if (!bitcoreModule.prototype ||
!bitcoreModule.dependencies ||
!bitcoreModule.prototype.start || !bitcoreModule.prototype.start ||
!bitcoreModule.prototype.stop) { !bitcoreModule.prototype.stop) {
throw new Error( throw new Error(
'Could not load module "' + moduleName + '" as it does not support necessary methods.' '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 // expand to the full path
fullConfig.datadir = path.resolve(configPath, config.datadir); fullConfig.datadir = path.resolve(configPath, config.datadir);
// delete until modules move to the node
delete fullConfig.modules;
// load the modules // load the modules
fullConfig.db = { fullConfig.modules = bitcoreModules;
modules: bitcoreModules
};
var node = new BitcoreNode(fullConfig); var node = new BitcoreNode(fullConfig);

View File

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

View File

@ -16,7 +16,6 @@ var bitcore = require('bitcore');
var Transaction = bitcore.Transaction; var Transaction = bitcore.Transaction;
describe('Bitcoin DB', function() { describe('Bitcoin DB', function() {
var coinbaseAmount = 50 * 1e8;
describe('#start', function() { describe('#start', function() {
it('should emit ready', function(done) { 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']); Module1.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op1', 'op2', 'op3']);
var Module2 = function() {}; var Module2 = function() {};
Module2.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op4', 'op5']); Module2.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op4', 'op5']);
db.modules = [ db.node = {};
db.node.modules = [
new Module1(), new Module1(),
new Module2() 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) { it('should give an error if one of the modules gives an error', function(done) {
var Module3 = function() {}; var Module3 = function() {};
Module3.prototype.blockHandler = sinon.stub().callsArgWith(2, new Error('error')); 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) { db.blockHandler('block', true, function(err) {
should.exist(err); should.exist(err);
@ -367,62 +367,11 @@ describe('Bitcoin DB', function() {
describe('#getAPIMethods', function() { describe('#getAPIMethods', function() {
it('should return the correct db methods', function() { it('should return the correct db methods', function() {
var db = new DB({store: memdown}); var db = new DB({store: memdown});
db.modules = []; db.node = {};
db.node.modules = [];
var methods = db.getAPIMethods(); var methods = db.getAPIMethods();
methods.length.should.equal(4); 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 fs = require('fs');
var bitcoinConfBuffer = fs.readFileSync(__dirname + '/data/bitcoin.conf'); var bitcoinConfBuffer = fs.readFileSync(__dirname + '/data/bitcoin.conf');
var chainHashes = require('./data/hashes.json'); 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 Node;
var BadNode; 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() { describe('#openBus', function() {
it('will create a new bus', function() { it('will create a new bus', function() {
var node = new Node({}); var node = new Node({});
@ -56,19 +88,41 @@ describe('Bitcoind Node', function() {
bus.db.should.equal(db); 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() { describe('#getAllAPIMethods', function() {
it('should return db methods and modules methods', function() { it('should return db methods and modules methods', function() {
var node = new Node({}); var node = new Node({});
node.modules = [
{
getAPIMethods: sinon.stub().returns(['mda1', 'mda2'])
},
{
getAPIMethods: sinon.stub().returns(['mdb1', 'mdb2'])
}
];
var db = { var db = {
getAPIMethods: sinon.stub().returns(['db1', 'db2']), getAPIMethods: sinon.stub().returns(['db1', 'db2']),
modules: [
{
getAPIMethods: sinon.stub().returns(['mda1', 'mda2'])
},
{
getAPIMethods: sinon.stub().returns(['mdb1', 'mdb2'])
}
]
}; };
node.db = db; node.db = db;
@ -79,16 +133,16 @@ describe('Bitcoind Node', function() {
describe('#getAllPublishEvents', function() { describe('#getAllPublishEvents', function() {
it('should return modules publish events', function() { it('should return modules publish events', function() {
var node = new Node({}); var node = new Node({});
node.modules = [
{
getPublishEvents: sinon.stub().returns(['mda1', 'mda2'])
},
{
getPublishEvents: sinon.stub().returns(['mdb1', 'mdb2'])
}
];
var db = { var db = {
getPublishEvents: sinon.stub().returns(['db1', 'db2']), getPublishEvents: sinon.stub().returns(['db1', 'db2']),
modules: [
{
getPublishEvents: sinon.stub().returns(['mda1', 'mda2'])
},
{
getPublishEvents: sinon.stub().returns(['mdb1', 'mdb2'])
}
]
}; };
node.db = db; node.db = db;
@ -648,15 +702,96 @@ describe('Bitcoind Node', function() {
it('should return the services in the correct order', function() { it('should return the services in the correct order', function() {
var node = new Node({}); var node = new Node({});
node.getServices = function() { node.getServices = function() {
return { return [
'chain': ['db'], {
'db': ['daemon', 'p2p'], name: 'chain',
'daemon': [], dependencies: ['db']
'p2p': [] },
}; {
name: 'db',
dependencies: ['daemon', 'p2p']
},
{
name:'daemon',
dependencies: []
},
{
name: 'p2p',
dependencies: []
}
];
}; };
var order = node.getServiceOrder(); 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) { it('require each bitcore-node module', function(done) {
var node; var node;
var TestNode = function(options) { 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.on = sinon.stub();
TestNode.prototype.chain = { TestNode.prototype.chain = {