Skip to content

Commit b3d59a7

Browse files
Handle negative e2e timeout more gracefully by throwing CosmsoException instead of IllegalArgumentException (Azure#36507)
* Handling negative e2e timeout gracefully by throwin OperationCancelledException instead of IllegalArguemntException when creating the config * Update CosmosDiagnosticsTest.java * Update CHANGELOG.md * Update CosmosDiagnosticsTest.java * Update CosmosDiagnosticsTest.java
1 parent fc95454 commit b3d59a7

File tree

5 files changed

+172
-43
lines changed

5 files changed

+172
-43
lines changed

sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,6 +1405,79 @@ public void responseStatisticRequestStartTimeUTCForDirectCall() {
14051405
}
14061406
}
14071407

1408+
@Test(groups = {"emulator"}, timeOut = TIMEOUT)
1409+
public void negativeE2ETimeoutWithPointOperation() {
1410+
CosmosAsyncClient client = null;
1411+
String databaseId = DatabaseForTest.generateId();
1412+
1413+
try {
1414+
client = new CosmosClientBuilder()
1415+
.key(TestConfigurations.MASTER_KEY)
1416+
.endpoint(TestConfigurations.HOST)
1417+
.buildAsyncClient();
1418+
1419+
createDatabase(client, databaseId);
1420+
CosmosAsyncContainer container = createCollection(client, databaseId, getCollectionDefinition());
1421+
1422+
TestItem testItem = TestItem.createNewItem();
1423+
CosmosItemRequestOptions requestOptions = new CosmosItemRequestOptions();
1424+
requestOptions.setCosmosEndToEndOperationLatencyPolicyConfig(
1425+
new CosmosEndToEndOperationLatencyPolicyConfigBuilder(Duration.ofSeconds(-1)).build()
1426+
);
1427+
container.createItem(testItem, requestOptions).block();
1428+
fail("This should have failed with an exception");
1429+
} catch(OperationCancelledException cancelledException) {
1430+
assertThat(cancelledException).isNotNull();
1431+
assertThat(cancelledException.getStatusCode()).isEqualTo(408);
1432+
assertThat(cancelledException.getSubStatusCode())
1433+
.isEqualTo(HttpConstants.SubStatusCodes.NEGATIVE_TIMEOUT_PROVIDED);
1434+
assertThat(cancelledException.getDiagnostics()).isNotNull();
1435+
logger.info("Expected request timeout: ", cancelledException);
1436+
}
1437+
finally {
1438+
safeClose(client);
1439+
}
1440+
}
1441+
1442+
@Test(groups = {"emulator"}, timeOut = TIMEOUT)
1443+
public void negativeE2ETimeoutWithQueryOperation() {
1444+
CosmosAsyncClient client = null;
1445+
String databaseId = DatabaseForTest.generateId();
1446+
1447+
try {
1448+
client = new CosmosClientBuilder()
1449+
.key(TestConfigurations.MASTER_KEY)
1450+
.endpoint(TestConfigurations.HOST)
1451+
.buildAsyncClient();
1452+
1453+
createDatabase(client, databaseId);
1454+
CosmosAsyncContainer container = createCollection(client, databaseId, getCollectionDefinition());
1455+
1456+
TestItem testItem = TestItem.createNewItem();
1457+
container.createItem(testItem).block();
1458+
1459+
CosmosQueryRequestOptions requestOptions = new CosmosQueryRequestOptions();
1460+
requestOptions.setCosmosEndToEndOperationLatencyPolicyConfig(
1461+
new CosmosEndToEndOperationLatencyPolicyConfigBuilder(Duration.ofSeconds(-1)).build()
1462+
);
1463+
CosmosPagedFlux<ObjectNode> flux = container.readAllItems(requestOptions, ObjectNode.class);
1464+
List<ObjectNode> results = flux.collectList().block();
1465+
1466+
fail("This should have failed with an exception");
1467+
} catch(OperationCancelledException cancelledException) {
1468+
assertThat(cancelledException).isNotNull();
1469+
assertThat(cancelledException.getStatusCode()).isEqualTo(408);
1470+
assertThat(cancelledException.getSubStatusCode())
1471+
.isEqualTo(HttpConstants.SubStatusCodes.NEGATIVE_TIMEOUT_PROVIDED);
1472+
// No pending requests - so, diagnostics are not guaranteed to be there
1473+
// assertThat(cancelledException.getDiagnostics()).isNotNull();
1474+
logger.info("Expected request timeout: ", cancelledException);
1475+
}
1476+
finally {
1477+
safeClose(client);
1478+
}
1479+
}
1480+
14081481
private InternalObjectNode getInternalObjectNode() {
14091482
InternalObjectNode internalObjectNode = new InternalObjectNode();
14101483
String uuid = UUID.randomUUID().toString();

sdk/cosmos/azure-cosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#### Bugs Fixed
1010

1111
#### Other Changes
12+
* Handling negative end-to-end timeouts provided more gracefully by throwing a `CosmsoException` (`OperationCancelledException`) instead of `IllegalArgumentException`. - See [PR 36507](https://github.com/Azure/azure-sdk-for-java/pull/36507)
1213

1314
### 4.49.0 (2023-08-21)
1415

sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosEndToEndOperationLatencyPolicyConfigBuilder.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ public CosmosEndToEndOperationLatencyPolicyConfig build() {
3434
if (endToEndOperationTimeout == null && isEnabled) {
3535
throw new IllegalArgumentException("endToEndOperationTimeout must be set if the policy is enabled");
3636
}
37-
if (endToEndOperationTimeout != null && endToEndOperationTimeout.isNegative()) {
38-
throw new IllegalArgumentException("endToEndOperationTimeout must be a positive Duration");
39-
}
37+
4038
return new CosmosEndToEndOperationLatencyPolicyConfig(isEnabled, endToEndOperationTimeout, availabilityStrategy);
4139
}
4240

sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/HttpConstants.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,14 +425,18 @@ public static class SubStatusCodes {
425425
public static final int USER_REQUEST_RATE_TOO_LARGE = 3200;
426426

427427
//SDK Codes(Client)
428-
// IMPORTANT - whenever possible use consistency substaus codes that .Net SDK also uses
428+
// IMPORTANT - whenever possible use consistency substatus codes that .Net SDK also uses
429429
public static final int TRANSPORT_GENERATED_410 = 20001;
430430
public static final int TIMEOUT_GENERATED_410 = 20002;
431431
// Client generated operation timeout exception
432432
public static final int CLIENT_OPERATION_TIMEOUT = 20008;
433433

434+
// IMPORTANT - below sub status codes have no corresponding .Net
435+
// version, because they are only applicable in Java
436+
public static final int NEGATIVE_TIMEOUT_PROVIDED = 20901; // .Net has different cancellation concept
437+
434438
//SDK Codes (Server)
435-
// IMPORTANT - whenever possible use consistency substaus codes that .Net SDK also uses
439+
// IMPORTANT - whenever possible use consistency substatus codes that .Net SDK also uses
436440
public static final int NAME_CACHE_IS_STALE_EXCEEDED_RETRY_LIMIT = 21001;
437441
public static final int PARTITION_KEY_RANGE_GONE_EXCEEDED_RETRY_LIMIT = 21002;
438442
public static final int COMPLETING_SPLIT_EXCEEDED_RETRY_LIMIT = 21003;

sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/RxDocumentClientImpl.java

Lines changed: 91 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
import java.net.URI;
9191
import java.net.URLEncoder;
9292
import java.nio.ByteBuffer;
93+
import java.time.Duration;
9394
import java.time.Instant;
9495
import java.util.ArrayList;
9596
import java.util.Arrays;
@@ -1001,57 +1002,86 @@ private <T> Flux<FeedResponse<T>> createQueryInternal(
10011002
}, Queues.SMALL_BUFFER_SIZE, 1);
10021003
}
10031004

1005+
private static void applyExceptionToMergedDiagnostics(
1006+
CosmosQueryRequestOptions requestOptions,
1007+
CosmosException exception) {
1008+
1009+
List<CosmosDiagnostics> cancelledRequestDiagnostics =
1010+
ImplementationBridgeHelpers
1011+
.CosmosQueryRequestOptionsHelper
1012+
.getCosmosQueryRequestOptionsAccessor()
1013+
.getCancelledRequestDiagnosticsTracker(requestOptions);
1014+
1015+
// if there is any cancelled requests, collect cosmos diagnostics
1016+
if (cancelledRequestDiagnostics != null && !cancelledRequestDiagnostics.isEmpty()) {
1017+
// combine all the cosmos diagnostics
1018+
CosmosDiagnostics aggregratedCosmosDiagnostics =
1019+
cancelledRequestDiagnostics
1020+
.stream()
1021+
.reduce((first, toBeMerged) -> {
1022+
ClientSideRequestStatistics clientSideRequestStatistics =
1023+
ImplementationBridgeHelpers
1024+
.CosmosDiagnosticsHelper
1025+
.getCosmosDiagnosticsAccessor()
1026+
.getClientSideRequestStatisticsRaw(first);
1027+
1028+
ClientSideRequestStatistics toBeMergedClientSideRequestStatistics =
1029+
ImplementationBridgeHelpers
1030+
.CosmosDiagnosticsHelper
1031+
.getCosmosDiagnosticsAccessor()
1032+
.getClientSideRequestStatisticsRaw(first);
1033+
1034+
if (clientSideRequestStatistics == null) {
1035+
return toBeMerged;
1036+
} else {
1037+
clientSideRequestStatistics.mergeClientSideRequestStatistics(toBeMergedClientSideRequestStatistics);
1038+
return first;
1039+
}
1040+
})
1041+
.get();
1042+
1043+
BridgeInternal.setCosmosDiagnostics(exception, aggregratedCosmosDiagnostics);
1044+
}
1045+
}
1046+
10041047
private static <T> Flux<FeedResponse<T>> getFeedResponseFluxWithTimeout(
10051048
Flux<FeedResponse<T>> feedResponseFlux,
10061049
CosmosEndToEndOperationLatencyPolicyConfig endToEndPolicyConfig,
10071050
CosmosQueryRequestOptions requestOptions,
10081051
final AtomicBoolean isQueryCancelledOnTimeout) {
10091052

1053+
Duration endToEndTimeout = endToEndPolicyConfig.getEndToEndOperationTimeout();
1054+
1055+
1056+
Flux<FeedResponse<T>> flux;
1057+
if (endToEndTimeout.isNegative()) {
1058+
return feedResponseFlux
1059+
.timeout(endToEndTimeout)
1060+
.onErrorMap(throwable -> {
1061+
if (throwable instanceof TimeoutException) {
1062+
CosmosException cancellationException = getNegativeTimeoutException(null, endToEndTimeout);
1063+
cancellationException.setStackTrace(throwable.getStackTrace());
1064+
1065+
isQueryCancelledOnTimeout.set(true);
1066+
1067+
applyExceptionToMergedDiagnostics(requestOptions, cancellationException);
1068+
1069+
return cancellationException;
1070+
}
1071+
return throwable;
1072+
});
1073+
}
1074+
10101075
return feedResponseFlux
1011-
.timeout(endToEndPolicyConfig.getEndToEndOperationTimeout())
1076+
.timeout(endToEndTimeout)
10121077
.onErrorMap(throwable -> {
10131078
if (throwable instanceof TimeoutException) {
10141079
CosmosException exception = new OperationCancelledException();
10151080
exception.setStackTrace(throwable.getStackTrace());
10161081

10171082
isQueryCancelledOnTimeout.set(true);
10181083

1019-
List<CosmosDiagnostics> cancelledRequestDiagnostics =
1020-
ImplementationBridgeHelpers
1021-
.CosmosQueryRequestOptionsHelper
1022-
.getCosmosQueryRequestOptionsAccessor()
1023-
.getCancelledRequestDiagnosticsTracker(requestOptions);
1024-
1025-
// if there is any cancelled requests, collect cosmos diagnostics
1026-
if (cancelledRequestDiagnostics != null && !cancelledRequestDiagnostics.isEmpty()) {
1027-
// combine all the cosmos diagnostics
1028-
CosmosDiagnostics aggregratedCosmosDiagnostics =
1029-
cancelledRequestDiagnostics
1030-
.stream()
1031-
.reduce((first, toBeMerged) -> {
1032-
ClientSideRequestStatistics clientSideRequestStatistics =
1033-
ImplementationBridgeHelpers
1034-
.CosmosDiagnosticsHelper
1035-
.getCosmosDiagnosticsAccessor()
1036-
.getClientSideRequestStatisticsRaw(first);
1037-
1038-
ClientSideRequestStatistics toBeMergedClientSideRequestStatistics =
1039-
ImplementationBridgeHelpers
1040-
.CosmosDiagnosticsHelper
1041-
.getCosmosDiagnosticsAccessor()
1042-
.getClientSideRequestStatisticsRaw(first);
1043-
1044-
if (clientSideRequestStatistics == null) {
1045-
return toBeMerged;
1046-
} else {
1047-
clientSideRequestStatistics.mergeClientSideRequestStatistics(toBeMergedClientSideRequestStatistics);
1048-
return first;
1049-
}
1050-
})
1051-
.get();
1052-
1053-
BridgeInternal.setCosmosDiagnostics(exception, aggregratedCosmosDiagnostics);
1054-
}
1084+
applyExceptionToMergedDiagnostics(requestOptions, exception);
10551085

10561086
return exception;
10571087
}
@@ -2022,9 +2052,15 @@ private static <T> Mono<T> getRxDocumentServiceResponseMonoWithE2ETimeout(RxDocu
20222052
CosmosEndToEndOperationLatencyPolicyConfig endToEndPolicyConfig,
20232053
Mono<T> rxDocumentServiceResponseMono) {
20242054
if (endToEndPolicyConfig != null && endToEndPolicyConfig.isEnabled()) {
2055+
2056+
Duration endToEndTimeout = endToEndPolicyConfig.getEndToEndOperationTimeout();
2057+
if (endToEndTimeout.isNegative()) {
2058+
return Mono.error(getNegativeTimeoutException(request, endToEndTimeout));
2059+
}
2060+
20252061
request.requestContext.setEndToEndOperationLatencyPolicyConfig(endToEndPolicyConfig);
20262062
return rxDocumentServiceResponseMono
2027-
.timeout(endToEndPolicyConfig.getEndToEndOperationTimeout())
2063+
.timeout(endToEndTimeout)
20282064
.onErrorMap(throwable -> getCancellationException(request, throwable));
20292065
}
20302066
return rxDocumentServiceResponseMono;
@@ -2045,6 +2081,23 @@ private static Throwable getCancellationException(RxDocumentServiceRequest reque
20452081
return throwable;
20462082
}
20472083

2084+
private static CosmosException getNegativeTimeoutException(RxDocumentServiceRequest request, Duration negativeTimeout) {
2085+
checkNotNull(negativeTimeout, "Argument 'negativeTimeout' must not be null");
2086+
checkArgument(
2087+
negativeTimeout.isNegative(),
2088+
"This exception should only be used for negative timeouts");
2089+
2090+
String message = String.format("Negative timeout '%s' provided.", negativeTimeout);
2091+
CosmosException exception = new OperationCancelledException(message, null);
2092+
BridgeInternal.setSubStatusCode(exception, HttpConstants.SubStatusCodes.NEGATIVE_TIMEOUT_PROVIDED);
2093+
if (request != null && request.requestContext != null) {
2094+
request.requestContext.setIsRequestCancelledOnTimeout(new AtomicBoolean(true));
2095+
return BridgeInternal.setCosmosDiagnostics(exception, request.requestContext.cosmosDiagnostics);
2096+
}
2097+
2098+
return exception;
2099+
}
2100+
20482101
@Override
20492102
public Mono<ResourceResponse<Document>> upsertDocument(String collectionLink, Object document,
20502103
RequestOptions options, boolean disableAutomaticIdGeneration) {

0 commit comments

Comments
 (0)