Skip to content

Commit acf2ffa

Browse files
dougqhclaude
andcommitted
Tighten AggregateEntry / PeerTagSchema surface area
Three small cleanups that the recent design review surfaced: - Move test-only AggregateEntry.forSnapshot(SpanSnapshot) to AggregateEntryTestUtils. Production callers (AggregateTable.findOrInsert) already use the two-arg forSnapshot(snap, keyHash); the no-keyHash overload existed for tests. AggregateEntryTest now goes through the test helper. MetricsIntegrationTest can't see src/test, so it inlines forSnapshot(snap, hashOf(snap)) using the production API directly. - Change AggregateEntry.recordOneDuration to return void. Returned `this` for fluent-style chaining but the only caller (Aggregator.accept) discards the return. - Remove PeerTagSchema.hashCode/equals + cachedHashCode field. Used only by AggregateEntry.hashOf, which now inlines Arrays.hashCode(schema.names) with an explicit null guard. Drops 42 lines from PeerTagSchema and three now-redundant equals tests from PeerTagSchemaTest -- the schema's identity contract is enforced by the hash function and hasSameTagsAs rather than the Object#equals contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a06c2a8 commit acf2ffa

6 files changed

Lines changed: 36 additions & 114 deletions

File tree

dd-trace-core/src/main/java/datadog/trace/common/metrics/AggregateEntry.java

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -161,20 +161,11 @@ static AggregateEntry forSnapshot(SpanSnapshot s, long keyHash) {
161161
return new AggregateEntry(s, keyHash);
162162
}
163163

