1
0
Fork 0

DataValidator: Hold concurrent requests

Since we maintain state (as a kluge for the time being to integrate
with the rest of the system), we need to be careful to protect against
concurrent requests that might mess with it before the original
request is complete.

* src/validate/DataValidator.js
  (validate, updateFailures): Hold concurrent requests.
  (_onceReady): Add method.

* test/validate/DataValidatorTest.js: Add tests.
master
Mike Gerwitz 2017-02-16 16:53:43 -05:00
parent eefd268abf
commit ac7ec6c5f2
2 changed files with 98 additions and 28 deletions

View File

@ -109,8 +109,8 @@ module.exports = Class( 'DataValidator',
/** /**
* Validate diff and update field monitor * Validate diff and update field monitor
* *
* If a validation is pending completion, all further `#validate` * If an operation is pending completion, all further requests to this
* requests will be queued to prevent unexpected/inconsistent system * object will be queued to prevent unexpected/inconsistent system
* states and race conditions. * states and race conditions.
* *
* The external validator `validatef` is a kluge while the system * The external validator `validatef` is a kluge while the system
@ -126,16 +126,8 @@ module.exports = Class( 'DataValidator',
{ {
const _self = this; const _self = this;
// queue requests if we're already processing return this._onceReady( () =>
if ( this._pending )
{ {
this._pending.then(
() => this.validate( diff, classes, validatef )
);
return this._pending;
}
let failures = {}; let failures = {};
if ( diff !== undefined ) if ( diff !== undefined )
@ -156,14 +148,18 @@ module.exports = Class( 'DataValidator',
this._populateStore( this._populateStore(
classes, this._stores.cstore, 'indexes' classes, this._stores.cstore, 'indexes'
) )
.then( () => this.updateFailures( diff, failures ) ) .then( () => this._doUpdateFailures( diff, failures ) );
.then( () => this._pending = null ); } );
}, },
/** /**
* Update failures from external validation * Update failures from external validation
* *
* If an operation is pending completion, all further requests to this
* object will be queued to prevent unexpected/inconsistent system
* states and race conditions.
*
* TODO: This is a transitional API---we should handle all validations, * TODO: This is a transitional API---we should handle all validations,
* not allow external systems to meddle in our affairs. * not allow external systems to meddle in our affairs.
* *
@ -173,6 +169,23 @@ module.exports = Class( 'DataValidator',
* @return {Promise} promise to populate internal store * @return {Promise} promise to populate internal store
*/ */
'public updateFailures'( diff, failures ) 'public updateFailures'( diff, failures )
{
return this._onceReady( () =>
{
return this._doUpdateFailures( diff, failures );
} );
},
/**
* Update failures from external validation
*
* @param {Object} diff bucket diff
* @param {Object} failures failures per field name and index
*
* @return {Promise} promise to populate internal store
*/
'private _doUpdateFailures': function( diff, failures )
{ {
return this._populateStore( diff, this._stores.bstore ).then( () => return this._populateStore( diff, this._stores.bstore ).then( () =>
this._field_monitor.update( this._field_monitor.update(
@ -198,10 +211,31 @@ module.exports = Class( 'DataValidator',
'public clearFailures'( failures ) 'public clearFailures'( failures )
{ {
this._field_monitor.clearFailures( failures ); this._field_monitor.clearFailures( failures );
return this; return this;
}, },
/**
* Wait until all requests are complete and then trigger callback
*
* @param {Function} callback callback to trigger when ready
*
* @return {Promise}
*/
'private _onceReady': function( callback )
{
if ( this._pending )
{
this._pending.then( callback );
return this._pending;
}
return this._pending = callback()
.then( () => this._pending = null );
},
/** /**
* Populate store with data * Populate store with data
* *

View File

@ -316,6 +316,42 @@ describe( 'DataValidator', () =>
); );
} ); } );
} ); } );
it( 'queues concurrent requests', () =>
{
const { sut, vmonitor, getStore } = createStubs( {
vmonitor: sinon.createStubInstance( ValidStateMonitor ),
} );
const { bstore } = getStore();
const faila = {};
const failb = {};
let running_first = true;
vmonitor.update = ( _, fail ) =>
{
if ( fail === failb )
{
if ( running_first === true )
{
return Promise.reject( Error(
"Request not queued"
) );
}
}
return Promise.resolve( true );
};
return Promise.all( [
sut.updateFailures( {}, faila )
.then( () => running_first = false ),
sut.updateFailures( {}, failb ),
] );
} );
} ); } );