Skip to content

Commit fbc5e4e

Browse files
authored
Propagate the original error for write errors labeled NoWritesPerformed (#1013)
JAVA-4701
1 parent a8fe4ca commit fbc5e4e

File tree

6 files changed

+112
-27
lines changed

6 files changed

+112
-27
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-core/src/test/functional/com/mongodb/internal/operation/MixedBulkWriteOperationSpecification.groovy

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -846,14 +846,14 @@ class MixedBulkWriteOperationSpecification extends OperationFunctionalSpecificat
846846
given:
847847
getCollectionHelper().insertDocuments(getTestInserts())
848848
def operation = new MixedBulkWriteOperation(getNamespace(),
849-
[new InsertRequest(new BsonDocument('_id', new BsonInt32(7))),
850-
new InsertRequest(new BsonDocument('_id', new BsonInt32(1))) // duplicate key
851-
], false, ACKNOWLEDGED, true)
849+
[new DeleteRequest(new BsonDocument('_id', new BsonInt32(2))), // existing key
850+
new InsertRequest(new BsonDocument('_id', new BsonInt32(1))) // existing (duplicate) key
851+
], true, ACKNOWLEDGED, true)
852852

853853
def failPoint = BsonDocument.parse('''{
854854
"configureFailPoint": "failCommand",
855855
"mode": {"times": 2 },
856-
"data": { "failCommands": ["insert"],
856+
"data": { "failCommands": ["delete"],
857857
"writeConcernError": {"code": 91, "errmsg": "Replication is being shut down"}}}''')
858858
configureFailPoint(failPoint)
859859

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/FailPoint.java

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@
2929
public final class FailPoint implements AutoCloseable {
3030
private final BsonDocument failPointDocument;
3131
private final MongoClient client;
32-
private final boolean close;
3332

34-
private FailPoint(final BsonDocument failPointDocument, final MongoClient client, final boolean close) {
33+
private FailPoint(final BsonDocument failPointDocument, final MongoClient client) {
3534
this.failPointDocument = failPointDocument.toBsonDocument();
3635
this.client = client;
37-
this.close = close;
3836
}
3937

4038
/**
4139
* @param configureFailPointDoc A document representing {@code configureFailPoint} command to be issued as is via
4240
* {@link com.mongodb.client.MongoDatabase#runCommand(Bson)}.
41+
* @param serverAddress One may use {@link Fixture#getPrimary()} to get the address of a primary server
42+
* if that is what is needed.
4343
*/
4444
public static FailPoint enable(final BsonDocument configureFailPointDoc, final ServerAddress serverAddress) {
4545
MongoClientSettings clientSettings = getMongoClientSettingsBuilder()
@@ -48,18 +48,11 @@ public static FailPoint enable(final BsonDocument configureFailPointDoc, final S
4848
.hosts(Collections.singletonList(serverAddress)))
4949
.build();
5050
MongoClient client = MongoClients.create(clientSettings);
51-
return enable(configureFailPointDoc, client, true);
51+
return enable(configureFailPointDoc, client);
5252
}
5353

54-
/**
55-
* @see #enable(BsonDocument, ServerAddress)
56-
*/
57-
public static FailPoint enable(final BsonDocument configureFailPointDoc, final MongoClient client) {
58-
return enable(configureFailPointDoc, client, false);
59-
}
60-
61-
private static FailPoint enable(final BsonDocument configureFailPointDoc, final MongoClient client, final boolean close) {
62-
FailPoint result = new FailPoint(configureFailPointDoc, client, close);
54+
private static FailPoint enable(final BsonDocument configureFailPointDoc, final MongoClient client) {
55+
FailPoint result = new FailPoint(configureFailPointDoc, client);
6356
client.getDatabase("admin").runCommand(configureFailPointDoc);
6457
return result;
6558
}
@@ -71,9 +64,7 @@ public void close() {
7164
.append("configureFailPoint", failPointDocument.getString("configureFailPoint"))
7265
.append("mode", new BsonString("off")));
7366
} finally {
74-
if (close) {
75-
client.close();
76-
}
67+
client.close();
7768
}
7869
}
7970
}

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import com.mongodb.ConnectionString;
2020
import com.mongodb.MongoClientSettings;
2121
import com.mongodb.ServerAddress;
22-
import com.mongodb.client.internal.MongoClientImpl;
2322
import com.mongodb.connection.ServerDescription;
2423

