44 * you may not use this file except in compliance with the Elastic License.
55 */
66
7- import { LegacyClusterClient , Logger } from 'src/core/server' ;
7+ import { LegacyClusterClient } from 'src/core/server' ;
88import { elasticsearchServiceMock , loggingSystemMock } from 'src/core/server/mocks' ;
9- import { ClusterClientAdapter , IClusterClientAdapter } from './cluster_client_adapter' ;
9+ import {
10+ ClusterClientAdapter ,
11+ IClusterClientAdapter ,
12+ EVENT_BUFFER_LENGTH ,
13+ } from './cluster_client_adapter' ;
14+ import { contextMock } from './context.mock' ;
1015import { findOptionsSchema } from '../event_log_client' ;
16+ import { delay } from '../lib/delay' ;
17+ import { times } from 'lodash' ;
1118
1219type EsClusterClient = Pick < jest . Mocked < LegacyClusterClient > , 'callAsInternalUser' | 'asScoped' > ;
20+ type MockedLogger = ReturnType < typeof loggingSystemMock [ 'createLogger' ] > ;
1321
14- let logger : Logger ;
22+ let logger : MockedLogger ;
1523let clusterClient : EsClusterClient ;
1624let clusterClientAdapter : IClusterClientAdapter ;
1725
@@ -21,22 +29,130 @@ beforeEach(() => {
2129 clusterClientAdapter = new ClusterClientAdapter ( {
2230 logger,
2331 clusterClientPromise : Promise . resolve ( clusterClient ) ,
32+ context : contextMock . create ( ) ,
2433 } ) ;
2534} ) ;
2635
2736describe ( 'indexDocument' , ( ) => {
28- test ( 'should call cluster client with given doc' , async ( ) => {
29- await clusterClientAdapter . indexDocument ( { args : true } ) ;
30- expect ( clusterClient . callAsInternalUser ) . toHaveBeenCalledWith ( 'index' , {
31- args : true ,
37+ test ( 'should call cluster client bulk with given doc' , async ( ) => {
38+ clusterClientAdapter . indexDocument ( { body : { message : 'foo' } , index : 'event-log' } ) ;
39+
40+ await retryUntil ( 'cluster client bulk called' , ( ) => {
41+ return clusterClient . callAsInternalUser . mock . calls . length !== 0 ;
42+ } ) ;
43+
44+ expect ( clusterClient . callAsInternalUser ) . toHaveBeenCalledWith ( 'bulk' , {
45+ body : [ { create : { _index : 'event-log' } } , { message : 'foo' } ] ,
3246 } ) ;
3347 } ) ;
3448
35- test ( 'should throw error when cluster client throws an error' , async ( ) => {
36- clusterClient . callAsInternalUser . mockRejectedValue ( new Error ( 'Fail' ) ) ;
37- await expect (
38- clusterClientAdapter . indexDocument ( { args : true } )
39- ) . rejects . toThrowErrorMatchingInlineSnapshot ( `"Fail"` ) ;
49+ test ( 'should log an error when cluster client throws an error' , async ( ) => {
50+ clusterClient . callAsInternalUser . mockRejectedValue ( new Error ( 'expected failure' ) ) ;
51+ clusterClientAdapter . indexDocument ( { body : { message : 'foo' } , index : 'event-log' } ) ;
52+ await retryUntil ( 'cluster client bulk called' , ( ) => {
53+ return logger . error . mock . calls . length !== 0 ;
54+ } ) ;
55+
56+ const expectedMessage = `error writing bulk events: "expected failure"; docs: [{"create":{"_index":"event-log"}},{"message":"foo"}]` ;
57+ expect ( logger . error ) . toHaveBeenCalledWith ( expectedMessage ) ;
58+ } ) ;
59+ } ) ;
60+
61+ describe ( 'shutdown()' , ( ) => {
62+ test ( 'should work if no docs have been written' , async ( ) => {
63+ const result = await clusterClientAdapter . shutdown ( ) ;
64+ expect ( result ) . toBeFalsy ( ) ;
65+ } ) ;
66+
67+ test ( 'should work if some docs have been written' , async ( ) => {
68+ clusterClientAdapter . indexDocument ( { body : { message : 'foo' } , index : 'event-log' } ) ;
69+ const resultPromise = clusterClientAdapter . shutdown ( ) ;
70+
71+ await retryUntil ( 'cluster client bulk called' , ( ) => {
72+ return clusterClient . callAsInternalUser . mock . calls . length !== 0 ;
73+ } ) ;
74+
75+ const result = await resultPromise ;
76+ expect ( result ) . toBeFalsy ( ) ;
77+ } ) ;
78+ } ) ;
79+
80+ describe ( 'buffering documents' , ( ) => {
81+ test ( 'should write buffered docs after timeout' , async ( ) => {
82+ // write EVENT_BUFFER_LENGTH - 1 docs
83+ for ( let i = 0 ; i < EVENT_BUFFER_LENGTH - 1 ; i ++ ) {
84+ clusterClientAdapter . indexDocument ( { body : { message : `foo ${ i } ` } , index : 'event-log' } ) ;
85+ }
86+
87+ await retryUntil ( 'cluster client bulk called' , ( ) => {
88+ return clusterClient . callAsInternalUser . mock . calls . length !== 0 ;
89+ } ) ;
90+
91+ const expectedBody = [ ] ;
92+ for ( let i = 0 ; i < EVENT_BUFFER_LENGTH - 1 ; i ++ ) {
93+ expectedBody . push ( { create : { _index : 'event-log' } } , { message : `foo ${ i } ` } ) ;
94+ }
95+
96+ expect ( clusterClient . callAsInternalUser ) . toHaveBeenCalledWith ( 'bulk' , {
97+ body : expectedBody ,
98+ } ) ;
99+ } ) ;
100+
101+ test ( 'should write buffered docs after buffer exceeded' , async ( ) => {
102+ // write EVENT_BUFFER_LENGTH + 1 docs
103+ for ( let i = 0 ; i < EVENT_BUFFER_LENGTH + 1 ; i ++ ) {
104+ clusterClientAdapter . indexDocument ( { body : { message : `foo ${ i } ` } , index : 'event-log' } ) ;
105+ }
106+
107+ await retryUntil ( 'cluster client bulk called' , ( ) => {
108+ return clusterClient . callAsInternalUser . mock . calls . length >= 2 ;
109+ } ) ;
110+
111+ const expectedBody = [ ] ;
112+ for ( let i = 0 ; i < EVENT_BUFFER_LENGTH ; i ++ ) {
113+ expectedBody . push ( { create : { _index : 'event-log' } } , { message : `foo ${ i } ` } ) ;
114+ }
115+
116+ expect ( clusterClient . callAsInternalUser ) . toHaveBeenNthCalledWith ( 1 , 'bulk' , {
117+ body : expectedBody ,
118+ } ) ;
119+
120+ expect ( clusterClient . callAsInternalUser ) . toHaveBeenNthCalledWith ( 2 , 'bulk' , {
121+ body : [ { create : { _index : 'event-log' } } , { message : `foo 100` } ] ,
122+ } ) ;
123+ } ) ;
124+
125+ test ( 'should handle lots of docs correctly with a delay in the bulk index' , async ( ) => {
126+ // @ts -ignore
127+ clusterClient . callAsInternalUser . mockImplementation = async ( ) => await delay ( 100 ) ;
128+
129+ const docs = times ( EVENT_BUFFER_LENGTH * 10 , ( i ) => ( {
130+ body : { message : `foo ${ i } ` } ,
131+ index : 'event-log' ,
132+ } ) ) ;
133+
134+ // write EVENT_BUFFER_LENGTH * 10 docs
135+ for ( const doc of docs ) {
136+ clusterClientAdapter . indexDocument ( doc ) ;
137+ }
138+
139+ await retryUntil ( 'cluster client bulk called' , ( ) => {
140+ return clusterClient . callAsInternalUser . mock . calls . length >= 10 ;
141+ } ) ;
142+
143+ for ( let i = 0 ; i < 10 ; i ++ ) {
144+ const expectedBody = [ ] ;
145+ for ( let j = 0 ; j < EVENT_BUFFER_LENGTH ; j ++ ) {
146+ expectedBody . push (
147+ { create : { _index : 'event-log' } } ,
148+ { message : `foo ${ i * EVENT_BUFFER_LENGTH + j } ` }
149+ ) ;
150+ }
151+
152+ expect ( clusterClient . callAsInternalUser ) . toHaveBeenNthCalledWith ( i + 1 , 'bulk' , {
153+ body : expectedBody ,
154+ } ) ;
155+ }
40156 } ) ;
41157} ) ;
42158
@@ -575,3 +691,29 @@ describe('queryEventsBySavedObject', () => {
575691 ` ) ;
576692 } ) ;
577693} ) ;
694+
695+ type RetryableFunction = ( ) => boolean ;
696+
697+ const RETRY_UNTIL_DEFAULT_COUNT = 20 ;
698+ const RETRY_UNTIL_DEFAULT_WAIT = 1000 ; // milliseconds
699+
700+ async function retryUntil (
701+ label : string ,
702+ fn : RetryableFunction ,
703+ count : number = RETRY_UNTIL_DEFAULT_COUNT ,
704+ wait : number = RETRY_UNTIL_DEFAULT_WAIT
705+ ) : Promise < boolean > {
706+ while ( count > 0 ) {
707+ count -- ;
708+
709+ if ( fn ( ) ) return true ;
710+
711+ // eslint-disable-next-line no-console
712+ console . log ( `attempt failed waiting for "${ label } ", attempts left: ${ count } ` ) ;
713+
714+ if ( count === 0 ) return false ;
715+ await delay ( wait ) ;
716+ }
717+
718+ return false ;
719+ }
0 commit comments