Skip to content

Commit 6c60dbc

Browse files
FabianMeiswinkelxinlian12Copilot
authored
Fixes UnsupportedOperationException when using readManyByPartitionKey for empty pages (#49311)
* Fixes UnsupportedOperationException when using readManyByPartitionKey for empty pages * Updated changelogs * Update spark.yml JarStorageAccountName for ephemeral tenant rotation 202605 Update JarStorageAccountName from oltpsparkcijarstore0326 to oltpsparkcijarstore0529 to point to the new storage account provisioned in the current ephemeral tenant rotation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Keeping FeedResponse.header final --------- Co-authored-by: Annie Liang <xin.liang@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c8a68da commit 6c60dbc

11 files changed

Lines changed: 131 additions & 25 deletions

File tree

sdk/cosmos/azure-cosmos-spark_3-3_2-12/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#### Bugs Fixed
1010
* Improved partition planning performance for change feed with large number of feed ranges. - See [PR 49086](https://github.com/Azure/azure-sdk-for-java/pull/49086)
11+
* Fixed `UnsupportedOperationException` when using `readManyByPartitionKeys` for empty pages. - See [PR 49311](https://github.com/Azure/azure-sdk-for-java/pull/49311)
1112

1213
#### Other Changes
1314

sdk/cosmos/azure-cosmos-spark_3-4_2-12/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#### Bugs Fixed
1010
* Improved partition planning performance for change feed with large number of feed ranges. - See [PR 49086](https://github.com/Azure/azure-sdk-for-java/pull/49086)
11+
* Fixed `UnsupportedOperationException` when using `readManyByPartitionKeys` for empty pages. - See [PR 49311](https://github.com/Azure/azure-sdk-for-java/pull/49311)
1112

1213
#### Other Changes
1314

sdk/cosmos/azure-cosmos-spark_3-5_2-12/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#### Bugs Fixed
1010
* Improved partition planning performance for change feed with large number of feed ranges. - See [PR 49086](https://github.com/Azure/azure-sdk-for-java/pull/49086)
11+
* Fixed `UnsupportedOperationException` when using `readManyByPartitionKeys` for empty pages. - See [PR 49311](https://github.com/Azure/azure-sdk-for-java/pull/49311)
1112

1213
#### Other Changes
1314

sdk/cosmos/azure-cosmos-spark_3-5_2-13/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#### Bugs Fixed
1010
* Improved partition planning performance for change feed with large number of feed ranges. - See [PR 49086](https://github.com/Azure/azure-sdk-for-java/pull/49086)
11+
* Fixed `UnsupportedOperationException` when using `readManyByPartitionKeys` for empty pages. - See [PR 49311](https://github.com/Azure/azure-sdk-for-java/pull/49311)
1112

1213
#### Other Changes
1314

sdk/cosmos/azure-cosmos-spark_4-0_2-13/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#### Bugs Fixed
1010
* Improved partition planning performance for change feed with large number of feed ranges. - See [PR 49086](https://github.com/Azure/azure-sdk-for-java/pull/49086)
11+
* Fixed `UnsupportedOperationException` when using `readManyByPartitionKeys` for empty pages. - See [PR 49311](https://github.com/Azure/azure-sdk-for-java/pull/49311)
1112

1213
#### Other Changes
1314

sdk/cosmos/azure-cosmos-spark_4-1_2-13/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#### Bugs Fixed
1010
* Improved partition planning performance for change feed with large number of feed ranges. - See [PR 49086](https://github.com/Azure/azure-sdk-for-java/pull/49086)
11+
* Fixed `UnsupportedOperationException` when using `readManyByPartitionKeys` for empty pages. - See [PR 49311](https://github.com/Azure/azure-sdk-for-java/pull/49311)
1112

1213
#### Other Changes
1314

sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ReadManyByPartitionKeyContinuationTokenTest.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,17 +127,20 @@ public void roundtrip_lastBatchNoContinuation() {
127127

128128
@Test(groups = { "unit" })
129129
public void setFeedResponseContinuationToken_handlesEmptyHeadersWithoutCopyingNormalCase() {
130-
Map<String, String> immutableEmptyHeaders = Collections.emptyMap();
130+
// Immutable inputs are normalized to a mutable map at FeedResponse construction
131+
// time (so the field stays final). Clearing a continuation token on an empty
132+
// header map is a no-op and must not throw.
131133
FeedResponse<String> emptyResponse = ModelBridgeInternal.createFeedResponse(
132134
Collections.emptyList(),
133-
immutableEmptyHeaders);
135+
Collections.emptyMap());
134136

135137
ModelBridgeInternal.setFeedResponseContinuationToken(null, emptyResponse);
136138

137139
assertThat(emptyResponse.getContinuationToken()).isNull();
138-
assertThat(emptyResponse.getResponseHeaders()).isSameAs(immutableEmptyHeaders);
139140
assertThat(emptyResponse.getResponseHeaders()).isEmpty();
140141

142+
// Mutable header maps are passed through without copying, preserving the
143+
// reference returned by getResponseHeaders().
141144
Map<String, String> normalHeaders = new HashMap<>();
142145
normalHeaders.put(HttpConstants.HttpHeaders.ACTIVITY_ID, "test-activity-id");
143146
FeedResponse<String> normalResponse = ModelBridgeInternal.createFeedResponse(
@@ -150,6 +153,30 @@ public void setFeedResponseContinuationToken_handlesEmptyHeadersWithoutCopyingNo
150153
assertThat(normalResponse.getResponseHeaders()).isSameAs(normalHeaders);
151154
}
152155

156+
/**
157+
* Reproduces the customer-reported failure path: the parallel query pipeline's
158+
* "artificial empty page" branch (ParallelDocumentQueryExecutionContext.headerResponse)
159+
* emits a FeedResponse whose header map is {@code Utils.immutableMapOf(...)} - i.e. a
160+
* non-empty {@code Collections.unmodifiableMap} wrapper. When such a page reaches the
161+
* readManyByPartitionKeys stamping lambda, setFeedResponseContinuationToken attempts
162+
* to put the composite token into the immutable map, throwing UnsupportedOperationException.
163+
*/
164+
@Test(groups = { "unit" })
165+
public void setFeedResponseContinuationToken_immutableNonEmptyHeaders_doesNotThrow() {
166+
Map<String, String> immutableSingleEntryHeaders =
167+
Utils.immutableMapOf(HttpConstants.HttpHeaders.REQUEST_CHARGE, "1.23");
168+
FeedResponse<String> response = ModelBridgeInternal.createFeedResponse(
169+
Collections.emptyList(),
170+
immutableSingleEntryHeaders);
171+
172+
ModelBridgeInternal.setFeedResponseContinuationToken("composite-token", response);
173+
174+
assertThat(response.getContinuationToken()).isEqualTo("composite-token");
175+
assertThat(response.getResponseHeaders())
176+
.containsEntry(HttpConstants.HttpHeaders.CONTINUATION, "composite-token")
177+
.containsEntry(HttpConstants.HttpHeaders.REQUEST_CHARGE, "1.23");
178+
}
179+
153180
@Test(groups = { "unit" })
154181
public void deserialize_malformedInput_throws() {
155182
// Either the base64 decoder or the JSON parsing layer rejects garbage; both raise
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
package com.azure.cosmos.implementation;
7+
8+
import org.testng.annotations.Test;
9+
10+
import java.util.Collections;
11+
import java.util.HashMap;
12+
import java.util.LinkedHashMap;
13+
import java.util.Map;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
17+
/**
18+
* Unit tests for {@link Utils#immutableMapOf(Object, Object)} and the matching detector
19+
* {@link Utils#isImmutableMap(Map)}. These live next to the factory so that any future
20+
* change to the factory's runtime class shape is caught by the same regression suite.
21+
*/
22+
public class UtilsImmutableMapTests {
23+
24+
@Test(groups = { "unit" })
25+
public void immutableMapOf_isDetectedAsImmutable() {
26+
Map<String, String> m = Utils.immutableMapOf("k", "v");
27+
assertThat(Utils.isImmutableMap(m)).isTrue();
28+
}
29+
30+
@Test(groups = { "unit" })
31+
public void emptyMap_isDetectedAsImmutable() {
32+
assertThat(Utils.isImmutableMap(Collections.emptyMap())).isTrue();
33+
}
34+
35+
@Test(groups = { "unit" })
36+
public void hashMap_isNotDetectedAsImmutable() {
37+
assertThat(Utils.isImmutableMap(new HashMap<>())).isFalse();
38+
Map<String, String> populated = new HashMap<>();
39+
populated.put("k", "v");
40+
assertThat(Utils.isImmutableMap(populated)).isFalse();
41+
}
42+
43+
@Test(groups = { "unit" })
44+
public void linkedHashMap_isNotDetectedAsImmutable() {
45+
assertThat(Utils.isImmutableMap(new LinkedHashMap<>())).isFalse();
46+
}
47+
48+
@Test(groups = { "unit" })
49+
public void nullMap_isNotDetectedAsImmutable() {
50+
assertThat(Utils.isImmutableMap(null)).isFalse();
51+
}
52+
}

sdk/cosmos/azure-cosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#### Breaking Changes
99

1010
#### Bugs Fixed
11+
* Fixed `UnsupportedOperationException` when using `readManyByPartitionKeys` for empty pages. - See [PR 49311](https://github.com/Azure/azure-sdk-for-java/pull/49311)
1112

1213
#### Other Changes
1314
* Replaced per-client `Schedulers.newSingle()` schedulers in `GlobalEndpointManager` and `GlobalPartitionEndpointManagerForPerPartitionCircuitBreaker` with shared `BoundedElastic` schedulers in `CosmosSchedulers` to prevent thread count from scaling linearly with client/tenant count. - See [PR 49062](https://github.com/Azure/azure-sdk-for-java/pull/49062)

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,33 @@ public static <V> List<V> immutableListOf() {
574574
return map;
575575
}
576576

577+
// Runtime classes produced by the immutable factory methods above. Captured once at
578+
// class-init time so that callers can perform an O(1) reference-equality check to
579+
// decide whether they need a defensive mutable copy, without resorting to
580+
// exception-driven probing on the hot path.
581+
private static final Class<?> UNMODIFIABLE_MAP_CLASS =
582+
Collections.unmodifiableMap(new HashMap<>()).getClass();
583+
private static final Class<?> EMPTY_MAP_CLASS = Collections.emptyMap().getClass();
584+
585+
/**
586+
* Returns {@code true} if {@code map} is one of the immutable map shapes produced by
587+
* the factory methods in this class ({@link #immutableMapOf(Object, Object)}) or by
588+
* {@link Collections#emptyMap()}. The check is a single reference comparison and is
589+
* safe to call from hot paths.
590+
* <p>
591+
* Note: this is intentionally narrow - it only recognizes the wrappers the Cosmos
592+
* pipeline actually emits. It does not attempt to recognize every possible JDK or
593+
* third-party immutable map (e.g. {@code Map.of(...)}, Guava {@code ImmutableMap}).
594+
* Add new sentinels here if a new immutable producer is introduced.
595+
*/
596+
public static boolean isImmutableMap(Map<?, ?> map) {
597+
if (map == null) {
598+
return false;
599+
}
600+
Class<?> clazz = map.getClass();
601+
return clazz == UNMODIFIABLE_MAP_CLASS || clazz == EMPTY_MAP_CLASS;
602+
}
603+
577604
public static <V> V firstOrDefault(List<V> list) {
578605
return list.size() > 0? list.get(0) : null ;
579606
}

0 commit comments

Comments
 (0)