3232import static com .mongodb .kafka .connect .util .ConfigHelper .getMongoDriverInformation ;
3333import static com .mongodb .kafka .connect .util .ServerApiConfig .setServerApi ;
3434import static java .lang .String .format ;
35+ import static java .util .Arrays .asList ;
3536import static java .util .Collections .singletonMap ;
3637
3738import java .util .ArrayList ;
3839import java .util .Collections ;
3940import java .util .HashMap ;
41+ import java .util .HashSet ;
4042import java .util .List ;
4143import java .util .Locale ;
4244import java .util .Map ;
4345import java .util .Optional ;
46+ import java .util .Set ;
4447import java .util .concurrent .atomic .AtomicBoolean ;
4548import java .util .function .Supplier ;
4649
6366import com .mongodb .ConnectionString ;
6467import com .mongodb .MongoClientSettings ;
6568import com .mongodb .MongoCommandException ;
69+ import com .mongodb .MongoException ;
6670import com .mongodb .client .ChangeStreamIterable ;
6771import com .mongodb .client .MongoChangeStreamCursor ;
6872import com .mongodb .client .MongoClient ;
@@ -119,12 +123,24 @@ public final class MongoSourceTask extends SourceTask {
119123 private static final int NAMESPACE_NOT_FOUND_ERROR = 26 ;
120124 private static final int ILLEGAL_OPERATION_ERROR = 20 ;
121125 private static final int INVALIDATED_RESUME_TOKEN_ERROR = 260 ;
126+ private static final int CHANGE_STREAM_FATAL_ERROR = 280 ;
127+ private static final int CHANGE_STREAM_HISTORY_LOST = 286 ;
128+ private static final int BSON_OBJECT_TOO_LARGE = 10334 ;
129+ private static final Set <Integer > INVALID_CHANGE_STREAM_ERRORS =
130+ new HashSet <>(
131+ asList (
132+ INVALIDATED_RESUME_TOKEN_ERROR ,
133+ CHANGE_STREAM_FATAL_ERROR ,
134+ CHANGE_STREAM_HISTORY_LOST ,
135+ BSON_OBJECT_TOO_LARGE ));
122136 private static final int UNKNOWN_FIELD_ERROR = 40415 ;
123137 private static final int FAILED_TO_PARSE_ERROR = 9 ;
124138 private static final String RESUME_TOKEN = "resume token" ;
139+ private static final String RESUME_POINT = "resume point" ;
125140 private static final String NOT_FOUND = "not found" ;
126141 private static final String DOES_NOT_EXIST = "does not exist" ;
127142 private static final String INVALID_RESUME_TOKEN = "invalid resume token" ;
143+ private static final String NO_LONGER_IN_THE_OPLOG = "no longer be in the oplog" ;
128144
129145 private final Time time ;
130146 private final AtomicBoolean isRunning = new AtomicBoolean ();
@@ -366,6 +382,28 @@ MongoChangeStreamCursor<? extends BsonDocument> createCursor(
366382 return tryCreateCursor (sourceConfig , mongoClient , getResumeToken (sourceConfig ));
367383 }
368384
385+ private MongoChangeStreamCursor <? extends BsonDocument > tryRecreateCursor (
386+ final MongoException e ) {
387+ int errorCode =
388+ e instanceof MongoCommandException
389+ ? ((MongoCommandException ) e ).getErrorCode ()
390+ : e .getCode ();
391+ String errorMessage =
392+ e instanceof MongoCommandException
393+ ? ((MongoCommandException ) e ).getErrorMessage ()
394+ : e .getMessage ();
395+ LOGGER .warn (
396+ "Failed to resume change stream: {} {}\n "
397+ + "===================================================================================\n "
398+ + "When the resume token is no longer available there is the potential for data loss.\n \n "
399+ + "Restarting the change stream with no resume token because `errors.tolerance=all`.\n "
400+ + "===================================================================================\n " ,
401+ errorMessage ,
402+ errorCode );
403+ invalidatedCursor = true ;
404+ return tryCreateCursor (sourceConfig , mongoClient , null );
405+ }
406+
369407 private MongoChangeStreamCursor <? extends BsonDocument > tryCreateCursor (
370408 final MongoSourceConfig sourceConfig ,
371409 final MongoClient mongoClient ,
@@ -394,17 +432,8 @@ private MongoChangeStreamCursor<? extends BsonDocument> tryCreateCursor(
394432 } else if (doesNotSupportsStartAfter (e )) {
395433 supportsStartAfter = false ;
396434 return tryCreateCursor (sourceConfig , mongoClient , resumeToken );
397- } else if (sourceConfig .tolerateErrors () && resumeTokenNotFound (e )) {
398- LOGGER .warn (
399- "Failed to resume change stream: {} {}\n "
400- + "===================================================================================\n "
401- + "When the resume token is no longer available there is the potential for data loss.\n \n "
402- + "Restarting the change stream with no resume token because `errors.tolerance=all`.\n "
403- + "===================================================================================\n " ,
404- e .getErrorMessage (),
405- e .getErrorCode ());
406- invalidatedCursor = true ;
407- return tryCreateCursor (sourceConfig , mongoClient , null );
435+ } else if (sourceConfig .tolerateErrors () && changeStreamNotValid (e )) {
436+ return tryRecreateCursor (e );
408437 }
409438 }
410439 if (e .getErrorCode () == NAMESPACE_NOT_FOUND_ERROR ) {
@@ -438,7 +467,7 @@ private MongoChangeStreamCursor<? extends BsonDocument> tryCreateCursor(
438467 + "=====================================================================================\n " ,
439468 e .getErrorMessage (),
440469 e .getErrorCode ());
441- if (resumeTokenNotFound (e )) {
470+ if (changeStreamNotValid (e )) {
442471 throw new ConnectException (
443472 "ResumeToken not found. Cannot create a change stream cursor" , e );
444473 }
@@ -456,12 +485,19 @@ private boolean invalidatedResumeToken(final MongoCommandException e) {
456485 return e .getErrorCode () == INVALIDATED_RESUME_TOKEN_ERROR ;
457486 }
458487
459- private boolean resumeTokenNotFound (final MongoCommandException e ) {
460- String errorMessage = e .getErrorMessage ().toLowerCase (Locale .ROOT );
461- return errorMessage .contains (RESUME_TOKEN )
488+ private boolean changeStreamNotValid (final MongoException e ) {
489+ if (INVALID_CHANGE_STREAM_ERRORS .contains (e .getCode ())) {
490+ return true ;
491+ }
492+ String errorMessage =
493+ e instanceof MongoCommandException
494+ ? ((MongoCommandException ) e ).getErrorMessage ().toLowerCase (Locale .ROOT )
495+ : e .getMessage ().toLowerCase (Locale .ROOT );
496+ return (errorMessage .contains (RESUME_TOKEN ) || errorMessage .contains (RESUME_POINT ))
462497 && (errorMessage .contains (NOT_FOUND )
463498 || errorMessage .contains (DOES_NOT_EXIST )
464- || errorMessage .contains (INVALID_RESUME_TOKEN ));
499+ || errorMessage .contains (INVALID_RESUME_TOKEN )
500+ || errorMessage .contains (NO_LONGER_IN_THE_OPLOG ));
465501 }
466502
467503 Map <String , Object > createPartitionMap (final MongoSourceConfig sourceConfig ) {
@@ -588,26 +624,38 @@ private Optional<BsonDocument> getNextDocument() {
588624 next = cursor != null ? cursor .tryNext () : null ;
589625 }
590626 return Optional .ofNullable (next );
591- } catch (Exception e ) {
592- if (cursor != null ) {
593- try {
594- cursor .close ();
595- } catch (Exception e1 ) {
596- // ignore
627+ } catch (MongoException e ) {
628+ closeCursor ();
629+ if (isRunning .get ()) {
630+ if (sourceConfig .tolerateErrors () && changeStreamNotValid (e )) {
631+ cursor = tryRecreateCursor (e );
632+ } else {
633+ LOGGER .info (
634+ "An exception occurred when trying to get the next item from the Change Stream" , e );
597635 }
598- cursor = null ;
599636 }
637+ return Optional .empty ();
638+ } catch (Exception e ) {
639+ closeCursor ();
600640 if (isRunning .get ()) {
601- LOGGER .info (
602- "An exception occurred when trying to get the next item from the Change Stream: {}" ,
603- e .getMessage ());
641+ throw new ConnectException ("Unexpected error: " + e .getMessage (), e );
604642 }
605- return Optional .empty ();
606643 }
607644 }
608645 return Optional .empty ();
609646 }
610647
648+ private void closeCursor () {
649+ if (cursor != null ) {
650+ try {
651+ cursor .close ();
652+ } catch (Exception e1 ) {
653+ // ignore
654+ }
655+ cursor = null ;
656+ }
657+ }
658+
611659 private void invalidateCursorAndReinitialize () {
612660 invalidatedCursor = true ;
613661 cursor .close ();
0 commit comments