2524
import java.util.List;
@@ -109,12 +108,16 @@ public static MongoClientSettings.Builder getMongoClientSettings(final Connectio
109108
return builder;
110109
}
111110

111+
/**
112+
* Beware of a potential race condition hiding here: the primary you discover may differ from the one used by the {@code client}
113+
* when performing some operations, as the primary may change.
114+
*/
112115
public static ServerAddress getPrimary() throws InterruptedException {
113-
getMongoClient();
114-
List<ServerDescription> serverDescriptions = getPrimaries(((MongoClientImpl) mongoClient).getCluster().getDescription());
116+
MongoClient client = getMongoClient();
117+
List<ServerDescription> serverDescriptions = getPrimaries(client.getClusterDescription());
115118
while (serverDescriptions.isEmpty()) {
116119
Thread.sleep(100);
117-
serverDescriptions = getPrimaries(((MongoClientImpl) mongoClient).getCluster().getDescription());
120+
serverDescriptions = getPrimaries(client.getClusterDescription());
118121
}
119122
return serverDescriptions.get(0).getAddress();
120123
}

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

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
import com.mongodb.MongoClientException;
2121
import com.mongodb.MongoClientSettings;
2222
import com.mongodb.MongoException;
23+
import com.mongodb.ServerAddress;
24+
import com.mongodb.assertions.Assertions;
25+
import com.mongodb.event.CommandListener;
26+
import com.mongodb.event.CommandSucceededEvent;
2327
import com.mongodb.event.ConnectionCheckOutFailedEvent;
2428
import com.mongodb.event.ConnectionCheckedOutEvent;
2529
import com.mongodb.event.ConnectionPoolClearedEvent;
@@ -34,12 +38,16 @@
3438
import org.junit.Before;
3539
import org.junit.Test;
3640

41+
import java.util.concurrent.CompletableFuture;
3742
import java.util.concurrent.ExecutionException;
3843
import java.util.concurrent.ExecutorService;
3944
import java.util.concurrent.Executors;
4045
import java.util.concurrent.Future;
4146
import java.util.concurrent.TimeUnit;
4247
import java.util.concurrent.TimeoutException;
48+
import java.util.concurrent.atomic.AtomicBoolean;
49+
import java.util.stream.Collectors;
50+
import java.util.stream.Stream;
4351

4452
import static com.mongodb.ClusterFixture.getServerStatus;
4553
import static com.mongodb.ClusterFixture.isDiscoverableReplicaSet;
@@ -55,6 +63,7 @@
5563
import static java.util.concurrent.TimeUnit.SECONDS;
5664
import static org.junit.Assert.assertEquals;
5765
import static org.junit.Assert.assertTrue;
66+
import static org.junit.Assert.fail;
5867
import static org.junit.Assume.assumeFalse;
5968
import static org.junit.Assume.assumeTrue;
6069

@@ -138,7 +147,6 @@ public static <R> void poolClearedExceptionMustBeRetryable(
138147
* As a result, the client has to wait for at least its heartbeat delay until it hears back from a server
139148
* (while it waits for a response, calling `ServerMonitor.connect` has no effect).
140149
* Thus, we want to use small heartbeat delay to reduce delays in the test. */
141-
.minHeartbeatFrequency(50, TimeUnit.MILLISECONDS)
142150
.heartbeatFrequency(50, TimeUnit.MILLISECONDS))
143151
.retryReads(true)
144152
.retryWrites(true)
@@ -158,7 +166,7 @@ public static <R> void poolClearedExceptionMustBeRetryable(
158166
.append("blockTimeMS", new BsonInt32(1000)));
159167
int timeoutSeconds = 5;
160168
try (MongoClient client = clientCreator.apply(clientSettings);
161-
FailPoint ignored = FailPoint.enable(configureFailPoint, client)) {
169+
FailPoint ignored = FailPoint.enable(configureFailPoint, Fixture.getPrimary())) {
162170
MongoCollection<Document> collection = client.getDatabase(getDefaultDatabaseName())
163171
.getCollection("poolClearedExceptionMustBeRetryable");
164172
collection.drop();
@@ -179,6 +187,77 @@ public static <R> void poolClearedExceptionMustBeRetryable(
179187
}
180188
}
181189

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

0 commit comments

Comments
 (0)