-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Reporting] Fix scroll timeout logging bug (#49111)
* [Reporting] Fix scroll timeout logging bug * test cancellation token * test time out
- Loading branch information
Showing
5 changed files
with
248 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/hit_iterator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import expect from '@kbn/expect'; | ||
import sinon from 'sinon'; | ||
import { CancellationToken } from '../../../../../common/cancellation_token'; | ||
import { Logger, ScrollConfig } from '../../../../../types'; | ||
import { createHitIterator } from '../hit_iterator'; | ||
|
||
const mockLogger = { | ||
error: new Function(), | ||
debug: new Function(), | ||
warning: new Function(), | ||
} as Logger; | ||
const debugLogStub = sinon.stub(mockLogger, 'debug'); | ||
const warnLogStub = sinon.stub(mockLogger, 'warning'); | ||
const errorLogStub = sinon.stub(mockLogger, 'error'); | ||
const mockCallEndpoint = sinon.stub(); | ||
const mockSearchRequest = {}; | ||
const mockConfig: ScrollConfig = { duration: '2s', size: 123 }; | ||
let realCancellationToken = new CancellationToken(); | ||
let isCancelledStub: sinon.SinonStub; | ||
|
||
describe('hitIterator', function() { | ||
beforeEach(() => { | ||
debugLogStub.resetHistory(); | ||
warnLogStub.resetHistory(); | ||
errorLogStub.resetHistory(); | ||
mockCallEndpoint.resetHistory(); | ||
mockCallEndpoint.resetBehavior(); | ||
mockCallEndpoint.resolves({ _scroll_id: '123blah', hits: { hits: ['you found me'] } }); | ||
mockCallEndpoint.onCall(11).resolves({ _scroll_id: '123blah', hits: {} }); | ||
|
||
isCancelledStub = sinon.stub(realCancellationToken, 'isCancelled'); | ||
isCancelledStub.returns(false); | ||
}); | ||
|
||
afterEach(() => { | ||
realCancellationToken = new CancellationToken(); | ||
}); | ||
|
||
it('iterates hits', async () => { | ||
// Begin | ||
const hitIterator = createHitIterator(mockLogger); | ||
const iterator = hitIterator( | ||
mockConfig, | ||
mockCallEndpoint, | ||
mockSearchRequest, | ||
realCancellationToken | ||
); | ||
|
||
while (true) { | ||
const { done: iterationDone, value: hit } = await iterator.next(); | ||
if (iterationDone) { | ||
break; | ||
} | ||
expect(hit).to.be('you found me'); | ||
} | ||
|
||
expect(mockCallEndpoint.callCount).to.be(13); | ||
expect(debugLogStub.callCount).to.be(13); | ||
expect(warnLogStub.callCount).to.be(0); | ||
expect(errorLogStub.callCount).to.be(0); | ||
}); | ||
|
||
it('stops searches after cancellation', async () => { | ||
// Setup | ||
isCancelledStub.onFirstCall().returns(false); | ||
isCancelledStub.returns(true); | ||
|
||
// Begin | ||
const hitIterator = createHitIterator(mockLogger); | ||
const iterator = hitIterator( | ||
mockConfig, | ||
mockCallEndpoint, | ||
mockSearchRequest, | ||
realCancellationToken | ||
); | ||
|
||
while (true) { | ||
const { done: iterationDone, value: hit } = await iterator.next(); | ||
if (iterationDone) { | ||
break; | ||
} | ||
expect(hit).to.be('you found me'); | ||
} | ||
|
||
expect(mockCallEndpoint.callCount).to.be(3); | ||
expect(debugLogStub.callCount).to.be(3); | ||
expect(warnLogStub.callCount).to.be(1); | ||
expect(errorLogStub.callCount).to.be(0); | ||
|
||
expect(warnLogStub.firstCall.lastArg).to.be( | ||
'Any remaining scrolling searches have been cancelled by the cancellation token.' | ||
); | ||
}); | ||
|
||
it('handles time out', async () => { | ||
// Setup | ||
mockCallEndpoint.onCall(2).resolves({ status: 404 }); | ||
|
||
// Begin | ||
const hitIterator = createHitIterator(mockLogger); | ||
const iterator = hitIterator( | ||
mockConfig, | ||
mockCallEndpoint, | ||
mockSearchRequest, | ||
realCancellationToken | ||
); | ||
|
||
let errorThrown = false; | ||
try { | ||
while (true) { | ||
const { done: iterationDone, value: hit } = await iterator.next(); | ||
if (iterationDone) { | ||
break; | ||
} | ||
expect(hit).to.be('you found me'); | ||
} | ||
} catch (err) { | ||
expect(err).to.eql( | ||
new Error('Expected _scroll_id in the following Elasticsearch response: {"status":404}') | ||
); | ||
errorThrown = true; | ||
} | ||
|
||
expect(mockCallEndpoint.callCount).to.be(4); | ||
expect(debugLogStub.callCount).to.be(4); | ||
expect(warnLogStub.callCount).to.be(0); | ||
expect(errorLogStub.callCount).to.be(1); | ||
expect(errorThrown).to.be(true); | ||
}); | ||
}); |
71 changes: 0 additions & 71 deletions
71
x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.js
This file was deleted.
Oops, something went wrong.
97 changes: 97 additions & 0 deletions
97
x-pack/legacy/plugins/reporting/export_types/csv/server/lib/hit_iterator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
import { SearchParams, SearchResponse } from 'elasticsearch'; | ||
|
||
import { i18n } from '@kbn/i18n'; | ||
import { CancellationToken, ScrollConfig, Logger } from '../../../../types'; | ||
|
||
async function parseResponse(request: SearchResponse<any>) { | ||
const response = await request; | ||
if (!response || !response._scroll_id) { | ||
throw new Error( | ||
i18n.translate('xpack.reporting.exportTypes.csv.hitIterator.expectedScrollIdErrorMessage', { | ||
defaultMessage: 'Expected {scrollId} in the following Elasticsearch response: {response}', | ||
values: { response: JSON.stringify(response), scrollId: '_scroll_id' }, | ||
}) | ||
); | ||
} | ||
|
||
if (!response.hits) { | ||
throw new Error( | ||
i18n.translate('xpack.reporting.exportTypes.csv.hitIterator.expectedHitsErrorMessage', { | ||
defaultMessage: 'Expected {hits} in the following Elasticsearch response: {response}', | ||
values: { response: JSON.stringify(response), hits: 'hits' }, | ||
}) | ||
); | ||
} | ||
|
||
return { | ||
scrollId: response._scroll_id, | ||
hits: response.hits.hits, | ||
}; | ||
} | ||
|
||
export function createHitIterator(logger: Logger) { | ||
return async function* hitIterator( | ||
scrollSettings: ScrollConfig, | ||
callEndpoint: Function, | ||
searchRequest: SearchParams, | ||
cancellationToken: CancellationToken | ||
) { | ||
logger.debug('executing search request'); | ||
function search(index: string | boolean | string[] | undefined, body: object) { | ||
return parseResponse( | ||
callEndpoint('search', { | ||
index, | ||
body, | ||
scroll: scrollSettings.duration, | ||
size: scrollSettings.size, | ||
}) | ||
); | ||
} | ||
|
||
function scroll(scrollId: string | undefined) { | ||
logger.debug('executing scroll request'); | ||
return parseResponse( | ||
callEndpoint('scroll', { | ||
scrollId, | ||
scroll: scrollSettings.duration, | ||
}) | ||
); | ||
} | ||
|
||
function clearScroll(scrollId: string | undefined) { | ||
logger.debug('executing clearScroll request'); | ||
return callEndpoint('clearScroll', { | ||
scrollId: [scrollId], | ||
}); | ||
} | ||
|
||
try { | ||
let { scrollId, hits } = await search(searchRequest.index, searchRequest.body); | ||
try { | ||
while (hits && hits.length && !cancellationToken.isCancelled()) { | ||
for (const hit of hits) { | ||
yield hit; | ||
} | ||
|
||
({ scrollId, hits } = await scroll(scrollId)); | ||
|
||
if (cancellationToken.isCancelled()) { | ||
logger.warning( | ||
'Any remaining scrolling searches have been cancelled by the cancellation token.' | ||
); | ||
} | ||
} | ||
} finally { | ||
await clearScroll(scrollId); | ||
} | ||
} catch (err) { | ||
logger.error(err); | ||
throw err; | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters