|
18 | 18 |
|
19 | 19 | import com.mongodb.MongoChangeStreamException;
|
20 | 20 | import com.mongodb.MongoException;
|
| 21 | +import com.mongodb.MongoOperationTimeoutException; |
21 | 22 | import com.mongodb.ServerAddress;
|
22 | 23 | import com.mongodb.ServerCursor;
|
| 24 | +import com.mongodb.internal.TimeoutContext; |
23 | 25 | import com.mongodb.internal.binding.ReadBinding;
|
24 | 26 | import com.mongodb.lang.Nullable;
|
25 | 27 | import org.bson.BsonDocument;
|
|
37 | 39 | import static com.mongodb.internal.operation.ChangeStreamBatchCursorHelper.isResumableError;
|
38 | 40 | import static com.mongodb.internal.operation.SyncOperationHelper.withReadConnectionSource;
|
39 | 41 |
|
| 42 | +/** |
| 43 | + * A change stream cursor that wraps {@link CommandBatchCursor} with automatic resumption capabilities in the event |
| 44 | + * of timeouts or transient errors. |
| 45 | + * <p> |
| 46 | + * Upon encountering a resumable error during {@code hasNext()}, {@code next()}, or {@code tryNext()} calls, the {@link ChangeStreamBatchCursor} |
| 47 | + * attempts to establish a new change stream on the server. |
| 48 | + * </p> |
| 49 | + * If an error occurring during any of these method calls is not resumable, it is immediately propagated to the caller, and the {@link ChangeStreamBatchCursor} |
| 50 | + * is closed and invalidated on the server. Server errors that occur during this invalidation process are not propagated to the caller. |
| 51 | + * <p> |
| 52 | + * A {@link MongoOperationTimeoutException} does not invalidate the {@link ChangeStreamBatchCursor}, but is immediately propagated to the caller. |
| 53 | + * Subsequent method call will attempt to resume operation by establishing a new change stream on the server, without doing {@code getMore} |
| 54 | + * request first. |
| 55 | + * </p> |
| 56 | + */ |
40 | 57 | final class ChangeStreamBatchCursor<T> implements AggregateResponseBatchCursor<T> {
|
41 | 58 | private final ReadBinding binding;
|
42 | 59 | private final ChangeStreamOperation<T> changeStreamOperation;
|
43 | 60 | private final int maxWireVersion;
|
44 |
| - |
| 61 | + private final TimeoutContext timeoutContext; |
45 | 62 | private CommandBatchCursor<RawBsonDocument> wrapped;
|
46 | 63 | private BsonDocument resumeToken;
|
47 | 64 | private final AtomicBoolean closed;
|
48 | 65 |
|
| 66 | + /** |
| 67 | + * This flag is used to manage change stream resumption logic after a timeout error. |
| 68 | + * Indicates whether the last {@code hasNext()}, {@code next()}, or {@code tryNext()} call resulted in a {@link MongoOperationTimeoutException}. |
| 69 | + * If {@code true}, indicates a timeout occurred, prompting an attempt to resume the change stream on the subsequent call. |
| 70 | + */ |
| 71 | + private boolean lastOperationTimedOut; |
| 72 | + |
49 | 73 | ChangeStreamBatchCursor(final ChangeStreamOperation<T> changeStreamOperation,
|
50 | 74 | final CommandBatchCursor<RawBsonDocument> wrapped,
|
51 | 75 | final ReadBinding binding,
|
52 | 76 | @Nullable final BsonDocument resumeToken,
|
53 | 77 | final int maxWireVersion) {
|
| 78 | + this.timeoutContext = binding.getOperationContext().getTimeoutContext(); |
54 | 79 | this.changeStreamOperation = changeStreamOperation;
|
55 | 80 | this.binding = binding.retain();
|
56 | 81 | this.wrapped = wrapped;
|
57 | 82 | this.resumeToken = resumeToken;
|
58 | 83 | this.maxWireVersion = maxWireVersion;
|
59 | 84 | closed = new AtomicBoolean();
|
| 85 | + lastOperationTimedOut = false; |
60 | 86 | }
|
61 | 87 |
|
62 | 88 | CommandBatchCursor<RawBsonDocument> getWrapped() {
|
@@ -107,6 +133,7 @@ public List<T> tryNext() {
|
107 | 133 | @Override
|
108 | 134 | public void close() {
|
109 | 135 | if (!closed.getAndSet(true)) {
|
| 136 | + resetTimeout(); |
110 | 137 | wrapped.close();
|
111 | 138 | binding.release();
|
112 | 139 | }
|
@@ -184,22 +211,56 @@ static <T> List<T> convertAndProduceLastId(final List<RawBsonDocument> rawDocume
|
184 | 211 | }
|
185 | 212 |
|
186 | 213 | <R> R resumeableOperation(final Function<AggregateResponseBatchCursor<RawBsonDocument>, R> function) {
|
| 214 | + resetTimeout(); |
| 215 | + try { |
| 216 | + R result = execute(function); |
| 217 | + lastOperationTimedOut = false; |
| 218 | + return result; |
| 219 | + } catch (Throwable exception) { |
| 220 | + lastOperationTimedOut = isTimeoutException(exception); |
| 221 | + throw exception; |
| 222 | + } |
| 223 | + } |
| 224 | + |
| 225 | + private <R> R execute(final Function<AggregateResponseBatchCursor<RawBsonDocument>, R> function) { |
| 226 | + boolean shouldBeResumed = hasPreviousNextTimedOut(); |
187 | 227 | while (true) {
|
| 228 | + if (shouldBeResumed) { |
| 229 | + resumeChangeStream(); |
| 230 | + } |
188 | 231 | try {
|
189 | 232 | return function.apply(wrapped);
|
190 | 233 | } catch (Throwable t) {
|
191 | 234 | if (!isResumableError(t, maxWireVersion)) {
|
192 | 235 | throw MongoException.fromThrowableNonNull(t);
|
193 | 236 | }
|
| 237 | + shouldBeResumed = true; |
194 | 238 | }
|
195 |
| - wrapped.close(); |
| 239 | + } |
| 240 | + } |
| 241 | + |
| 242 | + private void resumeChangeStream() { |
| 243 | + wrapped.close(); |
| 244 | + |
| 245 | + withReadConnectionSource(binding, source -> { |
| 246 | + changeStreamOperation.setChangeStreamOptionsForResume(resumeToken, source.getServerDescription().getMaxWireVersion()); |
| 247 | + return null; |
| 248 | + }); |
| 249 | + wrapped = ((ChangeStreamBatchCursor<T>) changeStreamOperation.execute(binding)).getWrapped(); |
| 250 | + binding.release(); // release the new change stream batch cursor's reference to the binding |
| 251 | + } |
| 252 | + |
| 253 | + private boolean hasPreviousNextTimedOut() { |
| 254 | + return lastOperationTimedOut && !closed.get(); |
| 255 | + } |
196 | 256 |
|
197 |
| - withReadConnectionSource(binding, source -> { |
198 |
| - changeStreamOperation.setChangeStreamOptionsForResume(resumeToken, source.getServerDescription().getMaxWireVersion()); |
199 |
| - return null; |
200 |
| - }); |
201 |
| - wrapped = ((ChangeStreamBatchCursor<T>) changeStreamOperation.execute(binding)).getWrapped(); |
202 |
| - binding.release(); // release the new change stream batch cursor's reference to the binding |
| 257 | + private void resetTimeout() { |
| 258 | + if (timeoutContext.hasTimeoutMS()) { |
| 259 | + timeoutContext.resetTimeout(); |
203 | 260 | }
|
204 | 261 | }
|
| 262 | + |
| 263 | + private static boolean isTimeoutException(final Throwable exception) { |
| 264 | + return exception instanceof MongoOperationTimeoutException; |
| 265 | + } |
205 | 266 | }
|
0 commit comments