@@ -21,6 +21,7 @@ public class BulkAllObservable<T> : IDisposable, IObservable<IBulkAllResponse> w
2121 private readonly CancellationToken _compositeCancelToken ;
2222 private readonly CancellationTokenSource _compositeCancelTokenSource ;
2323 private readonly Func < IBulkResponseItem , T , bool > _retryPredicate ;
24+ private Action < IBulkResponseItem , T > _droppedDocumentCallBack ;
2425
2526 public BulkAllObservable (
2627 IElasticClient client ,
@@ -34,6 +35,7 @@ public BulkAllObservable(
3435 this . _backOffTime = ( this . _partionedBulkRequest ? . BackOffTime ? . ToTimeSpan ( ) ?? CoordinatedRequestDefaults . BulkAllBackOffTimeDefault ) ;
3536 this . _bulkSize = this . _partionedBulkRequest . Size ?? CoordinatedRequestDefaults . BulkAllSizeDefault ;
3637 this . _retryPredicate = this . _partionedBulkRequest . RetryDocumentPredicate ?? RetryBulkActionPredicate ;
38+ this . _droppedDocumentCallBack = this . _partionedBulkRequest . DroppedDocumentCallback ?? DroppedDocumentCallbackDefault ;
3739 this . _maxDegreeOfParallelism = _partionedBulkRequest . MaxDegreeOfParallelism ?? CoordinatedRequestDefaults . BulkAllMaxDegreeOfParallelismDefault ;
3840 this . _compositeCancelTokenSource = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
3941 this . _compositeCancelToken = this . _compositeCancelTokenSource . Token ;
@@ -53,9 +55,6 @@ public IDisposable Subscribe(IObserver<IBulkAllResponse> observer)
5355 return this ;
5456 }
5557
56- private static ElasticsearchClientException Throw ( string message , IApiCallDetails details ) =>
57- new ElasticsearchClientException ( PipelineFailure . BadResponse , message , details ) ;
58-
5958 private void BulkAll ( IObserver < IBulkAllResponse > observer )
6059 {
6160 var documents = this . _partionedBulkRequest . Documents ;
@@ -116,29 +115,85 @@ private async Task<IBulkAllResponse> BulkAsync(IList<T> buffer, long page, int b
116115
117116 this . _compositeCancelToken . ThrowIfCancellationRequested ( ) ;
118117
119- var retryDocuments = response . Items . Zip ( buffer , ( i , d ) => new { i , d } )
120- . Where ( x=> ! response . IsValid && this . _retryPredicate ( x . i , x . d ) )
121- . Select ( x => x . d )
118+ if ( ! response . ApiCall . Success )
119+ return await this . HandleBulkRequest ( buffer , page , backOffRetries , response ) ;
120+
121+ var documentsWithResponse = response . Items . Zip ( buffer , Tuple . Create ) . ToList ( ) ;
122+
123+ this . HandleDroppedDocuments ( documentsWithResponse , response ) ;
124+
125+ var retryDocuments = documentsWithResponse
126+ . Where ( x=> ! response . IsValid && this . _retryPredicate ( x . Item1 , x . Item2 ) )
127+ . Select ( x => x . Item2 )
122128 . ToList ( ) ;
123129
124130 if ( retryDocuments . Count > 0 && backOffRetries < this . _backOffRetries )
131+ return await this . RetryDocuments ( page , ++ backOffRetries , retryDocuments ) ;
132+ else if ( retryDocuments . Count > 0 )
133+ throw this . ThrowOnBadBulk ( response , $ "Bulk indexing failed and after retrying { backOffRetries } times") ;
134+
135+ this . _partionedBulkRequest . BackPressure ? . Release ( ) ;
136+ return new BulkAllResponse { Retries = backOffRetries , Page = page } ;
137+ }
138+
139+ private void HandleDroppedDocuments ( List < Tuple < IBulkResponseItem , T > > documentsWithResponse , IBulkResponse response )
140+ {
141+ var droppedDocuments = documentsWithResponse
142+ . Where ( x => ! response . IsValid && ! this . _retryPredicate ( x . Item1 , x . Item2 ) )
143+ . ToList ( ) ;
144+ if ( droppedDocuments . Count > 0 && ! this . _partionedBulkRequest . ContinueAfterDroppedDocuments )
145+ throw this . ThrowOnBadBulk ( response , $ "BulkAll halted after receiving failures that can not be retried from _bulk") ;
146+ else if ( droppedDocuments . Count > 0 && this . _partionedBulkRequest . ContinueAfterDroppedDocuments )
125147 {
126- this . _incrementRetries ( ) ;
127- await Task . Delay ( this . _backOffTime , this . _compositeCancelToken ) . ConfigureAwait ( false ) ;
128- return await this . BulkAsync ( retryDocuments , page , ++ backOffRetries ) . ConfigureAwait ( false ) ;
148+ foreach ( var dropped in droppedDocuments ) this . _droppedDocumentCallBack ( dropped . Item1 , dropped . Item2 ) ;
129149 }
130- if ( retryDocuments . Count > 0 )
150+ }
151+
152+ private async Task < IBulkAllResponse > HandleBulkRequest ( IList < T > buffer , long page , int backOffRetries , IBulkResponse response )
153+ {
154+ var clientException = response . ApiCall . OriginalException as ElasticsearchClientException ;
155+ //TODO expose this on IAPiCallDetails as RetryLater in 7.0?
156+ var failureReason = clientException ? . FailureReason . GetValueOrDefault ( PipelineFailure . Unexpected ) ;
157+ switch ( failureReason )
131158 {
132- this . _incrementFailed ( ) ;
133- this . _partionedBulkRequest . BackPressure ? . Release ( ) ;
134- throw Throw ( $ "Bulk indexing failed and after retrying { backOffRetries } times", response . ApiCall ) ;
159+ case PipelineFailure . MaxRetriesReached :
160+ //TODO move this to its own PipelineFailure classification in 7.0
161+ if ( response . ApiCall . AuditTrail . Last ( ) . Event == AuditEvent . FailedOverAllNodes )
162+ throw this . ThrowOnBadBulk ( response , $ "BulkAll halted after attempted bulk failed over all the active nodes") ;
163+ return await this . RetryDocuments ( page , ++ backOffRetries , buffer ) ;
164+ case PipelineFailure . CouldNotStartSniffOnStartup :
165+ case PipelineFailure . BadAuthentication :
166+ case PipelineFailure . NoNodesAttempted :
167+ case PipelineFailure . SniffFailure :
168+ case PipelineFailure . Unexpected :
169+ throw this . ThrowOnBadBulk ( response ,
170+ $ "BulkAll halted after { nameof ( PipelineFailure ) } { failureReason . GetStringValue ( ) } from _bulk") ;
171+ default :
172+ return await this . RetryDocuments ( page , ++ backOffRetries , buffer ) ;
135173 }
174+ }
175+
176+ private async Task < IBulkAllResponse > RetryDocuments ( long page , int backOffRetries , IList < T > retryDocuments )
177+ {
178+ this . _incrementRetries ( ) ;
179+ await Task . Delay ( this . _backOffTime , this . _compositeCancelToken ) . ConfigureAwait ( false ) ;
180+ return await this . BulkAsync ( retryDocuments , page , backOffRetries ) . ConfigureAwait ( false ) ;
181+ }
182+
183+ private Exception ThrowOnBadBulk ( IElasticsearchResponse response , string message )
184+ {
185+ this . _incrementFailed ( ) ;
136186 this . _partionedBulkRequest . BackPressure ? . Release ( ) ;
137- return new BulkAllResponse { Retries = backOffRetries , Page = page } ;
187+ return Throw ( message , response . ApiCall ) ;
138188 }
189+ private static ElasticsearchClientException Throw ( string message , IApiCallDetails details ) =>
190+ new ElasticsearchClientException ( PipelineFailure . BadResponse , message , details ) ;
191+
139192
140193 private static bool RetryBulkActionPredicate ( IBulkResponseItem bulkResponseItem , T d ) => bulkResponseItem . Status == 429 ;
141194
195+ private static void DroppedDocumentCallbackDefault ( IBulkResponseItem bulkResponseItem , T d ) { }
196+
142197 public bool IsDisposed { get ; private set ; }
143198
144199 public void Dispose ( )
0 commit comments