Make the TagMap Entry pathway null-tolerant#11963
Make the TagMap Entry pathway null-tolerant#11963gh-worker-dd-mergequeue-cf854d[bot] merged 3 commits into
Conversation
create(Object)/create(CharSequence) may return null for a null or empty value, and the Entry sinks -- getAndSet(Entry) / set(EntryReader) -- treat a null Entry as a no-op, so a null/empty value flows through the Entry pathway as "no tag" without any caller guarding. create(Object) now applies the empty-CharSequence check by runtime type, so the null/empty => absent convention holds regardless of the static type at the call site. The strict (key,value) setters keep their contract -- their values are now @nonnull -- so null tolerance is scoped to the Entry pathway. The annotations make the split self-describing. Fixes a latent NPE by construction: RemoteHostnameAdder sets create(TRACER_HOST, hostname) guarding only null, not empty, so an empty hostname previously NPE'd on set(null). Caller-side guard cleanup (incl. the redundant #11958 guard) is left to a follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
🎯 Code Coverage (details) 🔗 Commit SHA: c451336 | Docs | Datadog PR Page | Give us feedback! |
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
A tag has no valid null key, so put/set/getAndSet/create now take @nonnull tag. This completes the null contract alongside the value/Entry side: keys are strict (null = a bug), values/Entries on the Entry pathway are tolerant (null = no tag). Scoped to the write/create surface; read/lookup keys (getString, remove, getXxxOrDefault) are left for a possible follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
More details
The null guard in getAndSet(Entry) is placed before checkWriteAccess(), which means a null entry on a frozen map silently returns null instead of throwing an AccessException — this is intentional and documented. All new and existing tests pass (4 + 181 + 112 = 297 tests); adversarial coverage confirmed the empty-StringBuilder-as-Object path, frozen-map no-op contract, and no-existing-tag-removal invariant all hold correctly.
📊 Validated against 20 scenarios · Open Bits AI session
🤖 Datadog Autotest · Commit fc7bc19 · What is Autotest? · Any feedback? Reach out in #autotest
The interface -> final-class fold removed the TagMap interface decls, which carried the @Nullable/@nonnull param annotations from #11963. Re-home them onto the now-concrete methods: @nonnull tag keys and strict-setter values, @nullable on the set(EntryReader)/getAndSet(Entry) sinks (+ the getAndSet contract javadoc). The null-tolerance behavior was already preserved by the fold; this restores the self-describing contract on the write surface. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A fresh, mutable TagMap can read through to a frozen parent on local misses, so a span can layer its own tags over a shared, immutable set (e.g. merged tracer tags) without copying them. - createFromParent(parent): the only way to attach a parent; the parent must be frozen and is fixed at construction (no re-parenting), so read-through can treat it as stable. Single-parent by design in phase 1. - Reads resolve local-first, then the parent; a local entry shadows the parent's (local-wins). Removing a parent key locally records a lazy tombstone (removedFromParent) so it stops reading through; the tombstone set is null until first needed, keeping the hot paths untouched. - size()/isEmpty() are exact (Map contract) and resolve the parent; isDefinitelyEmpty()/estimateSize() are the cheap conservative variants for the hot path. copy() preserves the parent and tombstones; forEach walks local then parent. Built on the folded final-class TagMap (#11967); composes cleanly with the null-tolerant Entry pathway (#11963). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
/merge |
|
View all feedbacks in Devflow UI.
The expected merge time in
|
What Does This Do
Makes the TagMap Entry pathway null-tolerant, so a null/empty value flows through as "no tag" without any caller guarding:
Entry.create(Object)/Entry.create(CharSequence)returnnullfor a null or empty value (@Nullable).create(Object)now applies the empty-CharSequencecheck by runtime type, so an empty String passed asObjectskips the same as via theCharSequenceoverload — behavior no longer depends on the static type at the call site.getAndSet(Entry)andset(EntryReader)— treat a null Entry as a no-op (@Nullableparams). Two guards becauseset(EntryReader)derefs.entry()before delegating.(key, value)setters keep their contract: their values (set(String, Object)/set(String, CharSequence)) are@Nonnull.tagkeys on the write/create surface (put/set/getAndSet/create) are@Nonnull— a null key is a bug, not "no tag". So null-tolerance is scoped to values and Entries on the Entry pathway; keys and strict-setter values are non-null. The annotations make the whole split self-describing.Motivation
Make handling easier - and avoid NPEs
The Entry pathway is the prebuilt/producer path (
create(...) → set(Entry)); a null Entry is the designed "nothing to add" value, mirroringDDSpanContext.setTag's null/empty ⇒ absent convention. Centralizing the tolerance in the primitive means callers needn't each remember a guard — which they don't: this fixes a latent NPE by construction inRemoteHostnameAdder, which setscreate(TRACER_HOST, hostname)guarding only null (not empty), so an empty hostname previously NPE'd onset(null).Notes
getAndSet's null check is the first statement, ahead ofcheckWriteAccess()and any field read, so it dead-code-eliminates when inlined at a provably-non-null call site (e.g.set(String,value) → getAndSet(newAnyEntry(...))) and stays only where the arg can actually be null. A null Entry is thus a complete no-op and does not assert write access.InternalTagsAdder— is a fast follow-up.@Nonnullkeys cover the write/create surface; read/lookup keys (getString,remove,getXxxOrDefault) are intentionally left unannotated here — same "null key = bug" logic, but a broader, Map-contract-adjacent sweep worth doing on its own.Test
TagMapNullToleranceTestpins the contract:create(...)→ null for null/empty (incl. empty-as-Object);set/getAndSetnull no-op; end-to-endcreate()→set()with null/empty leaves no tag. ExistingTagMapTest/TagMapEntryTestgreen (no behavior regression).tag: ai generated
tag: no release note
🤖 Generated with Claude Code