Skip to content

Commit 77c8795

Browse files
stIncMalerozza
andauthored
Backport to 4.2.x JAVA-4044/PR#689 and the changes it depends on (JAVA-3938 & JAVA-3907 / PR#661) (#691)
* Regression test for change stream cancellation (#661) Ensures that all sessions are returned to the pool JAVA-3938 JAVA-3907 * Guarantee that ChangeStreamPublisher for a collection completes after dropping the collection (#689) Before the changes made within JAVA-3973, ChangeStreamPublisher had been terminating with onError. After the changes in JAVA-3973 neither onError nor onComplete is called, but those changes allow us to terminated it with onComplete. I could have specified only assertTerminalEvent() without specifying assertNoErrors(), thus accepting either onComplete or onError (the old behavior), but terminating with onComplete is nicer. The approach with using startAtOperationTime to ensure that a change stream is guaranteed to observe collection.drop() works only if there is no leader re-election that results in rolling back the delete operation from which the operationTime was extracted. While such rollback can be prevented by using the "majority" write concern, the common approach in driver tests is to not use it for efficiency and tolerate a tiny chance of experiencing a rollback. JAVA-4044 Co-authored-by: Ross Lawley <ross.lawley@gmail.com>
1 parent 1471dd2 commit 77c8795

File tree

7 files changed

+296
-34
lines changed

7 files changed

+296
-34
lines changed

driver-core/src/main/com/mongodb/assertions/Assertions.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,18 @@
1818
package com.mongodb.assertions;
1919

2020
import com.mongodb.internal.async.SingleResultCallback;
21+
import com.mongodb.lang.Nullable;
2122

2223
import java.util.Collection;
2324

2425
/**
2526
* <p>Design by contract assertions.</p> <p>This class is not part of the public API and may be removed or changed at any time.</p>
27+
* All {@code assert...} methods throw {@link AssertionError} and should be used to check conditions which may be violated if and only if
28+
* the driver code is incorrect. The intended usage of this methods is the same as of the
29+
* <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/language/assert.html">Java {@code assert} statement</a>. The reason
30+
* for not using the {@code assert} statements is that they are not always enabled. We prefer having internal checks always done at the
31+
* cost of our code doing a relatively small amount of additional work in production.
32+
* The {@code assert...} methods return values to open possibilities of being used fluently.
2633
*/
2734
public final class Assertions {
2835
/**
@@ -119,6 +126,57 @@ public static void doesNotContainNull(final String name, final Collection<?> col
119126
}
120127
}
121128

129+
/**
130+
* @param value A value to check.
131+
* @param <T> The type of {@code value}.
132+
* @return {@code null}.
133+
* @throws AssertionError If {@code value} is not {@code null}.
134+
*/
135+
@Nullable
136+
public static <T> T assertNull(@Nullable final T value) throws AssertionError {
137+
if (value != null) {
138+
throw new AssertionError(value.toString());
139+
}
140+
return null;
141+
}
142+
143+
/**
144+
* @param value A value to check.
145+
* @param <T> The type of {@code value}.
146+
* @return {@code value}
147+
* @throws AssertionError If {@code value} is {@code null}.
148+
*/
149+
public static <T> T assertNotNull(@Nullable final T value) throws AssertionError {
150+
if (value == null) {
151+
throw new AssertionError();
152+
}
153+
return value;
154+
}
155+
156+
/**
157+
* @param value A value to check.
158+
* @return {@code true}.
159+
* @throws AssertionError If {@code value} is {@code false}.
160+
*/
161+
public static boolean assertTrue(final boolean value) throws AssertionError {
162+
if (!value) {
163+
throw new AssertionError();
164+
}
165+
return true;
166+
}
167+
168+
/**
169+
* @param value A value to check.
170+
* @return {@code false}.
171+
* @throws AssertionError If {@code value} is {@code true}.
172+
*/
173+
public static boolean assertFalse(final boolean value) throws AssertionError {
174+
if (value) {
175+
throw new AssertionError();
176+
}
177+
return false;
178+
}
179+
122180
private Assertions() {
123181
}
124182
}

driver-core/src/main/com/mongodb/internal/async/AsyncBatchCursor.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,24 @@ public interface AsyncBatchCursor<T> extends Closeable {
6565
int getBatchSize();
6666

6767
/**
68-
* Return true if the AsyncBatchCursor has been closed
68+
* Implementations of {@link AsyncBatchCursor} are allowed to close themselves, see {@link #close()} for more details.
6969
*
70-
* @return true if the AsyncBatchCursor has been closed
70+
* @return {@code true} if {@code this} has been closed or has closed itself.
7171
*/
7272
boolean isClosed();
7373

74+
/**
75+
* Implementations of {@link AsyncBatchCursor} are allowed to close themselves synchronously via methods
76+
* {@link #next(SingleResultCallback)}/{@link #tryNext(SingleResultCallback)}.
77+
* Self-closing behavior is discouraged because it introduces an additional burden on code that uses {@link AsyncBatchCursor}.
78+
* To help making such code simpler, this method is required to be idempotent.
79+
* <p>
80+
* Another quirk is that this method is allowed to release resources "eventually",
81+
* i.e., not before (in the happens before order) returning.
82+
* Nevertheless, {@link #isClosed()} called after (in the happens-before order) {@link #close()} must return {@code true}.
83+
*
84+
* @see #close()
85+
*/
7486
@Override
7587
void close();
7688
}

driver-core/src/main/com/mongodb/internal/operation/AsyncChangeStreamBatchCursor.java

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@
2424
import com.mongodb.internal.binding.AsyncConnectionSource;
2525
import com.mongodb.internal.binding.AsyncReadBinding;
2626
import com.mongodb.internal.operation.OperationHelper.AsyncCallableWithSource;
27+
import com.mongodb.lang.NonNull;
2728
import org.bson.BsonDocument;
2829
import org.bson.BsonTimestamp;
2930
import org.bson.RawBsonDocument;
3031

3132
import java.util.ArrayList;
3233
import java.util.List;
3334
import java.util.concurrent.atomic.AtomicBoolean;
35+
import java.util.concurrent.atomic.AtomicReference;
3436

37+
import static com.mongodb.assertions.Assertions.assertNotNull;
38+
import static com.mongodb.assertions.Assertions.assertNull;
3539
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
3640
import static com.mongodb.internal.operation.ChangeStreamBatchCursorHelper.isRetryableError;
3741
import static com.mongodb.internal.operation.OperationHelper.LOGGER;
@@ -44,7 +48,12 @@ final class AsyncChangeStreamBatchCursor<T> implements AsyncAggregateResponseBat
4448
private final int maxWireVersion;
4549

4650
private volatile BsonDocument resumeToken;
47-
private volatile AsyncAggregateResponseBatchCursor<RawBsonDocument> wrapped;
51+
/**
52+
* {@linkplain ChangeStreamBatchCursorHelper#isRetryableError(Throwable, int) Retryable errors} can result in
53+
* {@code wrapped} containing {@code null} and {@link #isClosed} being {@code false}.
54+
* This represents a situation in which the wrapped object was closed by {@code this} but {@code this} remained open.
55+
*/
56+
private final AtomicReference<AsyncAggregateResponseBatchCursor<RawBsonDocument>> wrapped;
4857
private final AtomicBoolean isClosed;
4958

5059
AsyncChangeStreamBatchCursor(final ChangeStreamOperation<T> changeStreamOperation,
@@ -53,16 +62,17 @@ final class AsyncChangeStreamBatchCursor<T> implements AsyncAggregateResponseBat
5362
final BsonDocument resumeToken,
5463
final int maxWireVersion) {
5564
this.changeStreamOperation = changeStreamOperation;
56-
this.wrapped = wrapped;
65+
this.wrapped = new AtomicReference<>(assertNotNull(wrapped));
5766
this.binding = binding;
5867
binding.retain();
5968
this.resumeToken = resumeToken;
6069
this.maxWireVersion = maxWireVersion;
6170
isClosed = new AtomicBoolean();
6271
}
6372

73+
@NonNull
6474
AsyncAggregateResponseBatchCursor<RawBsonDocument> getWrapped() {
65-
return wrapped;
75+
return assertNotNull(wrapped.get());
6676
}
6777

6878
@Override
@@ -93,7 +103,7 @@ public void apply(final AsyncAggregateResponseBatchCursor<RawBsonDocument> curso
93103
public void close() {
94104
if (isClosed.compareAndSet(false, true)) {
95105
try {
96-
wrapped.close();
106+
nullifyAndCloseWrapped();
97107
} finally {
98108
binding.release();
99109
}
@@ -102,22 +112,63 @@ public void close() {
102112

103113
@Override
104114
public void setBatchSize(final int batchSize) {
105-
wrapped.setBatchSize(batchSize);
115+
getWrapped().setBatchSize(batchSize);
106116
}
107117

108118
@Override
109119
public int getBatchSize() {
110-
return wrapped.getBatchSize();
120+
return getWrapped().getBatchSize();
111121
}
112122

113123
@Override
114124
public boolean isClosed() {
115-
return isClosed.get();
125+
if (isClosed.get()) {
126+
return true;
127+
} else if (wrappedClosedItself()) {
128+
close();
129+
return true;
130+
} else {
131+
return false;
132+
}
133+
}
134+
135+
private boolean wrappedClosedItself() {
136+
AsyncAggregateResponseBatchCursor<RawBsonDocument> observedWrapped = wrapped.get();
137+
return observedWrapped != null && observedWrapped.isClosed();
138+
}
139+
140+
/**
141+
* {@code null} is written to {@link #wrapped} before closing the wrapped object to maintain the following guarantee:
142+
* if {@link #wrappedClosedItself()} observes a {@linkplain AsyncAggregateResponseBatchCursor#isClosed() closed} wrapped object,
143+
* then it closed itself as opposed to being closed by {@code this}.
144+
*/
145+
private void nullifyAndCloseWrapped() {
146+
AsyncAggregateResponseBatchCursor<RawBsonDocument> observedWrapped = wrapped.getAndSet(null);
147+
if (observedWrapped != null) {
148+
observedWrapped.close();
149+
}
150+
}
151+
152+
/**
153+
* This method guarantees that the {@code newValue} argument is closed even if
154+
* {@link #setWrappedOrCloseIt(AsyncAggregateResponseBatchCursor)} is called concurrently with or after (in the happens-before order)
155+
* the method {@link #close()}.
156+
*/
157+
private void setWrappedOrCloseIt(final AsyncAggregateResponseBatchCursor<RawBsonDocument> newValue) {
158+
if (isClosed()) {
159+
assertNull(this.wrapped.get());
160+
newValue.close();
161+
} else {
162+
assertNull(this.wrapped.getAndSet(newValue));
163+
if (isClosed()) {
164+
nullifyAndCloseWrapped();
165+
}
166+
}
116167
}
117168

118169
@Override
119170
public BsonDocument getPostBatchResumeToken() {
120-
return wrapped.getPostBatchResumeToken();
171+
return getWrapped().getPostBatchResumeToken();
121172
}
122173

123174
@Override
@@ -127,7 +178,7 @@ public BsonTimestamp getOperationTime() {
127178

128179
@Override
129180
public boolean isFirstBatchEmpty() {
130-
return wrapped.isFirstBatchEmpty();
181+
return getWrapped().isFirstBatchEmpty();
131182
}
132183

133184
@Override
@@ -178,18 +229,18 @@ private interface AsyncBlock {
178229

179230
private void resumeableOperation(final AsyncBlock asyncBlock, final SingleResultCallback<List<RawBsonDocument>> callback,
180231
final boolean tryNext) {
181-
if (isClosed.get()) {
232+
if (isClosed()) {
182233
callback.onResult(null, new MongoException(format("%s called after the cursor was closed.",
183234
tryNext ? "tryNext()" : "next()")));
184235
return;
185236
}
186-
asyncBlock.apply(wrapped, new SingleResultCallback<List<RawBsonDocument>>() {
237+
asyncBlock.apply(getWrapped(), new SingleResultCallback<List<RawBsonDocument>>() {
187238
@Override
188239
public void onResult(final List<RawBsonDocument> result, final Throwable t) {
189240
if (t == null) {
190241
callback.onResult(result, null);
191242
} else if (isRetryableError(t, maxWireVersion)) {
192-
wrapped.close();
243+
nullifyAndCloseWrapped();
193244
retryOperation(asyncBlock, callback, tryNext);
194245
} else {
195246
callback.onResult(null, t);
@@ -214,9 +265,15 @@ public void onResult(final AsyncBatchCursor<T> result, final Throwable t) {
214265
if (t != null) {
215266
callback.onResult(null, t);
216267
} else {
217-
wrapped = ((AsyncChangeStreamBatchCursor<T>) result).getWrapped();
218-
binding.release(); // release the new change stream batch cursor's reference to the binding
219-
resumeableOperation(asyncBlock, callback, tryNext);
268+
try {
269+
setWrappedOrCloseIt(((AsyncChangeStreamBatchCursor<T>) result).getWrapped());
270+
} finally {
271+
try {
272+
binding.release(); // release the new change stream batch cursor's reference to the binding
273+
} finally {
274+
resumeableOperation(asyncBlock, callback, tryNext);
275+
}
276+
}
220277
}
221278
}
222279
});

driver-core/src/main/com/mongodb/internal/operation/AsyncQueryBatchCursor.java

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
import java.util.concurrent.atomic.AtomicInteger;
4242
import java.util.concurrent.atomic.AtomicReference;
4343

44-
import static com.mongodb.assertions.Assertions.isTrue;
44+
import static com.mongodb.assertions.Assertions.assertFalse;
4545
import static com.mongodb.assertions.Assertions.isTrueArgument;
4646
import static com.mongodb.assertions.Assertions.notNull;
4747
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
@@ -114,6 +114,14 @@ class AsyncQueryBatchCursor<T> implements AsyncAggregateResponseBatchCursor<T> {
114114
this.maxWireVersion = connection == null ? 0 : connection.getDescription().getMaxWireVersion();
115115
}
116116

117+
/**
118+
* {@inheritDoc}
119+
* <p>
120+
* From the perspective of the code external to this class, this method is idempotent as required by its specification.
121+
* However, if this method sets {@link #isClosePending},
122+
* then it must be called by {@code this} again to release resources.
123+
* This behavior does not violate externally observable idempotence because this method is allowed to release resources "eventually".
124+
*/
117125
@Override
118126
public void close() {
119127
boolean doClose = false;
@@ -145,24 +153,20 @@ public void tryNext(final SingleResultCallback<List<T>> callback) {
145153

146154
@Override
147155
public void setBatchSize(final int batchSize) {
148-
synchronized (this) {
149-
isTrue("open", !isClosed);
150-
}
156+
assertFalse(isClosed());
151157
this.batchSize = batchSize;
152158
}
153159

154160
@Override
155161
public int getBatchSize() {
156-
synchronized (this) {
157-
isTrue("open", !isClosed);
158-
}
162+
assertFalse(isClosed());
159163
return batchSize;
160164
}
161165

162166
@Override
163167
public boolean isClosed() {
164168
synchronized (this) {
165-
return isClosed;
169+
return isClosed || isClosePending;
166170
}
167171
}
168172

@@ -197,23 +201,18 @@ private void next(final SingleResultCallback<List<T>> callback, final boolean tr
197201
results = null;
198202
}
199203
firstBatch = null;
200-
ServerCursor localCursor = getServerCursor();
201-
if (localCursor == null) {
202-
synchronized (this) {
203-
isClosed = true;
204-
}
204+
if (getServerCursor() == null) {
205+
close();
205206
}
206207
callback.onResult(results, null);
207208
} else {
208209
ServerCursor localCursor = getServerCursor();
209210
if (localCursor == null) {
210-
synchronized (this) {
211-
isClosed = true;
212-
}
211+
close();
213212
callback.onResult(null, null);
214213
} else {
215214
synchronized (this) {
216-
if (isClosed) {
215+
if (isClosed()) {
217216
callback.onResult(null, new MongoException(format("%s called after the cursor was closed.",
218217
tryNext ? "tryNext()" : "next()")));
219218
return;

0 commit comments

Comments
 (0)