Make Telemetry Tags Immutable - #20788
Conversation
PR Reviewer Guide 🔍(Review updated until commit 47bebbd)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 47bebbd Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit ed54255
Suggestions up to commit 08b48c1
Suggestions up to commit 078f77f
Suggestions up to commit fa2424a
Suggestions up to commit aa323b0
|
|
❌ Gradle check result for 94a2dab: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
1fb7782 to
8a5d0e6
Compare
…ed hash Replace the mutable HashMap-backed Tags with an immutable design using sorted parallel arrays and a precomputed hashCode. Adds allocation-efficient factories (Tags.of, Tags.concat, Tags.fromMap, Tags.toMap), an EMPTY singleton, and content-based equals/hashCode so Tags can be safely used as map keys, stored in fields, and shared across threads. Deprecated API (create(), addTag()) preserved for backward compatibility; addTag() now returns a new instance instead of mutating in place. Fixes AutoForceMergeMetrics which called addTag() without reassigning the return value -- a silent no-op now that Tags is immutable. Signed-off-by: Sam Akrah <sakrah@uber.com> Made-with: Cursor Signed-off-by: Sam Akrah <sakrah@uber.com>
8a5d0e6 to
cf4e273
Compare
|
Persistent review updated to latest commit cf4e273 |
|
Persistent review updated to latest commit 08b48c1 |
Tag values are now validated at creation time to ensure only the four types supported by telemetry providers (OTel, Micrometer) are accepted. Unsupported types throw IllegalArgumentException immediately rather than failing silently at the provider boundary. Signed-off-by: Sam Akrah <sakrah@uber.com>
|
Persistent review updated to latest commit ed54255 |
msfroh
left a comment
There was a problem hiding this comment.
@reta -- It looks like @sakrah made the type validation a runtime thing w/ instanceof checks. What are your thoughts on that?
I was kind of thinking of limiting the of methods to a single pair, with overloads for the four valid value types. (As mentioned previously, we can still keep ofStringPairs, since the (String, String) case should be common enough that people would want a convenience method.)
Signed-off-by: Sam Akrah <sakrah@uber.com>
|
Persistent review updated to latest commit 47bebbd |
|
❕ Gradle check result for 47bebbd: UNSTABLE Please review all flaky tests that succeeded after retry and create an issue if one does not already exist to track the flaky failure. |
Replace the mutable HashMap-backed Tags with an immutable design using sorted parallel arrays and a precomputed hashCode. Adds allocation-efficient factories (Tags.of, Tags.concat, Tags.fromMap, Tags.toMap), an EMPTY singleton, and content-based equals/hashCode so Tags can be safely used as map keys, stored in fields, and shared across threads. Deprecated API (create(), addTag()) preserved for backward compatibility; addTag() now returns a new instance instead of mutating in place. Fixes AutoForceMergeMetrics which called addTag() without reassigning the return value -- a silent no-op now that Tags is immutable. --------- Signed-off-by: Sam Akrah <sakrah@uber.com> Co-authored-by: Sam Akrah <sakrah@uber.com> Signed-off-by: Deepti24 <chauhan.deepti24@gmail.com>
Replace the mutable HashMap-backed Tags with an immutable design using sorted parallel arrays and a precomputed hashCode. Adds allocation-efficient factories (Tags.of, Tags.concat, Tags.fromMap, Tags.toMap), an EMPTY singleton, and content-based equals/hashCode so Tags can be safely used as map keys, stored in fields, and shared across threads. Deprecated API (create(), addTag()) preserved for backward compatibility; addTag() now returns a new instance instead of mutating in place. Fixes AutoForceMergeMetrics which called addTag() without reassigning the return value -- a silent no-op now that Tags is immutable. --------- Signed-off-by: Sam Akrah <sakrah@uber.com> Co-authored-by: Sam Akrah <sakrah@uber.com> Signed-off-by: Aparajita Pandey <aparajita31pandey@gmail.com>
Replace the mutable HashMap-backed Tags with an immutable design using sorted parallel arrays and a precomputed hashCode. Adds allocation-efficient factories (Tags.of, Tags.concat, Tags.fromMap, Tags.toMap), an EMPTY singleton, and content-based equals/hashCode so Tags can be safely used as map keys, stored in fields, and shared across threads. Deprecated API (create(), addTag()) preserved for backward compatibility; addTag() now returns a new instance instead of mutating in place. Fixes AutoForceMergeMetrics which called addTag() without reassigning the return value -- a silent no-op now that Tags is immutable. --------- Signed-off-by: Sam Akrah <sakrah@uber.com> Co-authored-by: Sam Akrah <sakrah@uber.com>
Problem
The
Tagsclass inlibs/telemetryis the dimension container for every metric emission in OpenSearch. On high-throughput clusters, tag construction and lookup become a significant CPU overhead on the metrics hot path -- before any actual metric work (atomic increment, gauge read) occurs.The root cause is structural:
HashMapbackend -- everyTags.create()allocates a newHashMap. Every metric call that passes tags pays this cost, even when the exact same tag combination fires millions of times per second.Tagsis mutable and has nohashCode()/equals()overrides, metric caches require defensive copies on every lookup, and every consumer must build its own caching layer.This means the existing API structurally prevents zero-allocation metric hot paths. Caching-only approaches reduce overhead but cannot eliminate it -- the cache key itself is an allocation, and every new metric consumer rediscovers the same problem.
Solution
Replace the mutable
HashMap-backedTagswith an immutable sorted-array implementation with a precomputed hash, inspired by Micrometer'sTags(sorted-array merge) and OpenTelemetry'sArrayBackedAttributes(parallel arrays, precomputed hash, last-wins dedup).Internal storage
New API
Tags.of(key, value)Tags.of(k1, v1, k2, v2)Tags.of(k1, v1, k2, v2, k3, v3)Tags.of(k1, v1, ..., k4, v4)Tags.of(String... keyValues)Tags.concat(a, b)bwins on collisionTags.fromMap(Map)size(),getKey(i),getValue(i)equals(),hashCode()Backward compatibility
Tags.create()and alladdTag()overloads are deprecated, not removed.create()returnsTags.EMPTY. EachaddTag()returns a new immutable instance viaTags.concat(this, Tags.of(key, value)). Existing fluent chains likeTags.create().addTag("a", "1").addTag("b", "2")compile and produce correct results -- they just allocate more than the equivalentTags.of("a", "1", "b", "2").Why this matters for downstream consumers
With immutable Tags and a precomputed hash, metric consumers can:
concat()on pre-sorted arraysTest plan
TagsTests.javacovering:ofoverloads,fromMap, varargs)concat()-- partial overlap, full overlap, interleaved, large remainder, hash consistency, null/empty inputsequals()/hashCode()contractgetTagsMap()(String conversion, type preservation)create(),addTag()chaining, all value types)./gradlew :libs:opensearch-telemetry:precommitpassesRelated Issues
Resolves #[Issue number to be closed when this PR is merged]
Check List
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.