Adds overload retry for not read/write retryable commands - #2048
Conversation
…d/write retry policies - Implements overload-only retry (gated on retryWrites, per client-backpressure) for the write commands that previously dispatched with no retry wrapper: createIndexes, dropIndexes, create/drop/rename collection, aggregate with $out/$merge, and the search-index commands. - Removes the JAVA-5956 skips that hid these commands from the unified client-backpressure suite, and adds prose tests (not part of the spec suite) covering the commands that have no unified test coverage. JAVA-6308
There was a problem hiding this comment.
🟡 Changes recommended
Constructor compatibility, legacy integration, and multi-command retry-scope defects must be resolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds retryWrites-gated overload retries to previously non-retryable write commands across core and synchronous driver paths.
Changes:
- Adds synchronous and asynchronous overload retry loops for DDL and search-index operations.
- Propagates retry settings to write aggregations.
- Enables unified backpressure tests and adds prose coverage.
Human review is required. Blocking issues include stale constructor call sites, incomplete legacy-driver wiring, and incorrect retry scopes for encrypted collection operations.
File summaries
| File | Description |
|---|---|
UnifiedTestModifications.java |
Re-enables backpressure tests. |
BackpressureProseTest.java |
Adds command retry coverage. |
MongoDatabaseImpl.java |
Propagates aggregate retryWrites. |
MongoCollectionImpl.java |
Propagates aggregate retryWrites. |
AggregateIterableImpl.java |
Accepts write retry settings. |
UpdateSearchIndexesOperation.java |
Adds retry configuration. |
RenameCollectionOperation.java |
Adds overload retry loop. |
Operations.java |
Wires retry settings into operations. |
DropSearchIndexOperation.java |
Adds retry configuration. |
DropIndexOperation.java |
Adds overload retry loop. |
DropDatabaseOperation.java |
Adds overload retry loop. |
DropCollectionOperation.java |
Adds overload retry loop. |
CreateViewOperation.java |
Adds overload retry loop. |
CreateSearchIndexesOperation.java |
Adds retry configuration. |
CreateIndexesOperation.java |
Adds overload retry loop. |
CreateCollectionOperation.java |
Adds overload retry loop. |
AggregateToCollectionOperation.java |
Enables write-gated overload retries. |
AbstractWriteSearchIndexOperation.java |
Centralizes search-index retries. |
Review details
- Files reviewed: 18/18 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…olicies - Wraps DDL and search-index write commands in an overload-only retry policy gated on retryWrites, and extends the prose suite to cover them. JAVA-6065
There was a problem hiding this comment.
🔵 Needs a closer look
Core retry and connection-selection behavior has unresolved command-scoping and retargeting issues and requires human review.
Review details
Suppressed comments (2)
driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java:289
- The async path likewise retains one
AsyncConnectionSourcefor the entire sequence, so retries cannot perform server selection and cannot honor overload retargeting. Acquire the write source as part of eachdecorateWithRetriesAsyncattempt rather than aroundProcessCommandsCallback.
withAsyncWriteConnectionSource(binding, operationContext, callback,
(source, operationContextWithMinRtt, sourceReleasingCallback) ->
new ProcessCommandsCallback(binding, source, operationContextWithMinRtt, sourceReleasingCallback)
.onResult(null, null));
driver-core/src/main/com/mongodb/internal/operation/DropCollectionOperation.java:153
- The async retry supplier encloses
ProcessCommandsCallback, which processes every encrypted-drop command. An overload on a later command therefore reruns prior successful drops and consumes a sequence-wide retry budget instead of retrying that command independently. Move retry-control creation intoProcessCommandsCallbackfor each dequeued command.
RetryControl<SpecRetryPolicy> retryControl = createSpecRetryControl(
createSpecRetryPolicy(),
operationContext);
AsyncCallbackSupplier<Void> retryingCommandExecutor = decorateWithRetriesAsync(retryControl, operationContext,
- Files reviewed: 23/23 changed files
- Comments generated: 2
- Review effort level: Balanced
| executeCommand(binding, operationContextWithMinRtt, databaseName, commandCreator.get(), connection, | ||
| writeConcernErrorTransformer(operationContextWithMinRtt.getTimeoutContext())) | ||
| ); | ||
| return withWriteConnectionSource(binding, operationContext, (source, operationContextWithMinRtt) -> { |
There was a problem hiding this comment.
Closed by re-selecting the write source per retry attempt for every create/drop sub-command, matching the other unsupported write commands.
A conditional shape was considered: pin one source for the Queryable Encryption multi-command sequence, while re-selecting for the single non-encrypted command. Re-selecting for all commands was chosen instead.
The tradeoff is:
- Under 462 overload, most cases should not result in stepdown, so pinning and re-selection are equivalent: writes target the primary either way.
- If stepdown does happen, re-selection can recover the in-flight command on the new primary. Pinning would fail against the old primary with
NotWritablePrimary. - The main case where pinning helps is a narrow partial-state edge case when a previously succeeded QE sub-command did not replicate before stepdown. For example: create esc succeeds on primary A, A steps down before esc replicates to
B, and the next sub-command fails with 462. With re-selection, the retry can continue onB, so the sequence may complete with esc missing onB. With pinning, the retry stays onA, getsNotWritablePrimary, and aborts the sequence without a slient failure. Then the application-level can decide what to do with the failure.
Net: re-selecting gives better availability and keeps this consistent with the other unsupported write commands. The conditional pin/re-select shape did not seem worth the extra complexity
- Hoists the encryptedFields lookup out of the async drop retry loop so it is not retried under the write gate, matching the sync path. - Gives each drop sub-command its own retry budget, mirroring the create path. - Re-selects a write source per attempt for the single non-encrypted command (retargets on stepdown); pins one source for the encrypted multi-command sequence so it fails loudly rather than silently losing a state collection. JAVA-6065
Each create/drop sub-command now re-selects a write source per retry attempt via withConnection(binding, ...), matching the other unsupported write commands, instead of pinning one source across the Queryable Encryption sequence. Drops the source-accepting withConnection helpers that are no longer used. JAVA-6065
- Adds overloadForWrite/overloadForRead on SpecRetryPolicy.IndividualPolicies so the unsupported-write commands and getMore build their overload-only policy through a single shared entry point instead of repeating the IndividualPolicies+includeOverload+ErrorPropagation construction at each call site JAVA-6308
There was a problem hiding this comment.
🟡 Changes recommended
The legacy aggregate-to-collection path still disables overload retries.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 29/29 changed files
- Comments generated: 1
- Review effort level: Balanced
jyemin
left a comment
There was a problem hiding this comment.
I didn't read every line of every operation, but overall the design holds up and the changes appear (mostly) mechanical.
I posted one question, but LGTM regardless.
| // backpressure | ||
|
|
||
| def.modify(WAIT_FOR_BATCH_CURSOR_CREATION) | ||
| def.modify(WAIT_FOR_BATCH_CURSOR_CREATION, IGNORE_EXTRA_EVENTS) |
There was a problem hiding this comment.
The backpressure changeStream tests were intermittently failing in reactive: after the aggregate succeeds, the reactive change-stream cursor proactively issues a getMore (awaitData), and those trailing events land non-deterministically relative to the runner's exact-count event snapshot. WAIT_FOR_BATCH_CURSOR_CREATION only synchronizes cursor creation (the aggregate); it doesn't account for the subsequent getMore/killCursors.
Summary
Adds overload-only retry support to 11 write-command operations that previously dispatched commands without any retry wrapper.
The retry policy is gated on
retryWritesand uses:includeOverload(maxAdaptiveRetriesSetting, ErrorPropagation.AS_WRITE_POLICY)It intentionally does not use
includeWrite(), because these commands are not retryable writes under the retryable-writes spec.Commands covered
createIndexesCreateIndexesOperationdropIndexesDropIndexOperationcreateCreateCollectionOperationcreateviewCreateViewOperationdropDropCollectionOperationdropDatabaseDropDatabaseOperationrenameCollectionRenameCollectionOperationaggregatewith$out/$mergeAggregateToCollectionOperationcreateSearchIndexesCreateSearchIndexesOperationupdateSearchIndexUpdateSearchIndexesOperationdropSearchIndexDropSearchIndexOperationImplementation details
createSpecRetryControl+decorateWithRetries.createSpecRetryControl+decorateWithRetriesAsync.onCommand(this::getCommandName)for retry debug logging.retryWritesandmaxAdaptiveRetriesSettingfrom the client layer throughOperationsinto the affected operations.$out/$mergerespects the actualretryWritessetting.Scope notes
Already covered before this change:
updateMany/deleteMany/bulkWritefindAndModifyrunCommandgetMoreexecuteRetryableReadIntentionally excluded:
mapReduce{w:0}writeskillCursorsTest coverage
JAVA-5956skips for unified client-backpressure tests:createIndexdropIndexdropIndexesJAVA-6308
JAVA-6119 - Fix client construction in backpressure unified tests
Reason to close: the
TODO-JAVA-5956skips that masked the client-construction gap are removed.The unified backpressure suite now constructs per-test clients such as
client_retryWrites_falseandclient_retryReads_falseaccording to the spec and executes those scenarios instead of skipping them. No remaining driver-side client-construction issue is known.JAVA-6124 - Test overload retry when
retryReads/retryWritesis falseReason to close: the 34 unified “does not retry if retry*=false” cases run and pass.
They verify gate behavior across command families: retryable reads are gated on
retryReads, retryable writes onretryWrites, andrunCommandonretryReads && retryWrites.This was a verification ticket, not an implementation ticket, and the suite is now green.
JAVA-5956 - Exponential backoff and jitter in retry loops
Reason to close: the backoff/jitter implementation already exists in prior work.
All
TODO-JAVA-5956unified-test skips are now removed:createIndex/dropIndex/dropIndexes/ aggregate-write skips via JAVA-6308, and thegetMorebackpressure skip via JAVA-6296.The only remaining getMore-related skip is a reactive-only
skipNoncompliantReactivefor the cursor auto-close flake, which is not aTODO-JAVA-5956marker. NoTODO-JAVA-5956markers remain.