Merge pull request #1585 from strongloop/fix-lazy-connect

Fix datasource state management
This commit is contained in:
Raymond Feng 2018-05-22 10:51:35 -07:00 committed by GitHub
commit 41c9a6d9b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 214 additions and 11 deletions

View File

@ -314,8 +314,6 @@ DataSource.prototype.connect = function(callback) {
// The connect is already in progress
if (this.connecting) return callback.promise;
// Set connecting flag to be true
this.connecting = true;
this.connector.connect(function(err, result) {
self.connecting = false;
if (!err) self.connected = true;
@ -340,6 +338,11 @@ DataSource.prototype.connect = function(callback) {
if (err) throw err; // It should not happen
});
});
// Set connecting flag to be `true` so that the connector knows there is
// a connect in progress. The change of `connecting` should happen immediately
// after the connect request is sent
this.connecting = true;
return callback.promise;
};
@ -416,6 +419,7 @@ DataSource.prototype.setup = function(dsName, settings) {
// Disconnected by default
this.connected = false;
this.connecting = false;
this.initialized = false;
this.name = dsName || (typeof this.settings.name === 'string' && this.settings.name);
@ -453,20 +457,38 @@ DataSource.prototype.setup = function(dsName, settings) {
throw new Error(g.f('Connector is not defined correctly: ' +
'it should create `{{connector}}` member of dataSource'));
}
this.connected = !err; // Connected now
if (!err) {
this.initialized = true;
this.emit('initialized');
}
debug('Connector is initialized for dataSource %s', this.name);
// If `result` is set to `false` explicitly, the connection will be
// lazily established
if (!this.settings.lazyConnect) {
this.connected = (!err) && (result !== false); // Connected now
}
if (this.connected) {
debug('DataSource %s is now connected to %s', this.name, this.connector.name);
this.emit('connected');
} else {
// The connection fails, let's report it and hope it will be recovered in the next call
g.error('Connection fails: %s\nIt will be retried for the next request.', err);
this.emit('error', err);
this.connecting = false;
if (err) {
// Reset the connecting to `false`
this.connecting = false;
g.error('Connection fails: %s\nIt will be retried for the next request.', err);
this.emit('error', err);
} else {
// Either lazyConnect or connector initialize() defers the connection
debug('DataSource %s will be connected to connector %s', this.name,
this.connector.name);
}
}
}.bind(this);
try {
if ('function' === typeof connector.initialize) {
// Call the async initialize method
debug('Initializing connector %s', connector.name);
connector.initialize(this, postInit);
} else if ('function' === typeof connector) {
// Use the connector constructor directly
@ -2464,13 +2486,21 @@ DataSource.prototype.isRelational = function() {
};
/**
* Check if the data source is ready.
* Returns a Boolean value.
* @param {Object} obj Deferred method call if the connector is not fully connected yet
* @param {Object} args argument passing to method call.
* Check if the data source is connected. If not, the method invocation will be
* deferred and queued.
*
*
* @param {Object} obj Receiver for the method call
* @param {Object} args Arguments passing to the method call
* @returns - a Boolean value to indicate if the method invocation is deferred.
* false: The datasource is already connected
* - true: The datasource is yet to be connected
*/
DataSource.prototype.ready = function(obj, args) {
DataSource.prototype.queueInvocation = DataSource.prototype.ready =
function(obj, args) {
var self = this;
debug('Datasource %s: connected=%s connecting=%s', this.name,
this.connected, this.connecting);
if (this.connected) {
// Connected
return false;
@ -2481,6 +2511,7 @@ DataSource.prototype.ready = function(obj, args) {
var onConnected = null, onError = null, timeoutHandle = null;
onConnected = function() {
debug('Datasource %s is now connected - executing method %s', self.name, method.name);
// Remove the error handler
self.removeListener('error', onError);
if (timeoutHandle) {
@ -2502,6 +2533,7 @@ DataSource.prototype.ready = function(obj, args) {
}
};
onError = function(err) {
debug('Datasource %s fails to connect - aborting method %s', self.name, method.name);
// Remove the connected listener
self.removeListener('connected', onConnected);
if (timeoutHandle) {
@ -2521,6 +2553,9 @@ DataSource.prototype.ready = function(obj, args) {
// Set up a timeout to cancel the invocation
var timeout = this.settings.connectionTimeout || 5000;
timeoutHandle = setTimeout(function() {
debug('Datasource %s fails to connect due to timeout - aborting method %s',
self.name, method.name);
self.connecting = false;
self.removeListener('error', onError);
self.removeListener('connected', onConnected);
var params = [].slice.call(args);
@ -2531,6 +2566,7 @@ DataSource.prototype.ready = function(obj, args) {
}, timeout);
if (!this.connecting) {
debug('Connecting datasource %s to connector %s', this.name, this.connector.name);
this.connect();
}
return true;

View File

@ -155,6 +155,172 @@ describe('DataSource', function() {
dataSource.connector.should.equal(mockConnector);
});
it('should set states correctly with eager connect', function(done) {
var mockConnector = {
name: 'loopback-connector-mock',
initialize: function(ds, cb) {
ds.connector = mockConnector;
this.connect(cb);
},
connect: function(cb) {
process.nextTick(function() {
cb(null);
});
},
};
var dataSource = new DataSource(mockConnector);
// DataSource is instantiated
// connected: false, connecting: false, initialized: false
dataSource.connected.should.be.false();
dataSource.connecting.should.be.false();
dataSource.initialized.should.be.false();
dataSource.on('initialized', function() {
// DataSource is initialized with lazyConnect
// connected: false, connecting: false, initialized: true
dataSource.connected.should.be.false();
dataSource.connecting.should.be.false();
dataSource.initialized.should.be.true();
});
dataSource.on('connected', function() {
// DataSource is now connected
// connected: true, connecting: false
dataSource.connected.should.be.true();
dataSource.connecting.should.be.false();
});
// Call connect() in next tick so that we'll receive initialized event
// first
process.nextTick(function() {
// At this point, the datasource is already connected by
// connector's (mockConnector) initialize function
dataSource.connect(function() {
// DataSource is now connected
// connected: true, connecting: false
dataSource.connected.should.be.true();
dataSource.connecting.should.be.false();
done();
});
// As the datasource is already connected, no connecting will happen
// connected: true, connecting: false
dataSource.connected.should.be.true();
dataSource.connecting.should.be.false();
});
});
it('should set states correctly with deferred connect', function(done) {
var mockConnector = {
name: 'loopback-connector-mock',
initialize: function(ds, cb) {
ds.connector = mockConnector;
// Explicitly call back with false to denote connection is not ready
process.nextTick(function() {
cb(null, false);
});
},
connect: function(cb) {
process.nextTick(function() {
cb(null);
});
},
};
var dataSource = new DataSource(mockConnector);
// DataSource is instantiated
// connected: false, connecting: false, initialized: false
dataSource.connected.should.be.false();
dataSource.connecting.should.be.false();
dataSource.initialized.should.be.false();
dataSource.on('initialized', function() {
// DataSource is initialized with lazyConnect
// connected: false, connecting: false, initialized: true
dataSource.connected.should.be.false();
dataSource.connecting.should.be.false();
dataSource.initialized.should.be.true();
});
dataSource.on('connected', function() {
// DataSource is now connected
// connected: true, connecting: false
dataSource.connected.should.be.true();
dataSource.connecting.should.be.false();
});
// Call connect() in next tick so that we'll receive initialized event
// first
process.nextTick(function() {
dataSource.connect(function() {
// DataSource is now connected
// connected: true, connecting: false
dataSource.connected.should.be.true();
dataSource.connecting.should.be.false();
done();
});
// As the datasource is not connected, connecting will happen
// connected: false, connecting: true
dataSource.connected.should.be.false();
dataSource.connecting.should.be.true();
});
});
it('should set states correctly with lazyConnect = true', function(done) {
var mockConnector = {
name: 'loopback-connector-mock',
initialize: function(ds, cb) {
ds.connector = mockConnector;
process.nextTick(function() {
cb(null);
});
},
connect: function(cb) {
process.nextTick(function() {
cb(null);
});
},
};
var dataSource = new DataSource(mockConnector, {lazyConnect: true});
// DataSource is instantiated
// connected: false, connecting: false, initialized: false
dataSource.connected.should.be.false();
dataSource.connecting.should.be.false();
dataSource.initialized.should.be.false();
dataSource.on('initialized', function() {
// DataSource is initialized with lazyConnect
// connected: false, connecting: false, initialized: true
dataSource.connected.should.be.false();
dataSource.connecting.should.be.false();
dataSource.initialized.should.be.true();
});
dataSource.on('connected', function() {
// DataSource is now connected
// connected: true, connecting: false
dataSource.connected.should.be.true();
dataSource.connecting.should.be.false();
});
// Call connect() in next tick so that we'll receive initialized event
// first
process.nextTick(function() {
dataSource.connect(function() {
// DataSource is now connected
// connected: true, connecting: false
dataSource.connected.should.be.true();
dataSource.connecting.should.be.false();
done();
});
// DataSource is now connecting
// connected: false, connecting: true
dataSource.connected.should.be.false();
dataSource.connecting.should.be.true();
});
});
describe('deleteModelByName()', () => {
it('removes the model from ModelBuilder registry', () => {
const ds = new DataSource('ds', {connector: 'memory'});

View File

@ -76,6 +76,7 @@ export declare class DataSource {
name: string;
settings: Options;
initialized?: boolean;
connected?: boolean;
connecting?: boolean;