1
0
Fork 0

DataValidator: Queue validations when incomplete

* src/validate/DataValidator.js (validate): If a validation is
    ongoing, queue requests.

* test/validate/DataValidatorTest.js: Add test.

DEV-2299
master
Mike Gerwitz 2017-02-13 13:36:31 -05:00
parent 9225924e7e
commit 71024bd389
2 changed files with 91 additions and 2 deletions

View File

@ -61,6 +61,12 @@ module.exports = Class( 'DataValidator',
*/ */
'private _stores': {}, 'private _stores': {},
/**
* Pending validation
* @type {Promise}
*/
'private _pending': null,
/** /**
* Initialize validator * Initialize validator
@ -103,6 +109,10 @@ 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`
* requests will be queued to prevent unexpected/inconsistent system
* states and race conditions.
*
* The external validator `validatef` is a kluge while the system * The external validator `validatef` is a kluge while the system
* undergoes refactoring. * undergoes refactoring.
* *
@ -116,6 +126,16 @@ module.exports = Class( 'DataValidator',
{ {
const _self = this; const _self = this;
// queue requests if we're already processing
if ( this._pending )
{
this._pending.then(
() => this.validate( diff, classes, validatef )
);
return this._pending;
}
let failures = {}; let failures = {};
if ( diff !== undefined ) if ( diff !== undefined )
@ -132,8 +152,12 @@ module.exports = Class( 'DataValidator',
} }
// XXX: this assumes that the above is synchronous // XXX: this assumes that the above is synchronous
return this._populateStore( classes, this._stores.cstore, 'indexes' ) return this._pending =
.then( () => this.updateFailures( diff, failures ) ); this._populateStore(
classes, this._stores.cstore, 'indexes'
)
.then( () => this.updateFailures( diff, failures ) )
.then( () => this._pending = null );
}, },

View File

@ -251,6 +251,71 @@ describe( 'DataValidator', () =>
expect( cleared.b && cleared.c ).to.be.true expect( cleared.b && cleared.c ).to.be.true
); );
} ) ); } ) );
// otherwise system might get into an unexpected state
it( 'queues concurrent validations', () =>
{
const expected_failure = {};
let vcalled = 0;
const bvalidator = createMockBucketValidator( function( _, __, ___ )
{
vcalled++;
} );
const vmonitor = sinon.createStubInstance( ValidStateMonitor );
const dep_factory = createMockDependencyFactory();
const getStore = createStubStore();
const diff_a = { foo: [ 'a', 'b', 'c' ] };
const diff_b = { foo: [ 'd' ] };
const validatef = ( diff, failures ) =>
{
// not a real failure; just used to transfer state to stub
// (see below)
failures.failedon = diff;
};
return new Promise( ( accept, reject ) =>
{
// by the time it gets to this the second time, store could
// be in any sort of state depending on what callbacks were
// invoked first (implementation details)
vmonitor.update = ( _, failures ) =>
{
const orig_diff = failures.failedon;
// if the external validator was called twice, then they
// didn't wait for us to finish
if ( ( orig_diff === diff_a ) && ( vcalled !== 1 ) )
{
reject( Error( "Request not queued" ) );
}
// if this key doesn't exist, then the store has been
// cleared (which happens before it's re-populated with
// the new diff)
return expect( getStore().bstore.get( 'foo' ) )
.to.eventually.deep.equal( orig_diff.foo )
.then( () => {
// the second test, after which we're done
if ( orig_diff === diff_b )
{
accept();
}
} )
.catch( e => reject( e ) );
};
const sut = Sut( bvalidator, vmonitor, dep_factory, getStore );
sut.validate( diff_a, {}, validatef );
sut.validate( diff_b, {}, validatef );
} );
} );
} ); } );