-
Notifications
You must be signed in to change notification settings - Fork 23
Retry the setup of an ingestion reader when it fails #2806
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development/9.5
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -278,6 +278,322 @@ describe('Ingestion Populator', () => { | |
| }); | ||
| }); | ||
|
|
||
| describe('_setupUpdatedReaders', () => { | ||
| const FAILING_BUCKET = 'failing-zenko-bucket'; | ||
| const WORKING_BUCKET = 'working-zenko-bucket'; | ||
|
Comment on lines
+282
to
+283
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: nothing working or failing for these buckets. Only ingestion is failing, not the buckets putting the name here (which may be needed because the mocks are hidden deep down c.f. https://github.com/scality/backbeat/pull/2806/changes#r3880675313) gives a false impression there is something, where in fact it is just the ingestion setup mock which fail... |
||
|
|
||
| /** | ||
| * @param {string} zenkoBucket - target zenko bucket of the reader | ||
| * @param {Error|null} setupError - error to fail `setup` with | ||
| * @return {object} the stubbed reader | ||
| */ | ||
| function createLogReaderMock(zenkoBucket, setupError) { | ||
| const logReader = sinon.createStubInstance(IngestionReader); | ||
| logReader.getTargetZenkoBucketName.returns(zenkoBucket); | ||
| logReader.setup.yieldsAsync(setupError); | ||
| return logReader; | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| ip.logReaders = []; | ||
| ip.logReadersUpdate = []; | ||
| }); | ||
|
|
||
| it('should activate a log reader once its setup succeeds', done => { | ||
| const logReaderMock = createLogReaderMock(WORKING_BUCKET, null); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: calling Of you stick with this form to reduce dedup, than should the next 2 lines as well into that helper function createLogReaderMock(zenkoBucket, setupError) {
const logReader = sinon.createStubInstance(IngestionReader);
logReader.getTargetZenkoBucketName.returns(zenkoBucket);
logReader.setup.yieldsAsync(setupError);
ip._ingestionSources[zenkoBucket] = logReader;
ip.logReadersUpdate = [logReader];
return logReader;
}(mixing both approaches makes it a bit weird)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or even just just use the helper to create/add the reader ; but keep the useful mock call ( function addLogReader(zenkoBucket) {
const logReader = sinon.createStubInstance(IngestionReader);
logReader.getTargetZenkoBucketName.returns(zenkoBucket);
ip._ingestionSources[zenkoBucket] = logReader;
ip.logReadersUpdate = [logReader];
return logReader;
}
it(....) {
addLogReader("bucket1").setup.yieldsAsync(errors.InternalError);
}In addition to making the mocks more explicit, it would also make the tests more readable, by removing extraneous list modifications - like the following, where the test needs to manually edit only to simulate a race condition (nothing needed for simple case): // should not queue a log reader again when its setup fails and its source is no longer configured
createLogReaderMock("bucket1")
delete ip._ingestionSources["bucket1"]; // remove from the config |
||
| ip._ingestionSources[WORKING_BUCKET] = logReaderMock; | ||
| ip.logReadersUpdate = [logReaderMock]; | ||
|
|
||
| ip._setupUpdatedReaders(err => { | ||
| assert.ifError(err); | ||
| assert.deepStrictEqual(ip.logReaders, [logReaderMock]); | ||
| assert.deepStrictEqual(ip.logReadersUpdate, []); | ||
| done(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should queue a log reader again when its setup fails', done => { | ||
| const logReaderMock = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. line break not needed I guess, fits within max column in our prettier config? |
||
| createLogReaderMock(FAILING_BUCKET, errors.InternalError); | ||
| ip._ingestionSources[FAILING_BUCKET] = logReaderMock; | ||
| ip.logReadersUpdate = [logReaderMock]; | ||
|
|
||
| ip._setupUpdatedReaders(err => { | ||
| assert.ifError(err); | ||
| assert.deepStrictEqual(ip.logReaders, []); | ||
| assert.deepStrictEqual(ip.logReadersUpdate, [logReaderMock]); | ||
| done(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should not queue a log reader again when its setup fails and ' + | ||
| 'its source is no longer configured', done => { | ||
| const logReaderMock = | ||
| createLogReaderMock(FAILING_BUCKET, errors.InternalError); | ||
| delete ip._ingestionSources[FAILING_BUCKET]; | ||
| ip.logReadersUpdate = [logReaderMock]; | ||
|
|
||
| ip._setupUpdatedReaders(err => { | ||
| assert.ifError(err); | ||
| assert.deepStrictEqual(ip.logReaders, []); | ||
| assert.deepStrictEqual(ip.logReadersUpdate, []); | ||
| done(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should not queue a log reader again when its setup fails and ' + | ||
| 'its source has been registered with another reader', done => { | ||
| const staleReader = | ||
| createLogReaderMock(FAILING_BUCKET, errors.InternalError); | ||
| const currentReader = createLogReaderMock(FAILING_BUCKET, null); | ||
| ip._ingestionSources[FAILING_BUCKET] = currentReader; | ||
| ip.logReadersUpdate = [staleReader]; | ||
|
|
||
| ip._setupUpdatedReaders(err => { | ||
| assert.ifError(err); | ||
| assert.deepStrictEqual(ip.logReaders, []); | ||
| assert.deepStrictEqual(ip.logReadersUpdate, []); | ||
| done(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should not activate a log reader when its setup succeeds and ' + | ||
| 'its source is no longer configured', done => { | ||
| const logReaderMock = createLogReaderMock(WORKING_BUCKET, null); | ||
| delete ip._ingestionSources[WORKING_BUCKET]; | ||
| ip.logReadersUpdate = [logReaderMock]; | ||
|
|
||
| ip._setupUpdatedReaders(err => { | ||
| assert.ifError(err); | ||
| assert.deepStrictEqual(ip.logReaders, []); | ||
| assert.deepStrictEqual(ip.logReadersUpdate, []); | ||
| done(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should not activate a log reader when its setup succeeds and ' + | ||
| 'its source has been registered with another reader', done => { | ||
| const staleReader = createLogReaderMock(WORKING_BUCKET, null); | ||
| const currentReader = createLogReaderMock(WORKING_BUCKET, null); | ||
| ip._ingestionSources[WORKING_BUCKET] = currentReader; | ||
| ip.logReadersUpdate = [staleReader]; | ||
|
|
||
| ip._setupUpdatedReaders(err => { | ||
| assert.ifError(err); | ||
| assert.deepStrictEqual(ip.logReaders, []); | ||
| assert.deepStrictEqual(ip.logReadersUpdate, []); | ||
| done(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should keep setting up other log readers when one fails', done => { | ||
| const failingReader = | ||
| createLogReaderMock(FAILING_BUCKET, errors.InternalError); | ||
| const workingReader = createLogReaderMock(WORKING_BUCKET, null); | ||
| ip._ingestionSources[FAILING_BUCKET] = failingReader; | ||
| ip._ingestionSources[WORKING_BUCKET] = workingReader; | ||
| ip.logReadersUpdate = [failingReader, workingReader]; | ||
|
|
||
| ip._setupUpdatedReaders(err => { | ||
| assert.ifError(err); | ||
| assert.deepStrictEqual(ip.logReaders, [workingReader]); | ||
| assert.deepStrictEqual(ip.logReadersUpdate, [failingReader]); | ||
| done(); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('removing a source while its setup is in flight', () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this describe section is weird: this is still testing
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it tests the interaction between _setupUpdatedReaders and _closeLogState, but is it not redundant? e.g. if you validate on one side the way |
||
| const REMOVED_BUCKET = 'removed-zenko-bucket'; | ||
|
|
||
| // `IngestionPopulatorMock` stubs out `_closeLogState`, so a real | ||
| // populator is needed to exercise the removal path. | ||
| let populator; | ||
|
|
||
| beforeEach(() => { | ||
| populator = new IngestionPopulator( | ||
| zkConfig, | ||
| kafkaConfig, | ||
| qpConfig, | ||
| mConfig, | ||
| rConfig, | ||
| ingestionConfig, | ||
| s3Config, | ||
| ); | ||
| }); | ||
|
|
||
| /** | ||
| * Build a reader whose `setup` stays pending until it is released, to | ||
| * hold the reader in neither `logReaders` nor `logReadersUpdate`. | ||
| * | ||
| * @param {string} zenkoBucket - target zenko bucket of the reader | ||
| * @param {Error|null} setupError - error to fail `setup` with | ||
| * @return {object} the stubbed reader and the release function | ||
| */ | ||
| function createPendingLogReaderMock(zenkoBucket, setupError) { | ||
| const logReader = sinon.createStubInstance(IngestionReader); | ||
| logReader.getTargetZenkoBucketName.returns(zenkoBucket); | ||
|
|
||
| let setupCb = null; | ||
| logReader.setup.callsFake(cb => { | ||
| setupCb = cb; | ||
| }); | ||
|
|
||
| return { logReader, release: () => setupCb(setupError) }; | ||
| } | ||
|
|
||
| it('should not queue a log reader again when its source is removed ' + | ||
| 'while its setup fails', done => { | ||
| const { logReader, release } = | ||
| createPendingLogReaderMock(REMOVED_BUCKET, errors.InternalError); | ||
| populator._ingestionSources[REMOVED_BUCKET] = logReader; | ||
| populator.logReadersUpdate = [logReader]; | ||
|
|
||
| populator._setupUpdatedReaders(err => { | ||
| assert.ifError(err); | ||
| assert.deepStrictEqual(populator.logReadersUpdate, []); | ||
| done(); | ||
| }); | ||
|
|
||
| populator._closeLogState(REMOVED_BUCKET); | ||
| assert.strictEqual(populator._ingestionSources[REMOVED_BUCKET], | ||
| undefined); | ||
|
|
||
| release(); | ||
| }); | ||
|
|
||
| it('should not activate a log reader when its source is removed ' + | ||
| 'while its setup succeeds', done => { | ||
| const { logReader, release } = | ||
| createPendingLogReaderMock(REMOVED_BUCKET, null); | ||
| populator._ingestionSources[REMOVED_BUCKET] = logReader; | ||
| populator.logReadersUpdate = [logReader]; | ||
|
|
||
| populator._setupUpdatedReaders(err => { | ||
| assert.ifError(err); | ||
| assert.deepStrictEqual(populator.logReaders, []); | ||
| assert.deepStrictEqual(populator.logReadersUpdate, []); | ||
| done(); | ||
| }); | ||
|
|
||
| populator._closeLogState(REMOVED_BUCKET); | ||
| release(); | ||
| }); | ||
|
|
||
| it('should not activate a log reader when its source is replaced ' + | ||
| 'while its setup succeeds', done => { | ||
| const { logReader, release } = | ||
| createPendingLogReaderMock(REMOVED_BUCKET, null); | ||
| const freshReader = sinon.createStubInstance(IngestionReader); | ||
| freshReader.getTargetZenkoBucketName.returns(REMOVED_BUCKET); | ||
|
|
||
| populator._ingestionSources[REMOVED_BUCKET] = logReader; | ||
| populator.logReadersUpdate = [logReader]; | ||
|
|
||
| populator._setupUpdatedReaders(err => { | ||
| assert.ifError(err); | ||
| assert.deepStrictEqual(populator.logReaders, []); | ||
| assert.deepStrictEqual(populator.logReadersUpdate, []); | ||
| // the reader registered in the meantime is left untouched | ||
| assert.strictEqual(populator._ingestionSources[REMOVED_BUCKET], | ||
| freshReader); | ||
| done(); | ||
| }); | ||
|
|
||
| // the source is removed, then configured again before the setup | ||
| // of the first reader completes | ||
| populator._closeLogState(REMOVED_BUCKET); | ||
| populator._ingestionSources[REMOVED_BUCKET] = freshReader; | ||
|
|
||
| release(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('_closeLogState', () => { | ||
| const ACTIVE_BUCKET = 'active-zenko-bucket'; | ||
| const PENDING_BUCKET = 'pending-zenko-bucket'; | ||
|
|
||
| // `IngestionPopulatorMock` stubs out `_closeLogState`, so a real | ||
| // populator is needed to exercise it. | ||
| let populator; | ||
| let removeReaderState; | ||
|
|
||
| beforeEach(() => { | ||
| populator = new IngestionPopulator( | ||
| zkConfig, | ||
| kafkaConfig, | ||
| qpConfig, | ||
| mConfig, | ||
| rConfig, | ||
| ingestionConfig, | ||
| s3Config, | ||
| ); | ||
| // `_removeReaderState` polls the reader on a timer that would | ||
| // outlive the test | ||
| removeReaderState = sinon.stub(populator, '_removeReaderState'); | ||
| }); | ||
|
|
||
| /** | ||
| * @param {string} zenkoBucket - target zenko bucket of the reader | ||
| * @return {object} the stubbed reader | ||
| */ | ||
| function createReaderMock(zenkoBucket) { | ||
| const logReader = sinon.createStubInstance(IngestionReader); | ||
| logReader.getTargetZenkoBucketName.returns(zenkoBucket); | ||
| return logReader; | ||
| } | ||
|
|
||
| it('should unregister a source whose reader is still pending setup', | ||
| () => { | ||
| const logReader = createReaderMock(PENDING_BUCKET); | ||
| populator._ingestionSources[PENDING_BUCKET] = logReader; | ||
| populator.logReadersUpdate = [logReader]; | ||
|
|
||
| populator._closeLogState(PENDING_BUCKET); | ||
|
|
||
| assert.deepStrictEqual(populator.logReadersUpdate, []); | ||
| assert.strictEqual(populator._ingestionSources[PENDING_BUCKET], | ||
| undefined); | ||
| // zookeeper state is only created once the setup succeeded | ||
| assert.strictEqual(removeReaderState.called, false); | ||
| }); | ||
|
|
||
| it('should unregister an active source and clean its zookeeper state', | ||
| () => { | ||
| const logReader = createReaderMock(ACTIVE_BUCKET); | ||
| populator._ingestionSources[ACTIVE_BUCKET] = logReader; | ||
| populator.logReaders = [logReader]; | ||
|
|
||
| populator._closeLogState(ACTIVE_BUCKET); | ||
|
|
||
| assert.deepStrictEqual(populator.logReaders, []); | ||
| assert.strictEqual(populator._ingestionSources[ACTIVE_BUCKET], | ||
| undefined); | ||
| // the reader is read before being unregistered, as | ||
| // `_removeReaderState` polls it until its batch completes | ||
| assert.strictEqual(removeReaderState.calledOnce, true); | ||
| assert.strictEqual(removeReaderState.firstCall.args[0], logReader); | ||
| }); | ||
|
|
||
| it('should unregister a source whose setup is still in flight', () => { | ||
| const logReader = createReaderMock(PENDING_BUCKET); | ||
| populator._ingestionSources[PENDING_BUCKET] = logReader; | ||
|
|
||
| populator._closeLogState(PENDING_BUCKET); | ||
|
|
||
| assert.strictEqual(populator._ingestionSources[PENDING_BUCKET], | ||
| undefined); | ||
| assert.strictEqual(removeReaderState.called, false); | ||
| }); | ||
|
|
||
| it('should not throw when the source is unknown', () => { | ||
| populator._closeLogState('never-configured-bucket'); | ||
|
|
||
| assert.deepStrictEqual(populator._ingestionSources, {}); | ||
| assert.strictEqual(removeReaderState.called, false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('_processLogReaderEntries', () => { | ||
| it('should skip when previous batch currently in progress', () => { | ||
| const logReaderMock = { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should add a log message (just info is enough), would help to troubleshoot if something ever happens in this corner case...