From 5a66f9ad725b4ac501e8f84204e6bd4368c9d6e5 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 22 May 2018 10:06:50 -0700 Subject: [PATCH] Fix datasource state management Use case: 1. Configure a datasource with lazyConnect = true 2. Do NOT start the DB 3. Start the app 4. Send first request and it fails to connnect to the DB 5. Start the DB 5. Requests are now served correctly --- lib/datasource.js | 58 +++++++++++--- test/datasource.test.js | 166 ++++++++++++++++++++++++++++++++++++++++ types/datasource.d.ts | 1 + 3 files changed, 214 insertions(+), 11 deletions(-) diff --git a/lib/datasource.js b/lib/datasource.js index cb9e96fb..029d7c5e 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -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; diff --git a/test/datasource.test.js b/test/datasource.test.js index 22ab7d47..a0cd1dc2 100644 --- a/test/datasource.test.js +++ b/test/datasource.test.js @@ -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'}); diff --git a/types/datasource.d.ts b/types/datasource.d.ts index 35ac5a20..117cbc97 100644 --- a/types/datasource.d.ts +++ b/types/datasource.d.ts @@ -76,6 +76,7 @@ export declare class DataSource { name: string; settings: Options; + initialized?: boolean; connected?: boolean; connecting?: boolean;