164-
/**
165-
* Convenience overload that computes the hash itself. For test callers that don't have a
166-
* precomputed hash on hand; the production path goes through {@link #forSnapshot(SpanSnapshot,
167-
* long)} from {@link AggregateTable#findOrInsert}.
168-
*/
169-
static AggregateEntry forSnapshot(SpanSnapshot s) {
170-
return new AggregateEntry(s, hashOf(s));
171-
}
172-
173164
/**
174165
* Records a single hit. {@code tagAndDuration} carries the duration nanos with optional {@link
175166
* #ERROR_TAG} / {@link #TOP_LEVEL_TAG} bits OR-ed in.
176167
*/
177-
AggregateEntry recordOneDuration(long tagAndDuration) {
168+
void recordOneDuration(long tagAndDuration) {
178169
++hitCount;
179170
if ((tagAndDuration & TOP_LEVEL_TAG) == TOP_LEVEL_TAG) {
180171
tagAndDuration ^= TOP_LEVEL_TAG;
@@ -188,7 +179,6 @@ AggregateEntry recordOneDuration(long tagAndDuration) {
188179
okLatencies.accept(tagAndDuration);
189180
}
190181
duration += tagAndDuration;
191-
return this;
192182
}
193183

194184
int getErrorCount() {
@@ -279,11 +269,13 @@ static long hashOf(SpanSnapshot s) {
279269
h = LongHashingUtils.addToHash(h, s.traceRoot);
280270
h = LongHashingUtils.addToHash(h, s.spanKind);
281271
// Always mix in both the schema's content hash and the values' content hash, unconditionally
282-
// (no null-skip). PeerTagSchema overrides hashCode() to be content-based on names; we use
283-
// Arrays.hashCode for the String[] values since the default Object[].hashCode is identity-
284-
// based, not content-based. Null inputs hash to 0 for both, distinct from any real schema's
285-
// hash or any non-empty values array.
286-
h = LongHashingUtils.addToHash(h, s.peerTagSchema);
272+
// (no null-skip). Arrays.hashCode is content-based for both String[]s; the default
273+
// Object[].hashCode is identity-based, which would let two snapshots with content-equal but
274+
// distinct PeerTagSchema instances hash to different buckets. Null inputs hash to 0 here,
275+
// distinct from {@code Arrays.hashCode(empty)} = 1 or any non-empty array.
276+
h =
277+
LongHashingUtils.addToHash(
278+
h, s.peerTagSchema == null ? 0 : Arrays.hashCode(s.peerTagSchema.names));
287279
h = LongHashingUtils.addToHash(h, Arrays.hashCode(s.peerTagValues));
288280
h = LongHashingUtils.addToHash(h, s.httpMethod);
289281
h = LongHashingUtils.addToHash(h, s.httpEndpoint);

dd-trace-core/src/main/java/datadog/trace/common/metrics/PeerTagSchema.java

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import static datadog.trace.api.DDTags.BASE_SERVICE;
44

55
import datadog.communication.ddagent.DDAgentFeaturesDiscovery;
6-
import java.util.Arrays;
76
import java.util.Set;
87

98
/**
@@ -53,15 +52,6 @@ final class PeerTagSchema {
5352
*/
5453
String state;
5554

56-
/**
57-
* Lazily computed content hash of {@link #names}, used as the bucket-distinguishing contribution
58-
* when {@link AggregateEntry#hashOf} hashes a snapshot's peer-tag schema. Benign race pattern: a
59-
* concurrent first-time read may recompute the value, but {@link Arrays#hashCode(Object[])} on
60-
* the same content array is deterministic so the recomputed value matches. {@code int} writes are
61-
* atomic per JLS.
62-
*/
63-
private int cachedHashCode;
64-
6555
private PeerTagSchema(String[] names, String state) {
6656
this.names = names;
6757
this.state = state;
@@ -101,36 +91,4 @@ boolean hasSameTagsAs(Set<String> other) {
10191
int size() {
10292
return names.length;
10393
}
104-
105-
/**
106-
* Content-based hash of {@link #names}. Used by {@link AggregateEntry#hashOf} to incorporate the
107-
* schema identity into a snapshot's lookup hash. Distinct schemas with the same names hash to the
108-
* same value so an entry built under one schema instance still matches a snapshot pinned to a
109-
* content-equal replacement (e.g. after reconcile rebuilds the schema).
110-
*/
111-
@Override
112-
public int hashCode() {
113-
int h = cachedHashCode;
114-
if (h == 0) {
115-
h = Arrays.hashCode(names);
116-
cachedHashCode = h;
117-
}
118-
return h;
119-
}
120-
121-
/**
122-
* Content equality on {@link #names}. {@link #state} is intentionally excluded: it is a
123-
* reconcile-bookkeeping field, not part of the schema's identity. Two schemas built from the same
124-
* tag list at different discovery snapshots represent the same schema.
125-
*/
126-
@Override
127-
public boolean equals(Object o) {
128-
if (this == o) {
129-
return true;
130-
}
131-
if (!(o instanceof PeerTagSchema)) {
132-
return false;
133-
}
134-
return Arrays.equals(names, ((PeerTagSchema) o).names);
135-
}
13694
}

dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ void testUtilsEqualsIsConsistentWithHashCodeAcrossDifferentSchemaLayouts() {
9595
// A: schema ["a","b"], values [null,"x"] -> encoded ["b:x"]
9696
// B: schema ["b","c"], values ["x",null] -> encoded ["b:x"]
9797
AggregateEntry a =
98-
AggregateEntry.forSnapshot(
98+
AggregateEntryTestUtils.forSnapshot(
9999
snapshotWithPeerTags(new String[] {"a", "b"}, new String[] {null, "x"}));
100100
AggregateEntry b =
101-
AggregateEntry.forSnapshot(
101+
AggregateEntryTestUtils.forSnapshot(
102102
snapshotWithPeerTags(new String[] {"b", "c"}, new String[] {"x", null}));
103103

104104
// Sanity: same encoded peer tags, despite different raw layout.
@@ -113,10 +113,10 @@ void testUtilsEqualsIsConsistentWithHashCodeAcrossDifferentSchemaLayouts() {
113113
@Test
114114
void testUtilsEqualEntriesHaveEqualHashCodes() {
115115
AggregateEntry a =
116-
AggregateEntry.forSnapshot(
116+
AggregateEntryTestUtils.forSnapshot(
117117
snapshotWithPeerTags(new String[] {"a", "b"}, new String[] {null, "x"}));
118118
AggregateEntry b =
119-
AggregateEntry.forSnapshot(
119+
AggregateEntryTestUtils.forSnapshot(
120120
snapshotWithPeerTags(new String[] {"a", "b"}, new String[] {null, "x"}));
121121

122122
assertTrue(AggregateEntryTestUtils.equals(a, b));
@@ -160,6 +160,6 @@ private static AggregateEntry newEntry() {
160160
null,
161161
null,
162162
0L);
163-
return AggregateEntry.forSnapshot(snapshot);
163+
return AggregateEntryTestUtils.forSnapshot(snapshot);
164164
}
165165
}

dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTestUtils.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ private AggregateEntryTestUtils() {}
3636
* <p><b>Test-only.</b> The split is at the <em>first</em> {@code ':'}, so peer-tag values
3737
* containing a colon (URLs, IPv6 addresses, {@code service:env} patterns) will be silently
3838
* misparsed and the recovered (name, value) pair will be wrong. Keep test data colon-free in
39-
* peer-tag values, or wire a production-style snapshot through {@link
40-
* AggregateEntry#forSnapshot(SpanSnapshot)} directly instead.
39+
* peer-tag values, or wire a production-style snapshot through {@link #forSnapshot(SpanSnapshot)}
40+
* directly instead.
4141
*/
4242
public static AggregateEntry of(
4343
CharSequence resource,
@@ -85,7 +85,18 @@ public static AggregateEntry of(
8585
httpEndpoint == null ? null : httpEndpoint.toString(),
8686
grpcStatusCode == null ? null : grpcStatusCode.toString(),
8787
0L);
88-
return AggregateEntry.forSnapshot(syntheticSnapshot);
88+
return forSnapshot(syntheticSnapshot);
89+
}
90+
91+
/**
92+
* Builds an {@link AggregateEntry} from {@code s} by computing its lookup hash via {@link
93+
* AggregateEntry#hashOf(SpanSnapshot)} and delegating to {@link
94+
* AggregateEntry#forSnapshot(SpanSnapshot, long)}. Production callers route through {@link
95+
* AggregateTable#findOrInsert} which already has the {@code keyHash} on hand; tests rarely do, so
96+
* this helper hides the second argument.
97+
*/
98+
public static AggregateEntry forSnapshot(SpanSnapshot s) {
99+
return AggregateEntry.forSnapshot(s, AggregateEntry.hashOf(s));
89100
}
90101

91102
/**

dd-trace-core/src/test/java/datadog/trace/common/metrics/PeerTagSchemaTest.java

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
44
import static org.junit.jupiter.api.Assertions.assertEquals;
55
import static org.junit.jupiter.api.Assertions.assertFalse;
6-
import static org.junit.jupiter.api.Assertions.assertNotEquals;
76
import static org.junit.jupiter.api.Assertions.assertNull;
87
import static org.junit.jupiter.api.Assertions.assertTrue;
98

@@ -91,44 +90,4 @@ void hasSameTagsAsHandlesEmpty() {
9190
assertTrue(empty.hasSameTagsAs(Collections.<String>emptySet()));
9291
assertFalse(empty.hasSameTagsAs(Collections.<String>singleton("peer.hostname")));
9392
}
94-
95-
@Test
96-
void equalsIsContentBasedOnNames() {
97-
PeerTagSchema a = PeerTagSchema.testSchema(new String[] {"peer.hostname", "peer.service"});
98-
PeerTagSchema b = PeerTagSchema.testSchema(new String[] {"peer.hostname", "peer.service"});
99-
100-
assertEquals(a, b);
101-
assertEquals(b, a);
102-
assertEquals(a.hashCode(), b.hashCode());
103-
}
104-
105-
@Test
106-
void equalsIgnoresState() {
107-
// state is a reconcile-bookkeeping field, not part of schema identity.
108-
PeerTagSchema early =
109-
PeerTagSchema.of(Collections.<String>singleton("peer.hostname"), "state-1");
110-
PeerTagSchema late =
111-
PeerTagSchema.of(Collections.<String>singleton("peer.hostname"), "state-2");
112-
113-
assertEquals(early, late);
114-
assertEquals(early.hashCode(), late.hashCode());
115-
}
116-
117-
@Test
118-
void equalsDistinguishesByOrder() {
119-
// names is positional -- the array index pairs with SpanSnapshot.peerTagValues. Schemas with
120-
// the same tags in different positions are NOT interchangeable.
121-
PeerTagSchema ab = PeerTagSchema.testSchema(new String[] {"a", "b"});
122-
PeerTagSchema ba = PeerTagSchema.testSchema(new String[] {"b", "a"});
123-
124-
assertNotEquals(ab, ba);
125-
}
126-
127-
@Test
128-
void equalsHandlesNullAndOtherTypes() {
129-
PeerTagSchema schema = PeerTagSchema.testSchema(new String[] {"peer.hostname"});
130-
131-
assertNotEquals(schema, null);
132-
assertNotEquals(schema, "peer.hostname");
133-
}
13493
}

dd-trace-core/src/traceAgentTest/groovy/MetricsIntegrationTest.groovy

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,20 @@ class MetricsIntegrationTest extends AbstractTraceAgentTest {
3939
sink
4040
)
4141
writer.startBucket(2, System.nanoTime(), SECONDS.toNanos(10))
42-
// Build entries via SpanSnapshot directly: the test factory lives in src/test/java but this
43-
// is the separate traceAgentTest source set, so we can't see it. Both entries use one peer
44-
// tag (grault:quux) -> schema names=["grault"], values=["quux"].
42+
// Build entries via the production AggregateEntry.forSnapshot(snap, keyHash) path -- same
43+
// construction as AggregateTable.findOrInsert. Both entries use one peer tag (grault:quux)
44+
// -> schema names=["grault"], values=["quux"].
4545
PeerTagSchema schema = PeerTagSchema.testSchema(["grault"] as String[])
46-
def entry1 = AggregateEntry.forSnapshot(new SpanSnapshot(
46+
SpanSnapshot snap1 = new SpanSnapshot(
4747
"resource1", "service1", "operation1", null, "sql", (short) 0,
48-
false, true, "xyzzy", schema, ["quux"] as String[], null, null, null, 0L))
48+
false, true, "xyzzy", schema, ["quux"] as String[], null, null, null, 0L)
49+
def entry1 = AggregateEntry.forSnapshot(snap1, AggregateEntry.hashOf(snap1))
4950
[2, 1, 2, 250, 4].each { entry1.recordOneDuration(it as long) }
5051
writer.add(entry1)
51-
def entry2 = AggregateEntry.forSnapshot(new SpanSnapshot(
52+
SpanSnapshot snap2 = new SpanSnapshot(
5253
"resource2", "service2", "operation2", null, "web", (short) 200,
53-
false, true, "xyzzy", schema, ["quux"] as String[], null, null, null, 0L))
54+
false, true, "xyzzy", schema, ["quux"] as String[], null, null, null, 0L)
55+
def entry2 = AggregateEntry.forSnapshot(snap2, AggregateEntry.hashOf(snap2))
5456
[1, 1, 200, 2, 3, 4, 5, 6, 7, 8].each { entry2.recordOneDuration(it as long) }
5557
writer.add(entry2)
5658
writer.finishBucket()

0 commit comments

Comments
 (0)