Skip to content

Commit 3abcfbb

Browse files
xinlian12Copilot
andauthored
perf(cosmos): strip unused fields when constructing PartitionKeyRange from ObjectNode (#49513)
* perf(cosmos): strip unused fields when constructing PartitionKeyRange from ObjectNode --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e6c0b2c commit 3abcfbb

3 files changed

Lines changed: 220 additions & 14 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.azure.cosmos.implementation;
4+
5+
import com.azure.cosmos.implementation.routing.Range;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import com.fasterxml.jackson.databind.node.ObjectNode;
8+
import org.testng.annotations.Test;
9+
10+
import java.util.Arrays;
11+
import java.util.List;
12+
13+
import static org.testng.Assert.assertEquals;
14+
import static org.testng.Assert.assertNotNull;
15+
import static org.testng.Assert.assertTrue;
16+
17+
/**
18+
* Tests for {@link PartitionKeyRange}, focused on the memory-saving "strip unused fields" behavior
19+
* applied when constructing from a Jackson {@link ObjectNode}.
20+
*
21+
* <p>The retained-field set is intentionally kept aligned with
22+
* <a href="https://github.com/Azure/azure-sdk-for-python/pull/46297">azure-sdk-for-python#46297</a>
23+
* (Python's {@code PKRange} namedtuple). These tests pin that contract.</p>
24+
*/
25+
public class PartitionKeyRangeTest {
26+
27+
private static final ObjectMapper MAPPER = Utils.getSimpleObjectMapper();
28+
29+
/** Mirrors the JSON shape the Cosmos DB service returns for a partition key range. */
30+
private static final String FULL_PK_RANGE_JSON =
31+
"{"
32+
+ "\"_rid\":\"90t-ALzvP44CAAAAAAAAUA==\","
33+
+ "\"id\":\"0\","
34+
+ "\"_etag\":\"\\\"00001e02-0000-0800-0000-6a2c41690000\\\"\","
35+
+ "\"minInclusive\":\"\","
36+
+ "\"maxExclusive\":\"FF\","
37+
+ "\"ridPrefix\":0,"
38+
+ "\"_self\":\"dbs/90t-AA==/colls/90t-ALzvP44=/pkranges/90t-ALzvP44CAAAAAAAAUA==/\","
39+
+ "\"throughputFraction\":1,"
40+
+ "\"status\":\"online\","
41+
+ "\"parents\":[],"
42+
+ "\"ownedArchivalPKRangeIds\":[],"
43+
+ "\"_ts\":1781285225,"
44+
+ "\"lsn\":87,"
45+
+ "\"_lsn\":87"
46+
+ "}";
47+
48+
/**
49+
* Allow-list kept in heap on every deserialized {@link PartitionKeyRange}.
50+
*
51+
* <p>Includes Python's {@code PKRange} namedtuple slots
52+
* ({@code id}, {@code minInclusive}, {@code maxExclusive}, {@code parents},
53+
* {@code status}, {@code throughputFraction}) plus {@code _rid} — Java-specific because
54+
* {@code AddressResolver.isSameCollection} reads {@code getResourceId()} on a
55+
* {@code PartitionKeyRange} during retry target-change detection.</p>
56+
*/
57+
private static final List<String> KEPT_FIELDS = Arrays.asList(
58+
"id", "minInclusive", "maxExclusive", "parents", "status", "throughputFraction", "_rid");
59+
60+
/** All non-kept fields present in the full payload above; everything here must be stripped. */
61+
private static final List<String> STRIPPED_FIELDS = Arrays.asList(
62+
"_etag", "ridPrefix", "_self", "ownedArchivalPKRangeIds", "_ts", "lsn", "_lsn");
63+
64+
private static ObjectNode fullPkRangeNode() throws Exception {
65+
return (ObjectNode) MAPPER.readTree(FULL_PK_RANGE_JSON);
66+
}
67+
68+
@Test(groups = "unit")
69+
public void objectNodeConstructor_stripsEverythingNotOnAllowList() throws Exception {
70+
// Pin the allow-list: every field not on Python's PKRange namedtuple must be dropped.
71+
ObjectNode node = fullPkRangeNode();
72+
PartitionKeyRange range = new PartitionKeyRange(node);
73+
74+
for (String dropped : STRIPPED_FIELDS) {
75+
assertEquals(range.has(dropped), false, dropped + " must be stripped (not on allow-list)");
76+
}
77+
}
78+
79+
@Test(groups = "unit")
80+
public void objectNodeConstructor_preservesAllowListedFields() throws Exception {
81+
// Every field on the allow-list must survive the strip.
82+
ObjectNode node = fullPkRangeNode();
83+
PartitionKeyRange range = new PartitionKeyRange(node);
84+
85+
// Routing-map essentials.
86+
assertEquals(range.getId(), "0");
87+
assertEquals(range.getMinInclusive(), "");
88+
assertEquals(range.getMaxExclusive(), "FF");
89+
assertNotNull(range.getParents());
90+
assertEquals(range.getParents().size(), 0);
91+
92+
// _rid is on the allow-list specifically so AddressResolver.isSameCollection
93+
// can call getResourceId() on a deserialized PartitionKeyRange during retry
94+
// target-change detection. Without this, ResourceId.parse(null) throws
95+
// "INVALID resource id null" on the first retry after a 410/Gone.
96+
assertEquals(range.getResourceId(), "90t-ALzvP44CAAAAAAAAUA==");
97+
98+
for (String kept : KEPT_FIELDS) {
99+
assertTrue(range.has(kept), kept + " must be preserved (on allow-list)");
100+
}
101+
}
102+
103+
@Test(groups = "unit")
104+
public void objectNodeConstructor_dropsUnknownFutureField() throws Exception {
105+
// Allow-list (not deny-list) semantics: a new server-side field tomorrow is dropped
106+
// by default so per-instance heap stays bounded against payload growth. Mirrors
107+
// Python's PKRange namedtuple, which has no slot for unknown fields.
108+
String json = "{"
109+
+ "\"id\":\"0\",\"minInclusive\":\"\",\"maxExclusive\":\"FF\","
110+
+ "\"futureFieldA\":\"hello\","
111+
+ "\"futureFieldB\":{\"nested\":42}"
112+
+ "}";
113+
ObjectNode node = (ObjectNode) MAPPER.readTree(json);
114+
PartitionKeyRange range = new PartitionKeyRange(node);
115+
116+
assertEquals(range.getId(), "0");
117+
assertEquals(range.has("futureFieldA"), false, "unknown future field must be dropped by allow-list");
118+
assertEquals(range.has("futureFieldB"), false, "unknown future nested field must be dropped by allow-list");
119+
}
120+
121+
@Test(groups = "unit")
122+
public void objectNodeConstructor_preservesNonEmptyParents() throws Exception {
123+
// Split-merge bookkeeping uses parents; verify it survives the strip.
124+
String json = "{"
125+
+ "\"_rid\":\"X==\",\"id\":\"1\",\"_etag\":\"\\\"e\\\"\","
126+
+ "\"minInclusive\":\"\",\"maxExclusive\":\"FF\","
127+
+ "\"_self\":\"x/\",\"parents\":[\"0\"],\"_ts\":1,\"lsn\":1"
128+
+ "}";
129+
ObjectNode node = (ObjectNode) MAPPER.readTree(json);
130+
PartitionKeyRange range = new PartitionKeyRange(node);
131+
132+
assertNotNull(range.getParents());
133+
assertEquals(range.getParents().size(), 1);
134+
assertEquals(range.getParents().get(0), "0");
135+
// Dropped fields still gone. _rid stays now that it's on the allow-list.
136+
assertEquals(range.has("_self"), false);
137+
assertEquals(range.has("lsn"), false);
138+
assertEquals(range.getResourceId(), "X==");
139+
}
140+
141+
@Test(groups = "unit")
142+
public void objectNodeConstructor_equalsAndHashCodeUnchanged() throws Exception {
143+
// PartitionKeyRange#equals / #hashCode use id/min/max only -- the slim instance must
144+
// remain value-equal to a manually-constructed instance with the same identity fields.
145+
ObjectNode node = fullPkRangeNode();
146+
PartitionKeyRange slim = new PartitionKeyRange(node);
147+
PartitionKeyRange handBuilt = new PartitionKeyRange("0", "", "FF");
148+
149+
assertEquals(slim, handBuilt);
150+
assertEquals(slim.hashCode(), handBuilt.hashCode());
151+
assertEquals(slim.toRange(), new Range<>("", "FF", true, false));
152+
}
153+
154+
@Test(groups = "unit")
155+
public void objectNodeConstructor_handlesNull() {
156+
// Defensive: a null ObjectNode argument must not throw; super(null) is the existing
157+
// pre-PR contract and must be preserved.
158+
new PartitionKeyRange((ObjectNode) null);
159+
}
160+
161+
@Test(groups = "unit")
162+
public void deserializationFunnelStripsForFeedResponsePath() throws Exception {
163+
// JsonSerializable.instantiateFromObjectNodeAndType is the funnel every FeedResponse
164+
// page uses when deserializing pkranges. Confirm it routes through the
165+
// PartitionKeyRange(ObjectNode) ctor and therefore inherits the strip.
166+
ObjectNode node = fullPkRangeNode();
167+
Object result =
168+
JsonSerializable.instantiateFromObjectNodeAndType(node, PartitionKeyRange.class);
169+
170+
assertTrue(result instanceof PartitionKeyRange);
171+
PartitionKeyRange range = (PartitionKeyRange) result;
172+
for (String dropped : STRIPPED_FIELDS) {
173+
assertEquals(range.has(dropped), false, dropped + " must be stripped via FeedResponse funnel");
174+
}
175+
assertEquals(range.getId(), "0");
176+
}
177+
}

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+
* Reduced memory footprint of deserialized `PartitionKeyRange` instances by stripping unused fields in the `PartitionKeyRange(ObjectNode)` constructor - See PR [49513](https://github.com/Azure/azure-sdk-for-java/pull/49513).
1213

1314
### 4.81.0 (2026-06-08)
1415

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

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
import com.azure.cosmos.implementation.routing.Range;
77
import com.fasterxml.jackson.databind.node.ObjectNode;
88

9+
import java.util.Arrays;
10+
import java.util.Collections;
11+
import java.util.HashSet;
912
import java.util.List;
13+
import java.util.Set;
1014

1115
/**
1216
* Represent a partition key range in the Azure Cosmos DB database service.
@@ -16,14 +20,49 @@ public class PartitionKeyRange extends Resource {
1620
public static final String MAXIMUM_EXCLUSIVE_EFFECTIVE_PARTITION_KEY = "FF";
1721
public static final String MASTER_PARTITION_KEY_RANGE_ID = "M";
1822

23+
/**
24+
* <p>This is an <b>allow-list</b>: any field the service returns that is not in this set
25+
* (including any field added by the service in the future) is dropped at construction.
26+
* That keeps per-instance heap bounded against server-side payload growth. Adding a new
27+
* field to the allow-list is a one-line change here when a consumer needs it.</p>
28+
*/
29+
private static final Set<String> KEPT_FIELDS = Collections.unmodifiableSet(
30+
new HashSet<>(Arrays.asList(
31+
Constants.Properties.ID,
32+
"minInclusive",
33+
"maxExclusive",
34+
Constants.Properties.PARENTS,
35+
"status",
36+
"throughputFraction",
37+
Constants.Properties.R_ID
38+
)));
39+
1940
/**
2041
* Constructor.
2142
*
22-
* @param objectNode the {@link ObjectNode} that represent the
23-
* {@link JsonSerializable}
43+
* <p>Fields not listed in {@link #KEPT_FIELDS} are removed from {@code objectNode} as part of
44+
* construction so the resulting instance retains only the fields the SDK actually needs.
45+
* This is the universal funnel for every {@code PartitionKeyRange} the SDK deserializes from
46+
* a service response (see {@link JsonSerializable#instantiateFromObjectNodeAndType}), so the
47+
* memory saving applies to all routing-map cache entries and any other code path that
48+
* consumes deserialized partition key ranges.</p>
49+
*
50+
* <p>The argument is mutated in place. This is safe because every production caller obtains
51+
* {@code objectNode} from Jackson deserialization and does not retain another reference to
52+
* it. Tests that need to preserve a fully-populated source object should use
53+
* {@code objectNode.deepCopy()} before handing it to this constructor.</p>
54+
*
55+
* @param objectNode the {@link ObjectNode} that represents the {@link JsonSerializable}
2456
*/
2557
public PartitionKeyRange(ObjectNode objectNode) {
26-
super(objectNode);
58+
super(stripToKeptFields(objectNode));
59+
}
60+
61+
private static ObjectNode stripToKeptFields(ObjectNode objectNode) {
62+
if (objectNode != null) {
63+
objectNode.retain(KEPT_FIELDS);
64+
}
65+
return objectNode;
2766
}
2867

2968
/**
@@ -33,17 +72,6 @@ public PartitionKeyRange() {
3372
super();
3473
}
3574

36-
/**
37-
* Initialize a partition key range object from json string.
38-
*
39-
* @param jsonString
40-
* the json string that represents the partition key range
41-
* object.
42-
*/
43-
public PartitionKeyRange(String jsonString) {
44-
super(jsonString);
45-
}
46-
4775
/**
4876
* Set id of partition key range
4977
* @param id the name of the resource.

0 commit comments

Comments
 (0)