Skip to content

Commit cf3b483

Browse files
committed
Propagate the original error for write errors labeled NoWritesPerformed
JAVA-4701
1 parent cadd125 commit cf3b483

File tree

3 files changed

+82
-2
lines changed

3 files changed

+82
-2
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ static Throwable chooseRetryableWriteException(
163163
return mostRecentAttemptException.getCause();
164164
}
165165
return mostRecentAttemptException;
166-
} else if (mostRecentAttemptException instanceof ResourceSupplierInternalException) {
166+
} else if (mostRecentAttemptException instanceof ResourceSupplierInternalException
167+
|| (mostRecentAttemptException instanceof MongoException
168+
&& ((MongoException)mostRecentAttemptException).hasErrorLabel(NO_WRITES_PERFORMED_ERROR_LABEL))) {
167169
return previouslyChosenException;
168170
} else {
169171
return mostRecentAttemptException;
@@ -571,6 +573,7 @@ private static boolean isRetryWritesEnabled(@Nullable final BsonDocument command
571573
}
572574

573575
static final String RETRYABLE_WRITE_ERROR_LABEL = "RetryableWriteError";
576+
private static final String NO_WRITES_PERFORMED_ERROR_LABEL = "NoWritesPerformed";
574577

575578
private static boolean decideRetryableAndAddRetryableWriteErrorLabel(final Throwable t, @Nullable final Integer maxWireVersion) {
576579
if (!(t instanceof MongoException)) {

driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/RetryableWritesProseTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ public void poolClearedExceptionMustBeRetryable() throws InterruptedException, E
9898
mongoCollection -> mongoCollection.insertOne(new Document()), "insert", true);
9999
}
100100

101+
/**
102+
* Prose test #3.
103+
*/
104+
@Test
105+
public void originalErrorMustBePropagatedIfNoWritesPerformed() throws InterruptedException {
106+
com.mongodb.client.RetryableWritesProseTest.originalErrorMustBePropagatedIfNoWritesPerformed(
107+
mongoClientSettings -> new SyncMongoClient(MongoClients.create(mongoClientSettings)));
108+
}
109+
101110
private boolean canRunTests() {
102111
Document storageEngine = (Document) getServerStatus().get("storageEngine");
103112

driver-sync/src/test/functional/com/mongodb/client/RetryableWritesProseTest.java

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@
1919
import com.mongodb.Function;
2020
import com.mongodb.MongoClientException;
2121
import com.mongodb.MongoClientSettings;
22+
import com.mongodb.MongoCommandException;
2223
import com.mongodb.MongoException;
24+
import com.mongodb.ServerAddress;
25+
import com.mongodb.assertions.Assertions;
26+
import com.mongodb.event.CommandFailedEvent;
27+
import com.mongodb.event.CommandListener;
2328
import com.mongodb.event.ConnectionCheckOutFailedEvent;
2429
import com.mongodb.event.ConnectionCheckedOutEvent;
2530
import com.mongodb.event.ConnectionPoolClearedEvent;
@@ -34,12 +39,17 @@
3439
import org.junit.Before;
3540
import org.junit.Test;
3641

42+
import java.util.List;
43+
import java.util.concurrent.CompletableFuture;
3744
import java.util.concurrent.ExecutionException;
3845
import java.util.concurrent.ExecutorService;
3946
import java.util.concurrent.Executors;
4047
import java.util.concurrent.Future;
4148
import java.util.concurrent.TimeUnit;
4249
import java.util.concurrent.TimeoutException;
50+
import java.util.concurrent.atomic.AtomicBoolean;
51+
import java.util.function.BiFunction;
52+
import java.util.stream.Collectors;
4353

4454
import static com.mongodb.ClusterFixture.getServerStatus;
4555
import static com.mongodb.ClusterFixture.isDiscoverableReplicaSet;
@@ -55,6 +65,7 @@
5565
import static java.util.concurrent.TimeUnit.SECONDS;
5666
import static org.junit.Assert.assertEquals;
5767
import static org.junit.Assert.assertTrue;
68+
import static org.junit.Assert.fail;
5869
import static org.junit.Assume.assumeFalse;
5970
import static org.junit.Assume.assumeTrue;
6071

@@ -138,7 +149,6 @@ public static <R> void poolClearedExceptionMustBeRetryable(
138149
* As a result, the client has to wait for at least its heartbeat delay until it hears back from a server
139150
* (while it waits for a response, calling `ServerMonitor.connect` has no effect).
140151
* Thus, we want to use small heartbeat delay to reduce delays in the test. */
141-
.minHeartbeatFrequency(50, TimeUnit.MILLISECONDS)
142152
.heartbeatFrequency(50, TimeUnit.MILLISECONDS))
143153
.retryReads(true)
144154
.retryWrites(true)
@@ -179,6 +189,64 @@ public static <R> void poolClearedExceptionMustBeRetryable(
179189
}
180190
}
181191

192+
/**
193+
* Prose test #3.
194+
*/
195+
@Test
196+
public void originalErrorMustBePropagatedIfNoWritesPerformed() throws InterruptedException {
197+
originalErrorMustBePropagatedIfNoWritesPerformed(MongoClients::create);
198+
}
199+
200+
public static void originalErrorMustBePropagatedIfNoWritesPerformed(
201+
final Function<MongoClientSettings, MongoClient> clientCreator) throws InterruptedException {
202+
assumeTrue(serverVersionAtLeast(6, 0) && isDiscoverableReplicaSet());
203+
BiFunction<Integer, List<String>, BsonDocument> configureFailPointDocCreator = (errorCode, errorLabels) ->
204+
new BsonDocument()
205+
.append("configureFailPoint", new BsonString("failCommand"))
206+
.append("mode", new BsonDocument()
207+
.append("times", new BsonInt32(1)))
208+
.append("data", new BsonDocument()
209+
.append("failCommands", new BsonArray(singletonList(new BsonString("insert"))))
210+
.append("errorCode", new BsonInt32(errorCode))
211+
.append("errorLabels", new BsonArray(errorLabels.stream().map(BsonString::new).collect(Collectors.toList()))));
212+
ServerAddress primaryServerAddress = Fixture.getPrimary();
213+
CompletableFuture<FailPoint> futureFailPoint = new CompletableFuture<>();
214+
CommandListener commandListener = new CommandListener() {
215+
private final AtomicBoolean configureFailPoint = new AtomicBoolean(true);
216+
217+
@Override
218+
public void commandFailed(final CommandFailedEvent event) {
219+
if (event.getCommandName().equals("insert") && configureFailPoint.compareAndSet(true, false)) {
220+
Assertions.assertTrue(futureFailPoint.complete(FailPoint.enable(
221+
configureFailPointDocCreator.apply(10107, asList("RetryableWriteError", "NoWritesPerformed")),
222+
primaryServerAddress
223+
)));
224+
}
225+
}
226+
};
227+
try (MongoClient client = clientCreator.apply(getMongoClientSettingsBuilder()
228+
.retryWrites(true)
229+
.addCommandListener(commandListener)
230+
.applyToServerSettings(builder ->
231+
// see `poolClearedExceptionMustBeRetryable` for the explanation
232+
builder.heartbeatFrequency(50, TimeUnit.MILLISECONDS))
233+
.build());
234+
FailPoint ignored = FailPoint.enable(configureFailPointDocCreator.apply(91, singletonList("RetryableWriteError")), client)) {
235+
MongoCollection<Document> collection = client.getDatabase(getDefaultDatabaseName())
236+
.getCollection("originalErrorMustBePropagatedIfErrorWithRetryableWriteErrorLabelHappens");
237+
collection.drop();
238+
try {
239+
collection.insertOne(new Document());
240+
} catch (MongoCommandException e) {
241+
assertEquals(e.getErrorCode(), 91);
242+
return;
243+
}
244+
fail("must not reach");
245+
} finally {
246+
futureFailPoint.thenAccept(FailPoint::close);
247+
}
248+
}
249+
182250
private boolean canRunTests() {
183251
Document storageEngine = (Document) getServerStatus().get("storageEngine");
184252

0 commit comments

Comments
 (0)