From 96c49d08cec75edd79993137325f7392f4a8a6e6 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Fri, 23 Oct 2026 00:00:06 -0400 Subject: [PATCH 1/2] Add id and name tag stores, not yet used IdStore keeps known tags by id; its one implementation today is HashIdStore, built through IdStoreFactory so a JVM loads a single IdStore subclass and calls on it stay devirtualized. HashStore keeps custom tags by name. Both reuse HashSupport's bucket mechanics and read through to a frozen parent's store, hiding an inherited tag with a tombstone: one shared, immutable Entry per known tag (Entry.tombstoneOf) in the id store, and a removed-name set in the name store. The hash id store's buckets are its exact state, so it needs no slot masks. Nothing uses the stores yet: TagMap and the span keep their current storage. IdStoreContractTest pins the stores' behavior directly. The only TagMap changes are to its nested Entry, EntryReader and BucketGroup, which the stores share: the tombstone flag and a lookup of a bucket chain by id. HashSupport.checkBuckets now indexes a group's entries by entry rather than by bucket, so the integrity check really inspects groups. The tag registry gains _dd.agent_psr as a trace-level known tag. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/HashIdStore.java | 417 ++++++++++++++ .../java/datadog/trace/api/HashStore.java | 364 ++++++++++++ .../java/datadog/trace/api/HashSupport.java | 281 ++++----- .../main/java/datadog/trace/api/IdStore.java | 159 +++++ .../datadog/trace/api/IdStoreFactory.java | 42 ++ .../main/java/datadog/trace/api/TagMap.java | 161 +++--- .../trace/api/IdStoreContractTest.java | 541 ++++++++++++++++++ tag-conventions.yaml | 1 + 8 files changed, 1751 insertions(+), 215 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/api/HashIdStore.java create mode 100644 internal-api/src/main/java/datadog/trace/api/HashStore.java create mode 100644 internal-api/src/main/java/datadog/trace/api/IdStore.java create mode 100644 internal-api/src/main/java/datadog/trace/api/IdStoreFactory.java create mode 100644 internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java new file mode 100644 index 00000000000..557dc730687 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -0,0 +1,417 @@ +package datadog.trace.api; + +import datadog.trace.api.TagMap.BucketGroup; +import datadog.trace.api.TagMap.Entry; +import datadog.trace.api.TagMap.EntryReader; +import datadog.trace.api.function.TriConsumer; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * The hash {@link IdStore}: known tags in their own bucket array ({@link HashSupport}), keyed by + * id, reading through to the frozen parent's id store. + * + *

The buckets are the exact state: a read probes them, and a local miss reads through to the + * parent if there is one. Removing an inherited tag puts that tag's shared tombstone ({@link + * Entry#tombstoneOf}) in the local buckets, so it allocates nothing. A tombstone is never counted, + * iterated or returned. + */ +final class HashIdStore extends IdStore { + /** An empty parentless store over one bucket, for {@link TagMap#EMPTY}, which is frozen. */ + static HashIdStore createEmpty() { + return new HashIdStore(new Object[1]); + } + + /** Walks the leaf's buckets, then each ancestor's, skipping tombstones and shadowed entries. */ + private static final class HashCursor implements Cursor { + private final HashIdStore leaf; + private HashIdStore level; + private final HashSupport.BucketWalker walker = new HashSupport.BucketWalker(); + + HashCursor(HashIdStore leaf) { + this.leaf = leaf; + this.level = leaf; + this.walker.reset(leaf.buckets); + } + + @Override + public EntryReader next() { + HashIdStore level = this.level; + while (level != null) { + Entry entry = this.walker.next(); + if (entry != null) { + if (!entry.tombstone + && (level == this.leaf || this.leaf.parentEntryVisible(entry, level))) { + return entry; + } + continue; + } + level = level.parent; + this.level = level; + if (level != null) { + this.walker.reset(level.buckets); + } + } + return null; + } + } + + final Object[] buckets; + + /** Live local entries, not counting tombstones. */ + int count; + + /** The frozen parent's id store, or null. */ + final HashIdStore parent; + + /** Whether the buckets may hold a tombstone; a hint that keeps the common case on fast paths. */ + boolean hasTombstones; + + HashIdStore(IdStore parent) { + this.buckets = HashSupport.newBuckets(); + this.parent = (HashIdStore) parent; + } + + private HashIdStore(Object[] buckets) { + this.buckets = buckets; + this.parent = null; + } + + @Override + public Entry getEntry(long tagId) { + if (!KnownTagCodec.isKnown(tagId)) { + throw notKnown(tagId); + } + return this.getVisible(tagId); + } + + /** The stored Entry is itself a stable reader, so this is {@link #getEntry}. */ + @Override + public Entry getEntryReader(long tagId) { + return this.getEntry(tagId); + } + + /** {@link #getEntry} for a validated id. */ + private Entry getVisible(long tagId) { + Entry local = HashSupport.findById(this.buckets, Entry.bucketHash(tagId), tagId); + if (local != null) { + return local.tombstone ? null : local; + } + HashIdStore parent = this.parent; + return parent == null ? null : parent.getVisible(tagId); + } + + @Override + public Object getValue(long tagId) { + Entry entry = this.getEntry(tagId); + return entry == null ? null : entry.objectValue(); + } + + @Override + public String getString(long tagId) { + Entry entry = this.getEntry(tagId); + return entry == null ? null : entry.stringValue(); + } + + @Override + public void set(long tagId, Object value) { + if (value == null) { + this.remove(tagId); // a null value removes the tag + return; + } + this.put(Entry.newAnyEntry(tagId, value)); + } + + @Override + public void set(long tagId, CharSequence value) { + if (value == null) { + this.remove(tagId); // a null value removes the tag + return; + } + this.put(Entry.newObjectEntry(tagId, value)); + } + + @Override + public void set(long tagId, boolean value) { + this.put(Entry.newBooleanEntry(tagId, value)); + } + + @Override + public void set(long tagId, int value) { + this.put(Entry.newIntEntry(tagId, value)); + } + + @Override + public void set(long tagId, long value) { + this.put(Entry.newLongEntry(tagId, value)); + } + + @Override + public void set(long tagId, float value) { + this.put(Entry.newFloatEntry(tagId, value)); + } + + @Override + public void set(long tagId, double value) { + this.put(Entry.newDoubleEntry(tagId, value)); + } + + @Override + public Entry put(EntryReader entryReader) { + Entry entry = entryReader.entry(); + if (entry == null) { + return null; // a reader with no entry is a no-op, as for TagMap.set + } + Entry replaced = HashSupport.put(this.buckets, entry); + if (replaced == null || replaced.tombstone) { + this.count++; // a tombstone replaced is a removal undone: no prior local entry + return null; + } + return replaced; + } + + @Override + public Entry getAndPut(EntryReader entryReader) { + Entry entry = entryReader.entry(); + if (entry == null) { + return null; // a reader with no entry is a no-op + } + Entry replaced = HashSupport.put(this.buckets, entry); + if (replaced != null && !replaced.tombstone) { + return replaced; // replaced a local entry -> that is the prior value + } + this.count++; + HashIdStore parent = this.parent; + if (replaced != null || parent == null) { + return null; // it was tombstoned, so not visible, or nothing to inherit + } + return parent.getVisible(entry.tagHash); + } + + @Override + public Entry remove(long tagId) { + if (!KnownTagCodec.isKnown(tagId)) { + throw notKnown(tagId); + } + int hash = Entry.bucketHash(tagId); + + Entry local = HashSupport.findById(this.buckets, hash, tagId); + if (local != null) { + if (local.tombstone) { + return null; // already removed + } + HashSupport.remove(this.buckets, hash, local.tag); + this.count--; + } + HashIdStore parent = this.parent; + if (parent != null) { + // Read-through: hide an inherited value with a tombstone. The prior *visible* value is the + // local entry if there was one, otherwise the inherited one. + Entry inherited = parent.getVisible(tagId); + if (inherited != null) { + HashSupport.put(this.buckets, Entry.tombstoneOf(tagId)); + this.hasTombstones = true; + return local != null ? local : inherited; + } + } + return local; + } + + @Override + int size() { + return this.parent == null ? this.count : this.count + this.visibleParentCount(); + } + + /** Ancestor entries not shadowed or tombstoned by a nearer level, nearest-level-wins. */ + private int visibleParentCount() { + // a plain loop, not forEachParent: counting through a lambda would allocate a counter + int count = 0; + for (HashIdStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + Object[] parentBuckets = ancestor.buckets; + for (int i = 0; i < parentBuckets.length; ++i) { + Object bucket = parentBuckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (!entry.tombstone && this.parentEntryVisible(entry, ancestor)) { + count++; + } + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && !entry.tombstone && this.parentEntryVisible(entry, ancestor)) { + count++; + } + } + } + } + } + } + return count; + } + + @Override + boolean isEmpty() { + if (this.count != 0) { + return false; + } + HashIdStore parent = this.parent; + if (parent == null) { + return true; + } + if (!this.hasTombstones) { + return parent.isEmpty(); // nothing hidden: the parent's tags are all visible + } + return this.visibleParentCount() == 0; + } + + @Override + boolean isDefinitelyEmpty() { + for (HashIdStore level = this; level != null; level = level.parent) { + if (level.count != 0) { + return false; + } + } + return true; + } + + @Override + int estimateSize() { + int total = 0; + for (HashIdStore level = this; level != null; level = level.parent) { + total += level.count; + } + return total; + } + + @Override + boolean hasParent() { + return this.parent != null; + } + + /** + * Whether {@code parentEntry}, from the ancestor level {@code fromAncestor}, is visible through + * this leaf: no nearer level holds an entry or a tombstone for it. + */ + boolean parentEntryVisible(Entry parentEntry, HashIdStore fromAncestor) { + long tagId = parentEntry.tagHash; + int hash = parentEntry.hash(); + for (HashIdStore nearer = this; nearer != fromAncestor; nearer = nearer.parent) { + if (HashSupport.findById(nearer.buckets, hash, tagId) != null) { + return false; + } + } + return true; + } + + @Override + void forEach(Consumer consumer) { + HashSupport.forEach(this.buckets, consumer); + if (this.parent != null) { + this.forEachParent(consumer, null, (c, unused, entry) -> c.accept(entry)); + } + } + + @Override + void forEach(T thisObj, BiConsumer consumer) { + HashSupport.forEach(this.buckets, thisObj, consumer); + if (this.parent != null) { + this.forEachParent(thisObj, consumer, (t, c, entry) -> c.accept(t, entry)); + } + } + + @Override + void forEach(T thisObj, U otherObj, TriConsumer consumer) { + HashSupport.forEach(this.buckets, thisObj, otherObj, consumer); + if (this.parent != null) { + this.forEachParent(thisObj, otherObj, consumer); + } + } + + private void forEachParent( + T thisObj, U otherObj, TriConsumer consumer) { + for (HashIdStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + Object[] parentBuckets = ancestor.buckets; + for (int i = 0; i < parentBuckets.length; ++i) { + Object bucket = parentBuckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (!entry.tombstone && this.parentEntryVisible(entry, ancestor)) { + consumer.accept(thisObj, otherObj, entry); + } + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && !entry.tombstone && this.parentEntryVisible(entry, ancestor)) { + consumer.accept(thisObj, otherObj, entry); + } + } + } + } + } + } + } + + @Override + Cursor cursor() { + return new HashCursor(this); + } + + @Override + IdStore copy() { + HashIdStore copy = new HashIdStore(this.parent); + HashSupport.copyInto(copy.buckets, this.buckets); + copy.count = this.count; + copy.hasTombstones = this.hasTombstones; + return copy; + } + + @Override + void putAllLocal(IdStore other) { + HashIdStore that = (HashIdStore) other; + if (that.count == 0) { + return; + } + if (this.hasTombstones) { + // A bucket merge would count a replaced tombstone as a replacement, so put one at a time. + HashSupport.forEach(that.buckets, this, (self, entryReader) -> self.put(entryReader)); + return; + } + if (this.count == 0) { + HashSupport.copyInto(this.buckets, that.buckets); + this.count = that.count; + } else { + this.count += HashSupport.merge(this.buckets, that.buckets); + } + } + + @Override + void fillMapLocal(Map map) { + HashSupport.fillMap(this.buckets, map); + } + + @Override + void fillStringMapLocal(Map map) { + HashSupport.fillStringMap(this.buckets, map); + } + + @Override + void checkIntegrity() { + HashSupport.checkBuckets(this.buckets); + if (this.count != this.computeSize()) { + throw new IllegalStateException("incorrect size"); + } + } + + @Override + int computeSize() { + return HashSupport.count(this.buckets); + } + + @Override + String toInternalString() { + return HashSupport.toInternalString(this.buckets); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/HashStore.java b/internal-api/src/main/java/datadog/trace/api/HashStore.java new file mode 100644 index 00000000000..3b746cb5244 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/HashStore.java @@ -0,0 +1,364 @@ +package datadog.trace.api; + +import datadog.apicontract.Restricted; +import datadog.trace.api.TagMap.BucketGroup; +import datadog.trace.api.TagMap.Entry; +import datadog.trace.api.TagMap.EntryReader; +import datadog.trace.api.function.TriConsumer; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * The custom-tag half of a {@link TagMap}: custom tags by name, in their own bucket array ({@link + * HashSupport}), reading through to the frozen parent's custom store. Name-keyed methods take only + * custom tag names (asserted): a known tag is resolved to its id first, and goes to the {@link + * IdStore}. + * + *

Created lazily by its owner -- a {@link TagMap}, or a span in the tracer core -- on the first + * custom write ({@link #create}); until then the owner borrows its parent's custom store, if any, + * read-only. No locking: guarded by the owner's lock. + */ +@Restricted(allowedIn = {"datadog.trace.core"}) +public final class HashStore { + final Object[] buckets; + + /** Local entries. */ + int count; + + /** The frozen parent's custom store, or null. */ + final HashStore parent; + + /** + * Parent keys removed locally (read-through tombstones): a tombstone stops read-through for its + * key. Allocated on the first such removal, so while it is {@code null} the hot paths skip the + * tombstone checks. + */ + private Set removedFromParent; + + /** + * A fresh custom store for an owner's first custom write, reading through to {@code borrowed}, + * the store it borrowed until now (may be null). + */ + public static HashStore create(HashStore borrowed) { + return new HashStore(borrowed); + } + + HashStore(HashStore parent) { + this.buckets = HashSupport.newBuckets(); + this.parent = parent; + } + + /** Exact count, including the visible part of the read-through chain. */ + int size() { + return this.parent == null ? this.count : this.count + this.visibleParentCount(); + } + + /** Ancestor entries not shadowed or tombstoned by a nearer level, nearest-level-wins. */ + private int visibleParentCount() { + // a plain loop, not forEachParent: counting through a lambda would allocate a counter + int count = 0; + for (HashStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + Object[] parentBuckets = ancestor.buckets; + for (int i = 0; i < parentBuckets.length; ++i) { + Object bucket = parentBuckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (this.parentEntryVisible(entry, ancestor)) { + count++; + } + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && this.parentEntryVisible(entry, ancestor)) { + count++; + } + } + } + } + } + } + return count; + } + + boolean isEmpty() { + if (this.count != 0) { + return false; + } + HashStore parent = this.parent; + if (parent == null) { + return true; + } + if (this.removedFromParent == null) { + return parent.isEmpty(); // nothing hidden: the parent's tags are all visible + } + return this.visibleParentCount() == 0; + } + + /** Cheap: no level of the chain holds a local entry (ignores shadowing and tombstones). */ + boolean isDefinitelyEmpty() { + for (HashStore level = this; level != null; level = level.parent) { + if (level.count != 0) { + return false; + } + } + return true; + } + + /** Upper bound: the local counts of every level. */ + int estimateSize() { + int total = 0; + for (HashStore level = this; level != null; level = level.parent) { + total += level.count; + } + return total; + } + + /** The custom tag {@code tag}'s entry, reading through to the parent chain. */ + public Entry get(String tag) { + assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; look it up by its id"; + return this.get(tag, Entry.bucketHash(Entry.customHash(tag))); + } + + private Entry get(String tag, int hash) { + Entry local = HashSupport.find(this.buckets, hash, tag); + if (local != null) { + return local; + } + HashStore parent = this.parent; + if (parent == null) { + return null; + } + if (this.removedFromParent != null && this.removedFromParent.contains(tag)) { + return null; // tombstoned: removed locally, do not read through + } + return parent.get(tag, hash); + } + + /* + * Sets the custom tag {@code tag}, which must not be a known tag's name. + */ + + public void set(String tag, Object value) { + if (value == null) { + this.remove(tag); // a null value removes the tag + return; + } + this.put(Entry.newAnyEntry(tag, value)); + } + + public void set(String tag, CharSequence value) { + if (value == null) { + this.remove(tag); // a null value removes the tag + return; + } + this.put(Entry.newObjectEntry(tag, value)); + } + + public void set(String tag, boolean value) { + this.put(Entry.newBooleanEntry(tag, value)); + } + + public void set(String tag, int value) { + this.put(Entry.newIntEntry(tag, value)); + } + + public void set(String tag, long value) { + this.put(Entry.newLongEntry(tag, value)); + } + + public void set(String tag, float value) { + this.put(Entry.newFloatEntry(tag, value)); + } + + public void set(String tag, double value) { + this.put(Entry.newDoubleEntry(tag, value)); + } + + /** + * Inserts or replaces a custom tag's entry, returning the local entry it replaced (or null). Does + * not consult the read-through parent. + */ + public Entry put(Entry newEntry) { + // Re-setting a key clears any read-through tombstone for it (the new value overrides the + // removal). Gated on the lazy field, so this is a no-op for the common no-tombstone case. + if (this.removedFromParent != null) { + this.removedFromParent.remove(newEntry.tag); + } + Entry replaced = HashSupport.put(this.buckets, newEntry); + if (replaced == null) { + this.count++; + } + return replaced; + } + + /** {@link #put}, returning the prior visible entry -- local, else read through. */ + public Entry getAndPut(Entry newEntry) { + // Capture whether the key was tombstoned BEFORE put clears it: a tombstoned key had no visible + // prior value (it was removed), so getAndPut must report null rather than the parent's. + boolean wasTombstoned = + this.removedFromParent != null && this.removedFromParent.contains(newEntry.tag); + + Entry priorLocal = this.put(newEntry); + if (priorLocal != null) { + return priorLocal; + } + if (wasTombstoned || this.parent == null) { + return null; + } + return this.parent.get(newEntry.tag, newEntry.hash()); + } + + /** + * Removes the custom tag {@code tag}, returning the prior visible entry. Removing a key the + * parent chain still exposes also installs a tombstone, so it stops reading through. + */ + public Entry remove(String tag) { + assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; remove it by its id"; + int hash = Entry.bucketHash(Entry.customHash(tag)); + Entry localRemoved = HashSupport.remove(this.buckets, hash, tag); + if (localRemoved != null) { + this.count--; + } + + HashStore parent = this.parent; + if (parent != null) { + boolean alreadyTombstoned = + this.removedFromParent != null && this.removedFromParent.contains(tag); + if (!alreadyTombstoned) { + Entry parentEntry = parent.get(tag, hash); + if (parentEntry != null) { + if (this.removedFromParent == null) { + // Small initial capacity: this set is rare and almost always holds only a handful of + // tombstoned keys, so the default 16-bucket HashSet table would be wasteful. + this.removedFromParent = new HashSet<>(4); + } + this.removedFromParent.add(tag); + return localRemoved != null ? localRemoved : parentEntry; + } + } + } + return localRemoved; + } + + /** + * Whether {@code parentEntry}, from the ancestor level {@code fromAncestor}, is visible through + * this leaf: not shadowed and not tombstoned by any nearer level. + */ + boolean parentEntryVisible(Entry parentEntry, HashStore fromAncestor) { + int hash = parentEntry.hash(); + String tag = parentEntry.tag; + for (HashStore nearer = this; nearer != fromAncestor; nearer = nearer.parent) { + if (nearer.removedFromParent != null && nearer.removedFromParent.contains(tag)) { + return false; + } + if (HashSupport.find(nearer.buckets, hash, tag) != null) { + return false; + } + } + return true; + } + + /** Visits the local entries, then the visible inherited ones. */ + void forEach(Consumer consumer) { + HashSupport.forEach(this.buckets, consumer); + if (this.parent != null) { + this.forEachParent(consumer, null, (c, unused, entry) -> c.accept(entry)); + } + } + + void forEach(T thisObj, BiConsumer consumer) { + HashSupport.forEach(this.buckets, thisObj, consumer); + if (this.parent != null) { + this.forEachParent(thisObj, consumer, (t, c, entry) -> c.accept(t, entry)); + } + } + + void forEach(T thisObj, U otherObj, TriConsumer consumer) { + HashSupport.forEach(this.buckets, thisObj, otherObj, consumer); + if (this.parent != null) { + this.forEachParent(thisObj, otherObj, consumer); + } + } + + private void forEachParent( + T thisObj, U otherObj, TriConsumer consumer) { + for (HashStore ancestor = this.parent; ancestor != null; ancestor = ancestor.parent) { + Object[] parentBuckets = ancestor.buckets; + for (int i = 0; i < parentBuckets.length; ++i) { + Object bucket = parentBuckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (this.parentEntryVisible(entry, ancestor)) { + consumer.accept(thisObj, otherObj, entry); + } + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && this.parentEntryVisible(entry, ancestor)) { + consumer.accept(thisObj, otherObj, entry); + } + } + } + } + } + } + } + + /** + * An independently mutable copy: the same (frozen, shared) parent, this store's own entries + * (shared Entry objects) and its tombstones. + */ + HashStore copy() { + HashStore copy = new HashStore(this.parent); + HashSupport.copyInto(copy.buckets, this.buckets); + copy.count = this.count; + if (this.removedFromParent != null) { + copy.removedFromParent = new HashSet<>(this.removedFromParent); + } + return copy; + } + + /** Copies the entries of the parentless store {@code that}, sharing Entry objects. */ + void putAllLocal(HashStore that) { + if (that.count == 0) { + return; + } + if (this.count == 0) { + HashSupport.copyInto(this.buckets, that.buckets); + this.count = that.count; + } else { + this.count += HashSupport.merge(this.buckets, that.buckets); + } + } + + void fillMapLocal(Map map) { + HashSupport.fillMap(this.buckets, map); + } + + void fillStringMapLocal(Map map) { + HashSupport.fillStringMap(this.buckets, map); + } + + /** Tests only: throws if the store's structure is inconsistent. */ + void checkIntegrity() { + HashSupport.checkBuckets(this.buckets); + if (this.count != this.computeSize()) { + throw new IllegalStateException("incorrect size"); + } + } + + /** Tests only: the local count, recomputed from the structure. */ + int computeSize() { + return HashSupport.count(this.buckets); + } + + /** For deep debugging. */ + String toInternalString() { + return HashSupport.toInternalString(this.buckets); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/HashSupport.java b/internal-api/src/main/java/datadog/trace/api/HashSupport.java index 1c7874433ab..368244f3264 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashSupport.java +++ b/internal-api/src/main/java/datadog/trace/api/HashSupport.java @@ -9,35 +9,50 @@ import java.util.function.Consumer; /** - * The raw bucket mechanics of {@link TagMap}, over one fixed-size {@code Object[]} of buckets of - * shareable, immutable {@link Entry} objects. A bucket holds a lone Entry directly; colliding - * entries share a fixed-size {@link BucketGroup}, chained when one fills. + * The raw bucket mechanics both tag stores share, over one fixed-size {@code Object[]} of buckets + * of shareable, immutable {@link Entry} objects. A bucket holds a lone Entry directly; because + * Entries are shared between maps, they cannot form a linked list, so colliding entries share a + * fixed-size {@link BucketGroup}, chained when one fills. Removals are assumed rare, so groups are + * never consolidated, but an emptied group is unlinked from its chain. * *

Stateless: each operation reports what it did through its return value -- put returns the - * entry it replaced, remove the entry it removed, merge how much the map grew -- and the calling - * map keeps its own count. Read-through to a parent map and its tombstones stay with the map. + * entry it replaced, remove the entry it removed -- and the calling store keeps its own count. + * Copies share Entries and clone only BucketGroup chains. Iteration skips tombstones ({@link + * Entry#tombstone}), which only an id store holds. */ final class HashSupport { private HashSupport() {} /** A fresh bucket array; its length must be a power of 2 for bucket masking. */ static Object[] newBuckets() { - // needs to be a power of 2 for bucket masking calculation to work as intended return new Object[1 << 4]; } - /** The entry for {@code (hash, tag)} in {@code buckets}, or null. */ + /** The entry for {@code (hash, tag)}, tombstone or not, or null. */ static Entry find(Object[] buckets, int hash, String tag) { return findInBucket(buckets[hash & (buckets.length - 1)], hash, tag); } /** - * Finds an entry by hash/tag within a single bucket object (Entry | BucketGroup chain | null). + * {@link #find} for a known tag, matching on its id rather than its name, so an id store never + * needs a name. {@code hash} is {@code Entry.bucketHash(tagId)}. */ + static Entry findById(Object[] buckets, int hash, long tagId) { + Object bucket = buckets[hash & (buckets.length - 1)]; + if (bucket instanceof Entry) { + Entry tagEntry = (Entry) bucket; + return tagEntry.tagHash == tagId ? tagEntry : null; + } else if (bucket instanceof BucketGroup) { + return ((BucketGroup) bucket).findInChainById(hash, tagId); + } + return null; + } + + /** Finds an entry by hash and tag in one bucket: an Entry, a BucketGroup chain, or null. */ static Entry findInBucket(Object bucket, int hash, String tag) { if (bucket instanceof Entry) { Entry tagEntry = (Entry) bucket; - return tagEntry.matches(tag) ? tagEntry : null; + return tagEntry.hash() == hash && tagEntry.matches(tag) ? tagEntry : null; } else if (bucket instanceof BucketGroup) { return ((BucketGroup) bucket).findInChain(hash, tag); } @@ -58,7 +73,7 @@ static Entry put(Object[] buckets, Entry newEntry) { return null; } else if (bucket instanceof Entry) { Entry existingEntry = (Entry) bucket; - if (existingEntry.matches(newEntry.tag)) { + if (existingEntry.hash() == newHash && existingEntry.matches(newEntry.tag)) { thisBuckets[bucketIndex] = newEntry; // replaced existing entry - no size change @@ -91,14 +106,13 @@ static Entry put(Object[] buckets, Entry newEntry) { /** Removes the entry for {@code (hash, tag)}, returning it, or null if there was none. */ static Entry remove(Object[] buckets, int hash, String tag) { Object[] thisBuckets = buckets; - int bucketIndex = hash & (thisBuckets.length - 1); Object bucket = thisBuckets[bucketIndex]; // null bucket case - do nothing if (bucket instanceof Entry) { Entry existingEntry = (Entry) bucket; - if (existingEntry.matches(tag)) { + if (existingEntry.hash() == hash && existingEntry.matches(tag)) { thisBuckets[bucketIndex] = null; return existingEntry; @@ -128,6 +142,7 @@ static Entry remove(Object[] buckets, int hash, String tag) { * cloning only BucketGroup chains. */ static void copyInto(Object[] thisBuckets, Object[] thatBuckets) { + // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check // elimination for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { @@ -149,12 +164,13 @@ static void copyInto(Object[] thisBuckets, Object[] thatBuckets) { /** * Merges {@code thatBuckets} into {@code thisBuckets}, sharing Entry objects and cloning only - * BucketGroup chains; incoming entries replace existing ones. Returns how much the map grew. + * BucketGroup chains; incoming entries replace existing ones. Returns how many it added. Both + * arrays must be the same length (stores don't expand, so buckets are perfectly aligned). */ static int merge(Object[] thisBuckets, Object[] thatBuckets) { int added = 0; - // Since TagMap-s don't support expansion, buckets are perfectly aligned + // Stores don't expand, so buckets are perfectly aligned // Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check // elimination for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) { @@ -211,7 +227,7 @@ static int merge(Object[] thisBuckets, Object[] thatBuckets) { Entry incomingEntry = thisNewGroup.findInChain(thisHash, thisEntry.tag()); if (incomingEntry != null) { // there's already an entry w/ the same tag from the incoming TagMap - // incoming entry clobbers the existing try, so we're done + // incoming entry clobbers the existing entry, so we're done thisBuckets[i] = thisNewGroup; // overlapping group - subtract one for clobbered existing entry @@ -220,13 +236,13 @@ static int merge(Object[] thisBuckets, Object[] thatBuckets) { // able to add thisEntry into the existing groups thisBuckets[i] = thisNewGroup; - // non overlapping group - existing entry already accounted for in this.size + // non overlapping group - existing entry already counted added += thisNewGroupSize; } else { // unable to add into the existing groups thisBuckets[i] = new BucketGroup(thisHash, thisEntry, thisNewGroup); - // non overlapping group - existing entry already accounted for in this.size + // non overlapping group - existing entry already counted added += thisNewGroupSize; } } @@ -265,141 +281,81 @@ static int merge(Object[] thisBuckets, Object[] thatBuckets) { return added; } - /** Visits every entry in {@code buckets}. */ + /** Visits every entry, skipping tombstones. */ static void forEach(Object[] buckets, Consumer consumer) { - Object[] thisBuckets = buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - consumer.accept(thisEntry); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.forEachInChain(consumer); + for (int i = 0; i < buckets.length; ++i) { + Object bucket = buckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (!entry.tombstone) consumer.accept(entry); + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && !entry.tombstone) consumer.accept(entry); + } + } } } } - /** {@link #forEach(Object[], Consumer)} with a context object for a non-capturing consumer. */ static void forEach( Object[] buckets, T thisObj, BiConsumer consumer) { - Object[] thisBuckets = buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - consumer.accept(thisObj, thisEntry); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.forEachInChain(thisObj, consumer); + for (int i = 0; i < buckets.length; ++i) { + Object bucket = buckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (!entry.tombstone) consumer.accept(thisObj, entry); + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && !entry.tombstone) consumer.accept(thisObj, entry); + } + } } } } - /** {@link #forEach(Object[], Consumer)} with two context objects. */ static void forEach( Object[] buckets, T thisObj, U otherObj, TriConsumer consumer) { - Object[] thisBuckets = buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - consumer.accept(thisObj, otherObj, thisEntry); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.forEachInChain(thisObj, otherObj, consumer); + for (int i = 0; i < buckets.length; ++i) { + Object bucket = buckets[i]; + if (bucket instanceof Entry) { + Entry entry = (Entry) bucket; + if (!entry.tombstone) consumer.accept(thisObj, otherObj, entry); + } else if (bucket instanceof BucketGroup) { + for (BucketGroup group = (BucketGroup) bucket; group != null; group = group.prev) { + for (int j = 0; j < BucketGroup.LEN; ++j) { + Entry entry = group._entryAt(j); + if (entry != null && !entry.tombstone) consumer.accept(thisObj, otherObj, entry); + } + } } } } - /** Puts every entry in {@code buckets} into {@code map}, by tag, as its object value. */ static void fillMap(Object[] buckets, Map map) { - Object[] thisBuckets = buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - map.put(thisEntry.tag, thisEntry.objectValue()); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.fillMapFromChain(map); - } - } + forEach(buckets, map, (m, entry) -> m.put(entry.tag(), entry.objectValue())); } - /** Puts every entry in {@code buckets} into {@code stringMap}, by tag, as its string value. */ - static void fillStringMap(Object[] buckets, Map stringMap) { - Object[] thisBuckets = buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object thisBucket = thisBuckets[i]; - - if (thisBucket instanceof Entry) { - Entry thisEntry = (Entry) thisBucket; - - stringMap.put(thisEntry.tag, thisEntry.stringValue()); - } else if (thisBucket instanceof BucketGroup) { - BucketGroup thisGroup = (BucketGroup) thisBucket; - - thisGroup.fillStringMapFromChain(stringMap); - } - } + static void fillStringMap(Object[] buckets, Map map) { + forEach(buckets, map, (m, entry) -> m.put(entry.tag(), entry.stringValue())); } - /** The number of entries in {@code buckets}, counted. */ + /** The number of live entries, not counting tombstones. Tests only. */ static int count(Object[] buckets) { - Object[] thisBuckets = buckets; - - int size = 0; - for (int i = 0; i < thisBuckets.length; ++i) { - Object curBucket = thisBuckets[i]; - - if (curBucket instanceof Entry) { - size += 1; - } else if (curBucket instanceof BucketGroup) { - BucketGroup curGroup = (BucketGroup) curBucket; - size += curGroup.sizeInChain(); - } - } - return size; + int[] count = new int[1]; + forEach(buckets, count, (c, entry) -> c[0]++); + return count[0]; } - /** Whether {@code buckets} holds no entry, checked bucket by bucket. */ - static boolean isEmpty(Object[] buckets) { - Object[] thisBuckets = buckets; - - for (int i = 0; i < thisBuckets.length; ++i) { - Object curBucket = thisBuckets[i]; - - if (curBucket instanceof Entry) { - return false; - } else if (curBucket instanceof BucketGroup) { - BucketGroup curGroup = (BucketGroup) curBucket; - if (!curGroup.isEmptyChain()) return false; - } - } - - return true; - } - - /** Checks that every entry in {@code buckets} sits in the bucket its hash selects. */ + /** Throws if an entry sits in the wrong bucket. Tests only. */ static void checkBuckets(Object[] buckets) { + // Decided to use if ( cond ) throw new IllegalStateException rather than assert + // That was done to avoid the extra static initialization needed for an assertion + // While that's probably an unnecessary optimization, this method is only called in tests + Object[] thisBuckets = buckets; for (int i = 0; i < thisBuckets.length; ++i) { @@ -418,11 +374,11 @@ static void checkBuckets(Object[] buckets) { for (BucketGroup curGroup = thisGroup; curGroup != null; curGroup = curGroup.prev) { for (int j = 0; j < BucketGroup.LEN; ++j) { - Entry thisEntry = curGroup._entryAt(i); + Entry thisEntry = curGroup._entryAt(j); if (thisEntry == null) continue; int thisHash = thisEntry.hash(); - assert curGroup._hashAt(i) == thisHash; + assert curGroup._hashAt(j) == thisHash; int expectedBucket = thisHash & (thisBuckets.length - 1); if (expectedBucket != i) { @@ -434,7 +390,6 @@ static void checkBuckets(Object[] buckets) { } } - /** The buckets' internal structure, for deep debugging. */ static String toInternalString(Object[] buckets) { Object[] thisBuckets = buckets; @@ -458,4 +413,72 @@ static String toInternalString(Object[] buckets) { } return ledger.toString(); } + + /** Whether {@code buckets} holds no entry, checked bucket by bucket. */ + static boolean isEmpty(Object[] buckets) { + Object[] thisBuckets = buckets; + + for (int i = 0; i < thisBuckets.length; ++i) { + Object curBucket = thisBuckets[i]; + + if (curBucket instanceof Entry) { + return false; + } else if (curBucket instanceof BucketGroup) { + BucketGroup curGroup = (BucketGroup) curBucket; + if (!curGroup.isEmptyChain()) return false; + } + } + + return true; + } + + /** Walks a bucket array's raw entries, tombstones included, for an iterator. */ + static final class BucketWalker { + private Object[] buckets; + private int bucketIndex; + private BucketGroup group; + private int groupIndex; + + /** Starts over on {@code buckets}. */ + void reset(Object[] buckets) { + this.buckets = buckets; + this.bucketIndex = -1; + this.group = null; + this.groupIndex = 0; + } + + /** The next raw entry, or null when the array is done. */ + Entry next() { + while (this.bucketIndex < this.buckets.length) { + if (this.group != null) { + for (++this.groupIndex; this.groupIndex < BucketGroup.LEN; ++this.groupIndex) { + Entry tagEntry = this.group._entryAt(this.groupIndex); + if (tagEntry != null) return tagEntry; + } + + // done processing - that group, go to next group + this.group = this.group.prev; + this.groupIndex = -1; + } + + // if the group is null, then we've finished the current bucket - so advance the bucket + if (this.group == null) { + for (++this.bucketIndex; this.bucketIndex < this.buckets.length; ++this.bucketIndex) { + Object bucket = this.buckets[this.bucketIndex]; + + if (bucket instanceof Entry) { + return (Entry) bucket; + } else if (bucket instanceof BucketGroup) { + this.group = (BucketGroup) bucket; + this.groupIndex = -1; + + break; + } + } + } + } + + return null; + } + } } diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java new file mode 100644 index 00000000000..d9e2f1bdf86 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -0,0 +1,159 @@ +package datadog.trace.api; + +import datadog.apicontract.Restricted; +import datadog.trace.api.TagMap.EntryReader; +import datadog.trace.api.function.TriConsumer; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Known tags keyed by their {@link KnownTags} id. Every method takes an id, or an {@link + * EntryReader} for a known tag -- never a name, so nothing here can be handed a name that a caller + * has not yet resolved. + * + *

A span in the tracer core owns its id store directly ({@link #create}); a {@link TagMap} owns + * one too. + * + *

{@link IdStoreFactory} builds every store, so the JVM loads one implementation and class + * hierarchy analysis devirtualizes every call on an {@code IdStore} without a type guard; nothing + * that is always loaded may extend this class. No locking: guarded by the owner's lock. + */ +@Restricted(allowedIn = {"datadog.trace.core"}) +public abstract class IdStore { + /** Visits a store's visible entries one at a time. */ + interface Cursor { + /** The next visible entry, or null when there are none left. */ + EntryReader next(); + } + + /** Throws for an id that names no known tag. */ + static IllegalArgumentException notKnown(long tagId) { + return new IllegalArgumentException("not a known tag id: " + Long.toHexString(tagId)); + } + + /* + * Point operations. A returned EntryReader is valid only until this store's next operation: a + * store that keeps known tags without an Entry per tag answers with a reused flyweight. Call + * EntryReader.entry() to keep one. Stores are inside the tracer's trust boundary, so callers are + * expected to follow this rather than have it enforced. + */ + + /** + * The entry for the known tag {@code tagId}, reading through to the parent chain, or null. + * + * @throws IllegalArgumentException if {@code tagId} names no known tag + */ + public abstract EntryReader getEntryReader(long tagId); + + /** + * The entry for the known tag {@code tagId}, reading through to the parent chain, or null. Unlike + * {@link #getEntryReader}, the result is an {@link TagMap.Entry} the caller may keep, and it + * shares no state with the store, so it is safe to call concurrently on a frozen store that + * several threads read, such as a span prototype's or the tracer-level tags. + * + * @throws IllegalArgumentException if {@code tagId} names no known tag + */ + public abstract TagMap.Entry getEntry(long tagId); + + /** + * {@link #getEntryReader}'s value, boxed if necessary, or null -- for a caller that needs only + * the value, so a store that keeps known tags without an Entry per tag can answer without one. + */ + public abstract Object getValue(long tagId); + + /** + * {@link #getValue} as a String, or null. A store that keeps an Entry per tag answers with the + * Entry's cached string, so a numeric tag read repeatedly, as the serializer does, is formatted + * once. + */ + public abstract String getString(long tagId); + + /* + * Sets a known tag without validating its id: the caller has checked KnownTagCodec.isKnown. + */ + + public abstract void set(long tagId, Object value); + + public abstract void set(long tagId, CharSequence value); + + public abstract void set(long tagId, boolean value); + + public abstract void set(long tagId, int value); + + public abstract void set(long tagId, long value); + + public abstract void set(long tagId, float value); + + public abstract void set(long tagId, double value); + + /** + * Inserts or replaces a known tag, returning the local entry it replaced (or null). Does not + * consult the read-through parent. A store that keeps Entry objects keeps {@code entryReader}'s + * Entry (free when it is one); a store that doesn't copies its value. + */ + public abstract EntryReader put(EntryReader entryReader); + + /** {@link #put}, returning the prior visible entry -- local, else read through. */ + public abstract EntryReader getAndPut(EntryReader entryReader); + + /** + * Removes the known tag {@code tagId}, returning the prior visible entry; an inherited one is + * hidden by a tombstone. + * + * @throws IllegalArgumentException if {@code tagId} names no known tag + */ + public abstract EntryReader remove(long tagId); + + /* + * Whole-store operations, for the owning TagMap. + */ + + /** Exact count, including the visible part of the read-through chain. */ + abstract int size(); + + abstract boolean isEmpty(); + + /** Cheap: no level of the chain holds a local entry (ignores shadowing and tombstones). */ + abstract boolean isDefinitelyEmpty(); + + /** Upper bound: the local counts of every level. */ + abstract int estimateSize(); + + /** Whether this store reads through to a parent. */ + abstract boolean hasParent(); + + /** Visits the local entries, then the visible inherited ones. */ + abstract void forEach(Consumer consumer); + + abstract void forEach(T thisObj, BiConsumer consumer); + + abstract void forEach( + T thisObj, U otherObj, TriConsumer consumer); + + /** + * A cursor over the visible entries, local then inherited, for an iterator. Each entry it returns + * is valid until its next call, as with a point read. + */ + abstract Cursor cursor(); + + /** An independently mutable copy with the same parent, sharing any Entry objects it holds. */ + abstract IdStore copy(); + + /** Copies the entries of the parentless store {@code that}, sharing any Entry objects. */ + abstract void putAllLocal(IdStore that); + + /** Puts the local entries into {@code map}, for a store with no parent. */ + abstract void fillMapLocal(Map map); + + abstract void fillStringMapLocal(Map map); + + /** Tests only: throws if the store's structure is inconsistent. */ + abstract void checkIntegrity(); + + /** Tests only: the local count, recomputed from the structure. */ + abstract int computeSize(); + + /** For deep debugging. */ + abstract String toInternalString(); +} diff --git a/internal-api/src/main/java/datadog/trace/api/IdStoreFactory.java b/internal-api/src/main/java/datadog/trace/api/IdStoreFactory.java new file mode 100644 index 00000000000..bd7385b9d6c --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/IdStoreFactory.java @@ -0,0 +1,42 @@ +package datadog.trace.api; + +/** + * Builds the {@link IdStore}s of the one implementation this JVM uses, today {@link HashIdStore}. + * + *

Every store is built through {@link Default#INSTANCE}, never with {@code new}, so a JVM loads + * one {@code IdStore} subclass and class hierarchy analysis devirtualizes every call on an {@code + * IdStore} without a guard. A store builds its own copies. + * + *

No static state here, and no {@link TagMap}: {@link TagMap#EMPTY} is built with {@link + * #createEmpty}, so class initialization works whichever of the two starts first. + */ +abstract class IdStoreFactory { + /** The JVM's store. */ + static final class Default { + static final IdStoreFactory INSTANCE = HashIdStoreFactory.INSTANCE; + + private Default() {} + } + + /** A fresh store reading through to {@code parent} (may be null). */ + abstract IdStore create(IdStore parent); + + /** A new empty parentless store, for a frozen map. */ + abstract IdStore createEmpty(); +} + +final class HashIdStoreFactory extends IdStoreFactory { + static final HashIdStoreFactory INSTANCE = new HashIdStoreFactory(); + + private HashIdStoreFactory() {} + + @Override + IdStore create(IdStore parent) { + return new HashIdStore(parent); + } + + @Override + IdStore createEmpty() { + return HashIdStore.createEmpty(); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/TagMap.java b/internal-api/src/main/java/datadog/trace/api/TagMap.java index 8cbc4358c9c..2a476c41c4c 100644 --- a/internal-api/src/main/java/datadog/trace/api/TagMap.java +++ b/internal-api/src/main/java/datadog/trace/api/TagMap.java @@ -159,6 +159,13 @@ public boolean isRemoval() { } } + /** + * A momentary view of one tag: read it where it was obtained and drop it. A reader MUST NOT be + * kept past the call that produced it -- not in a field, a collection or a later step. A store + * may answer with a flyweight it reuses for the next read or write, so a kept reader can later + * report another tag's value. To keep a tag, call {@link #entry()}: an {@link Entry} is the + * keepable form. (To be marked {@code @ScopeBounded}, APMLP-2086.) + */ public interface EntryReader { public static final byte OBJECT = 1; @@ -448,6 +455,41 @@ static Entry newDoubleEntry(String tag, Double box) { volatile String strCache = null; + /** + * True for a known tag's tombstone: the marker the hash id store puts in its own buckets to + * hide an inherited entry it has removed. Never visible outside the store; see {@link + * #tombstoneOf}. + */ + final boolean tombstone; + + /** The shared tombstone per known tag, by serial, created on first use. */ + private static final Entry[] TOMBSTONES = new Entry[KnownTagCodec.serialLimit()]; + + /** + * The tombstone for the known tag {@code tagId}: one shared, immutable entry per tag, so + * tombstoning a known tag allocates nothing after the first time. Racing first uses may each + * build one; either serves, since a tombstone is told apart by {@link #tombstone}, not + * identity. + */ + static Entry tombstoneOf(long tagId) { + int serial = KnownTagCodec.serialNum(tagId); + Entry tombstone = TOMBSTONES[serial]; + if (tombstone == null || tombstone.tagHash != tagId) { + tombstone = new Entry(tagId); + TOMBSTONES[serial] = tombstone; + } + return tombstone; + } + + private Entry(long tagId) { + super(requireKnownName(tagId)); + this.tagHash = tagId; + this.tombstone = true; + this.rawType = OBJECT; + this.rawPrim = 0L; + this.rawObj = null; + } + /** * A custom tag's entry: no registry lookup. A known tag is built from its id instead -- callers * that start from a name resolve it once and pick the id or name factory (see {@code @@ -462,6 +504,7 @@ private Entry(String tag, byte type, long prim, Object obj) { // Resolve the name once: it both validates the id and names the entry. super(requireKnownName(tagId)); this.tagHash = tagId; + this.tombstone = false; this.rawType = type; this.rawPrim = prim; @@ -472,6 +515,7 @@ private Entry(String tag, byte type, long prim, Object obj) { Entry(long tagId, String customTag, byte type, long prim, Object obj) { super(tagId != 0 ? KnownTagCodec.nameOf(tagId) : customTag); this.tagHash = tagId != 0 ? tagId : customHash(customTag); + this.tombstone = false; this.rawType = type; this.rawPrim = prim; @@ -2498,6 +2542,31 @@ Entry findInChain(int hash, String tag) { return null; } + /** + * {@link #findInChain} for a known tag: matches on the entry's id, so it needs no name. Only an + * entry stored under {@code tagId} has that id, and the hash check filters first. + */ + Entry findInChainById(int hash, long tagId) { + for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { + Entry curEntry = curGroup._findById(hash, tagId); + if (curEntry != null) return curEntry; + } + return null; + } + + Entry _findById(int hash, long tagId) { + if (this.hash0 == hash && this.entry0.tagHash == tagId) { + return this.entry0; + } else if (this.hash1 == hash && this.entry1.tagHash == tagId) { + return this.entry1; + } else if (this.hash2 == hash && this.entry2.tagHash == tagId) { + return this.entry2; + } else if (this.hash3 == hash && this.entry3.tagHash == tagId) { + return this.entry3; + } + return null; + } + Entry _find(int hash, String tag) { // if ( this._mayContain(hash) ) return null; @@ -2687,86 +2756,6 @@ Entry _remove(int hash, String tag) { return existingEntry; } - void forEachInChain(Consumer consumer) { - for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { - curGroup._forEach(consumer); - } - } - - void _forEach(Consumer consumer) { - if (this.entry0 != null) consumer.accept(this.entry0); - if (this.entry1 != null) consumer.accept(this.entry1); - if (this.entry2 != null) consumer.accept(this.entry2); - if (this.entry3 != null) consumer.accept(this.entry3); - } - - void forEachInChain(T thisObj, BiConsumer consumer) { - for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { - curGroup._forEach(thisObj, consumer); - } - } - - void _forEach(T thisObj, BiConsumer consumer) { - if (this.entry0 != null) consumer.accept(thisObj, this.entry0); - if (this.entry1 != null) consumer.accept(thisObj, this.entry1); - if (this.entry2 != null) consumer.accept(thisObj, this.entry2); - if (this.entry3 != null) consumer.accept(thisObj, this.entry3); - } - - void forEachInChain( - T thisObj, U otherObj, TriConsumer consumer) { - for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { - curGroup._forEach(thisObj, otherObj, consumer); - } - } - - void _forEach(T thisObj, U otherObj, TriConsumer consumer) { - if (this.entry0 != null) consumer.accept(thisObj, otherObj, this.entry0); - if (this.entry1 != null) consumer.accept(thisObj, otherObj, this.entry1); - if (this.entry2 != null) consumer.accept(thisObj, otherObj, this.entry2); - if (this.entry3 != null) consumer.accept(thisObj, otherObj, this.entry3); - } - - void fillMapFromChain(Map map) { - for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { - curGroup._fillMap(map); - } - } - - void _fillMap(Map map) { - Entry entry0 = this.entry0; - if (entry0 != null) map.put(entry0.tag, entry0.objectValue()); - - Entry entry1 = this.entry1; - if (entry1 != null) map.put(entry1.tag, entry1.objectValue()); - - Entry entry2 = this.entry2; - if (entry2 != null) map.put(entry2.tag, entry2.objectValue()); - - Entry entry3 = this.entry3; - if (entry3 != null) map.put(entry3.tag, entry3.objectValue()); - } - - void fillStringMapFromChain(Map map) { - for (BucketGroup curGroup = this; curGroup != null; curGroup = curGroup.prev) { - curGroup._fillStringMap(map); - } - } - - void _fillStringMap(Map map) { - Entry entry0 = this.entry0; - if (entry0 != null) map.put(entry0.tag, entry0.stringValue()); - - Entry entry1 = this.entry1; - if (entry1 != null) map.put(entry1.tag, entry1.stringValue()); - - Entry entry2 = this.entry2; - if (entry2 != null) map.put(entry2.tag, entry2.stringValue()); - - Entry entry3 = this.entry3; - if (entry3 != null) map.put(entry3.tag, entry3.stringValue()); - } - BucketGroup cloneChain() { BucketGroup thisClone = this._cloneEntries(); @@ -2791,15 +2780,15 @@ BucketGroup _cloneEntries() { @Override public String toString() { - StringBuilder ledger = new StringBuilder(32); - ledger.append('['); + StringBuilder builder = new StringBuilder(32); + builder.append('['); for (int i = 0; i < BucketGroup.LEN; ++i) { - if (i != 0) ledger.append(", "); + if (i != 0) builder.append(", "); - ledger.append(this._entryAt(i)); + builder.append(this._entryAt(i)); } - ledger.append(']'); - return ledger.toString(); + builder.append(']'); + return builder.toString(); } } diff --git a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java new file mode 100644 index 00000000000..9662c577240 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java @@ -0,0 +1,541 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.api.TagMap.EntryReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * The {@link IdStore} contract, run against both stores in the normal test JVM: the dense store is + * behind a flag that is off by default, so the shared {@link TagMap} suites only reach the hash + * store. Each store is built from a parent of its own kind, as the factories do. + */ +class IdStoreContractTest { + + static Stream stores() { + return Stream.of( + arguments(named("HashIdStore", (Function) HashIdStore::new))); + } + + /** Every known tag id, in declaration order. */ + private static List knownIds() throws IllegalAccessException { + List ids = new ArrayList<>(); + for (java.lang.reflect.Field field : KnownTags.class.getFields()) { + if (field.getType() == long.class) { + long id = field.getLong(null); + if (KnownTagCodec.isKnown(id) && !ids.contains(id)) { + ids.add(id); + } + } + } + return ids; + } + + private static Map visible(IdStore store) { + Map seen = new HashMap<>(); + store.forEach(entry -> assertNull(seen.put(entry.tag(), entry.objectValue()), entry.tag())); + return seen; + } + + @ParameterizedTest + @MethodSource("stores") + void typedSettersRoundTripThroughTheReader(Function newStore) { + IdStore store = newStore.apply(null); + store.set(KnownTags.HTTP_ROUTE_ID, (Object) "/users"); + store.set(KnownTags.HTTP_METHOD_ID, (CharSequence) "GET"); + store.set(KnownTags.ERROR_ID, true); + store.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + store.set(KnownTags.PEER_PORT_ID, 5432L); + store.set(KnownTags.DD_AGENT_PSR_ID, 0.5f); + store.set(KnownTags.DB_TYPE_ID, (Object) "postgres"); + store.set(KnownTags.COMPONENT_ID, (CharSequence) "jdbc"); + store.set(KnownTags.SPAN_KIND_ID, (Object) "client"); + store.set(KnownTags.DB_POOL_NAME_ID, (Object) "pool"); + store.set(KnownTags.HTTP_URL_ID, (Object) "http://x"); + store.set(KnownTags.DB_INSTANCE_ID, 2.5d); + + assertEquals("/users", store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("GET", store.getValue(KnownTags.HTTP_METHOD_ID)); + + EntryReader flag = store.getEntryReader(KnownTags.ERROR_ID); + assertTrue(flag.booleanValue()); + assertEquals(EntryReader.BOOLEAN, flag.type()); + assertFalse(flag.isNumber()); + + EntryReader status = store.getEntryReader(KnownTags.HTTP_STATUS_CODE_ID); + assertEquals(200, status.intValue()); + assertEquals(200L, status.longValue()); + assertTrue(status.is(EntryReader.INT)); + assertTrue(status.isNumericPrimitive()); + assertFalse(status.isObject()); + assertEquals(KnownTags.HTTP_STATUS_CODE_ID, status.tagId()); + assertEquals("http.status_code", status.tag()); + + assertEquals(5432L, store.getEntryReader(KnownTags.PEER_PORT_ID).longValue()); + assertEquals(0.5f, store.getEntryReader(KnownTags.DD_AGENT_PSR_ID).floatValue()); + assertEquals(2.5d, store.getEntryReader(KnownTags.DB_INSTANCE_ID).doubleValue()); + + EntryReader route = store.getEntryReader(KnownTags.HTTP_ROUTE_ID); + assertEquals("/users", route.stringValue()); + assertTrue(route.isObject()); + assertEquals("/users", route.entry().objectValue()); + assertEquals("http.route", route.mapEntry().getKey()); + + assertEquals(12, store.size()); + store.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void putAndGetAndPutReturnThePriorValue(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + IdStore store = newStore.apply(parent); + + assertNull(store.put(TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_ID, "GET"))); + EntryReader prior = store.put(TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_ID, "POST")); + assertEquals("GET", prior.objectValue()); + + // getAndPut reports the prior visible value, inherited included + EntryReader inherited = + store.getAndPut(TagMap.Entry.newAnyEntry(KnownTags.HTTP_ROUTE_ID, "/local")); + assertEquals("/inherited", inherited.objectValue()); + assertEquals("/local", store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals( + "/local", + store.getAndPut(TagMap.Entry.newAnyEntry(KnownTags.HTTP_ROUTE_ID, "/again")).objectValue()); + assertNull(store.getAndPut(TagMap.Entry.newAnyEntry(KnownTags.COMPONENT_ID, "okhttp"))); + store.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void removingAnInheritedTagHidesItUntilItIsSetAgain(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.set(KnownTags.COMPONENT_ID, "okhttp"); + IdStore store = newStore.apply(parent); + store.set(KnownTags.HTTP_METHOD_ID, "GET"); + assertTrue(store.hasParent()); + assertEquals(3, store.size()); + + assertEquals("/inherited", store.remove(KnownTags.HTTP_ROUTE_ID).objectValue()); + assertNull(store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertNull(store.remove(KnownTags.HTTP_ROUTE_ID), "already hidden"); + assertEquals(2, store.size()); + assertEquals("/inherited", parent.getValue(KnownTags.HTTP_ROUTE_ID), "parent untouched"); + + // a local value that also hides an inherited one is removed with its tombstone in place + store.set(KnownTags.COMPONENT_ID, "local"); + assertEquals("local", store.remove(KnownTags.COMPONENT_ID).objectValue()); + assertNull(store.getValue(KnownTags.COMPONENT_ID)); + + assertEquals("GET", store.remove(KnownTags.HTTP_METHOD_ID).objectValue()); + assertNull(store.remove(KnownTags.HTTP_METHOD_ID), "absent"); + assertTrue(store.isEmpty()); + assertTrue(store.isDefinitelyEmpty() || store.estimateSize() >= 0); + + // setting a hidden tag again revives it + store.set(KnownTags.HTTP_ROUTE_ID, "/revived"); + assertEquals("/revived", store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals(1, store.size()); + assertFalse(store.isEmpty()); + store.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void sizeAndEmptinessCountWhatIsVisible(Function newStore) { + IdStore empty = newStore.apply(null); + assertTrue(empty.isEmpty()); + assertTrue(empty.isDefinitelyEmpty()); + assertEquals(0, empty.estimateSize()); + assertFalse(empty.hasParent()); + + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/a"); + parent.set(KnownTags.HTTP_METHOD_ID, "GET"); + IdStore child = newStore.apply(parent); + assertFalse(child.isEmpty(), "inherits visible tags"); + assertFalse(child.isDefinitelyEmpty()); + + child.set(KnownTags.HTTP_METHOD_ID, "POST"); // shadows + child.set(KnownTags.COMPONENT_ID, "okhttp"); + assertEquals(3, child.size()); + assertTrue(child.estimateSize() >= 3); + + child.remove(KnownTags.HTTP_ROUTE_ID); + child.remove(KnownTags.HTTP_METHOD_ID); + child.remove(KnownTags.COMPONENT_ID); + assertTrue(child.isEmpty(), "every inherited tag hidden"); + assertEquals(0, child.size()); + } + + @ParameterizedTest + @MethodSource("stores") + void everyIterationSeesEachVisibleTagOnce(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/a"); + parent.set(KnownTags.HTTP_METHOD_ID, "GET"); + parent.set(KnownTags.COMPONENT_ID, "okhttp"); + IdStore store = newStore.apply(parent); + store.set(KnownTags.HTTP_METHOD_ID, "POST"); // shadows the parent's + store.set(KnownTags.SPAN_KIND_ID, "client"); + store.remove(KnownTags.COMPONENT_ID); // hides the parent's + + Map expected = new HashMap<>(); + expected.put("http.route", "/a"); + expected.put("http.method", "POST"); + expected.put("span.kind", "client"); + assertEquals(expected, visible(store)); + + Map viaContext = new HashMap<>(); + store.forEach(viaContext, (map, entry) -> map.put(entry.tag(), entry.objectValue())); + assertEquals(expected, viaContext); + + Map viaTwoContexts = new HashMap<>(); + store.forEach( + viaTwoContexts, "-", (map, suffix, entry) -> map.put(entry.tag(), entry.objectValue())); + assertEquals(expected, viaTwoContexts); + + Map viaCursor = new HashMap<>(); + IdStore.Cursor cursor = store.cursor(); + for (EntryReader entry = cursor.next(); entry != null; entry = cursor.next()) { + assertNull(viaCursor.put(entry.tag(), entry.objectValue()), entry.tag()); + } + assertEquals(expected, viaCursor); + } + + @ParameterizedTest + @MethodSource("stores") + void aCopyIsIndependent(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/a"); + IdStore store = newStore.apply(parent); + store.set(KnownTags.HTTP_METHOD_ID, "GET"); + store.remove(KnownTags.HTTP_ROUTE_ID); + + IdStore copy = store.copy(); + copy.set(KnownTags.HTTP_METHOD_ID, "POST"); + copy.set(KnownTags.HTTP_ROUTE_ID, "/copy"); + + assertEquals("GET", store.getValue(KnownTags.HTTP_METHOD_ID)); + assertNull(store.getValue(KnownTags.HTTP_ROUTE_ID), "the original keeps its tombstone"); + assertEquals("POST", copy.getValue(KnownTags.HTTP_METHOD_ID)); + assertEquals("/copy", copy.getValue(KnownTags.HTTP_ROUTE_ID)); + copy.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void putAllLocalMergesAnotherStoresTags(Function newStore) { + IdStore source = newStore.apply(null); + source.set(KnownTags.HTTP_ROUTE_ID, "/a"); + source.set(KnownTags.HTTP_METHOD_ID, "GET"); + + IdStore empty = newStore.apply(null); + empty.putAllLocal(source); // nothing here yet + assertEquals(visible(source), visible(empty)); + + IdStore merged = newStore.apply(null); + merged.set(KnownTags.HTTP_METHOD_ID, "POST"); + merged.set(KnownTags.COMPONENT_ID, "okhttp"); + merged.putAllLocal(source); + assertEquals("GET", merged.getValue(KnownTags.HTTP_METHOD_ID), "the source wins"); + assertEquals("okhttp", merged.getValue(KnownTags.COMPONENT_ID)); + assertEquals(3, merged.size()); + + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + IdStore tombstoned = newStore.apply(parent); + tombstoned.remove(KnownTags.HTTP_ROUTE_ID); + tombstoned.putAllLocal(source); // replaces the tombstone + assertEquals("/a", tombstoned.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals(2, tombstoned.size()); + + newStore.apply(null).putAllLocal(newStore.apply(null)); // an empty source is a no-op + merged.checkIntegrity(); + tombstoned.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void fillsCopyTheLocalTags(Function newStore) { + IdStore store = newStore.apply(null); + store.set(KnownTags.HTTP_ROUTE_ID, "/a"); + store.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + + Map map = new HashMap<>(); + store.fillMapLocal(map); + assertEquals("/a", map.get("http.route")); + assertEquals(200, map.get("http.status_code")); + + Map strings = new HashMap<>(); + store.fillStringMapLocal(strings); + assertEquals("200", strings.get("http.status_code")); + } + + @ParameterizedTest + @MethodSource("stores") + void growsPastItsFirstCapacity(Function newStore) throws Exception { + IdStore store = newStore.apply(null); + List ids = knownIds(); + int n = Math.min(40, ids.size()); + for (int i = 0; i < n; ++i) { + store.set(ids.get(i), "v" + i); + } + for (int i = 0; i < n; ++i) { + assertEquals("v" + i, store.getValue(ids.get(i))); + } + assertEquals(n, store.size()); + assertTrue(store.computeSize() > 0); + assertNotNull(store.toInternalString()); + store.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void putAllLocalMergesOverlappingStoresWithCollidingTags(Function newStore) + throws Exception { + // enough tags that buckets hold groups of colliding entries on both sides of the merge + List ids = knownIds(); + IdStore target = newStore.apply(null); + IdStore source = newStore.apply(null); + for (int i = 0; i < 30; ++i) { + target.set(ids.get(i), "target" + i); + } + for (int i = 20; i < 50; ++i) { + source.set(ids.get(i), "source" + i); + } + target.putAllLocal(source); + assertEquals(50, target.size()); + for (int i = 0; i < 50; ++i) { + assertEquals(i < 20 ? "target" + i : "source" + i, target.getValue(ids.get(i))); + } + target.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void getAndPutRevivesARemovedInheritedTag(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + IdStore store = newStore.apply(parent); + store.remove(KnownTags.HTTP_ROUTE_ID); + + // hidden, so there is no visible prior value + assertNull(store.getAndPut(TagMap.Entry.newAnyEntry(KnownTags.HTTP_ROUTE_ID, "/revived"))); + assertEquals("/revived", store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals(1, store.size()); + store.checkIntegrity(); + } + + @ParameterizedTest + @MethodSource("stores") + void readsGoThroughEveryAncestor(Function newStore) { + IdStore grandparent = newStore.apply(null); + grandparent.set(KnownTags.HTTP_ROUTE_ID, "/grandparent"); + grandparent.set(KnownTags.COMPONENT_ID, "okhttp"); + IdStore parent = newStore.apply(grandparent); + parent.set(KnownTags.HTTP_METHOD_ID, "GET"); + parent.remove(KnownTags.COMPONENT_ID); + IdStore store = newStore.apply(parent); + + assertEquals("/grandparent", store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("GET", store.getValue(KnownTags.HTTP_METHOD_ID)); + assertNull(store.getValue(KnownTags.COMPONENT_ID), "hidden by the parent"); + assertEquals(2, store.size()); + Map expected = new HashMap<>(); + expected.put("http.route", "/grandparent"); + expected.put("http.method", "GET"); + assertEquals(expected, visible(store)); + } + + @ParameterizedTest + @MethodSource("stores") + void getStringFormatsTheVisibleValue(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.set(KnownTags.COMPONENT_ID, "hidden"); + IdStore store = newStore.apply(parent); + store.set(KnownTags.PEER_PORT_ID, 5432); + store.set(KnownTags.DB_INSTANCE_ID, 1.5d); + store.remove(KnownTags.COMPONENT_ID); + + assertEquals("5432", store.getString(KnownTags.PEER_PORT_ID)); + assertEquals("1.5", store.getString(KnownTags.DB_INSTANCE_ID)); + assertEquals("/inherited", store.getString(KnownTags.HTTP_ROUTE_ID)); + assertNull(store.getString(KnownTags.COMPONENT_ID), "hidden by the removal"); + assertNull(store.getString(KnownTags.HTTP_METHOD_ID)); + assertThrows( + IllegalArgumentException.class, () -> store.getString(KnownTagCodec.makeTagId(9999))); + } + + @ParameterizedTest + @MethodSource("stores") + void settingANullValueRemovesTheTag(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + IdStore store = newStore.apply(parent); + store.set(KnownTags.HTTP_METHOD_ID, "GET"); + + store.set(KnownTags.HTTP_METHOD_ID, (Object) null); + store.set(KnownTags.HTTP_ROUTE_ID, (CharSequence) null); // hides the inherited tag + assertNull(store.getValue(KnownTags.HTTP_METHOD_ID)); + assertNull(store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals(0, store.size()); + assertEquals("/inherited", parent.getValue(KnownTags.HTTP_ROUTE_ID), "the parent keeps it"); + } + + @ParameterizedTest + @MethodSource("stores") + void aReaderWithNoEntryIsANoOp(Function newStore) { + IdStore store = newStore.apply(null); + store.set(KnownTags.HTTP_ROUTE_ID, "/a"); + NoEntryReader empty = new NoEntryReader("http.route", KnownTags.HTTP_ROUTE_ID); + + assertNull(store.put(empty)); + assertNull(store.getAndPut(empty)); + assertEquals("/a", store.getValue(KnownTags.HTTP_ROUTE_ID), "left as it was"); + assertEquals(1, store.size()); + } + + @ParameterizedTest + @MethodSource("stores") + void getEntryReturnsAnEntryTheCallerMayKeep(Function newStore) { + IdStore parent = newStore.apply(null); + parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + IdStore store = newStore.apply(parent); + store.set(KnownTags.HTTP_METHOD_ID, "GET"); + + TagMap.Entry method = store.getEntry(KnownTags.HTTP_METHOD_ID); + TagMap.Entry route = store.getEntry(KnownTags.HTTP_ROUTE_ID); + EntryReader methodReader = store.getEntryReader(KnownTags.HTTP_METHOD_ID); + EntryReader routeReader = store.getEntryReader(KnownTags.HTTP_ROUTE_ID); + + // kept side by side, each still holds its own tag + assertEquals("GET", method.objectValue()); + assertEquals(KnownTags.HTTP_METHOD_ID, method.tagId()); + assertEquals("/inherited", route.objectValue()); + assertEquals("GET", methodReader.objectValue()); + assertEquals("/inherited", routeReader.objectValue()); + assertNull(store.getEntry(KnownTags.COMPONENT_ID)); + assertThrows( + IllegalArgumentException.class, () -> store.getEntry(KnownTagCodec.makeTagId(9999))); + } + + @ParameterizedTest + @MethodSource("stores") + void anUnknownIdIsRejected(Function newStore) { + IdStore store = newStore.apply(null); + long unknown = KnownTagCodec.makeTagId(9999); + assertThrows(IllegalArgumentException.class, () -> store.getValue(unknown)); + assertThrows(IllegalArgumentException.class, () -> store.remove(unknown)); + } + + /** An EntryReader with no entry and no value, as a caller outside the tracer may pass. */ + static final class NoEntryReader implements TagMap.EntryReader { + private final String tag; + private final long tagId; + + NoEntryReader(String tag, long tagId) { + this.tag = tag; + this.tagId = tagId; + } + + @Override + public String tag() { + return tag; + } + + @Override + public long tagId() { + return tagId; + } + + @Override + public byte type() { + return TagMap.EntryReader.OBJECT; + } + + @Override + public boolean is(byte type) { + return false; + } + + @Override + public boolean isNumericPrimitive() { + return false; + } + + @Override + public boolean isNumber() { + return false; + } + + @Override + public boolean isObject() { + return true; + } + + @Override + public Object objectValue() { + return null; + } + + @Override + public String stringValue() { + return null; + } + + @Override + public boolean booleanValue() { + return false; + } + + @Override + public int intValue() { + return 0; + } + + @Override + public long longValue() { + return 0; + } + + @Override + public float floatValue() { + return 0; + } + + @Override + public double doubleValue() { + return 0; + } + + @Override + public java.util.Map.Entry mapEntry() { + return null; + } + + @Override + public TagMap.Entry entry() { + return null; + } + } +} diff --git a/tag-conventions.yaml b/tag-conventions.yaml index 3f13f0fbd49..0f38f733745 100644 --- a/tag-conventions.yaml +++ b/tag-conventions.yaml @@ -88,6 +88,7 @@ trace_level: - { dd-name: _dd.trace_span_attribute_schema, type: int, required: required } - { dd-name: _dd.profiling.ctx, type: string, required: optional } # once the profiling context integration is available - { dd-name: process_id, type: long, required: required } + - { dd-name: _dd.agent_psr, type: double, required: optional } # the agent-provided sampling rate the sampler applied - { dd-name: ai_guard.event, type: boolean, required: optional } # when the trace holds an AI Guard evaluation span span_types: From afad98bc652a192eddc4830ae66a4816f9844173 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Mon, 26 Oct 2026 14:47:49 -0400 Subject: [PATCH 2/2] Load and store values in the tag stores, as through SpanStorage The stores' value methods were get/set, the verbs of TagMap's Map surface and the span's intercepted setTag, though the stores are the raw layer SpanStorage reads and writes. Values now move by load, loadString and store, so the span's SpanStorage methods delegate verb for verb; entries keep getEntry, put and remove. HashStore.get, which answers an entry, becomes getEntry. Co-Authored-By: Claude Opus 5.5 --- .../java/datadog/trace/api/HashIdStore.java | 18 +- .../java/datadog/trace/api/HashStore.java | 21 ++- .../main/java/datadog/trace/api/IdStore.java | 25 +-- .../trace/api/IdStoreContractTest.java | 176 +++++++++--------- 4 files changed, 123 insertions(+), 117 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java index 557dc730687..86064f688b2 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashIdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashIdStore.java @@ -103,19 +103,19 @@ private Entry getVisible(long tagId) { } @Override - public Object getValue(long tagId) { + public Object load(long tagId) { Entry entry = this.getEntry(tagId); return entry == null ? null : entry.objectValue(); } @Override - public String getString(long tagId) { + public String loadString(long tagId) { Entry entry = this.getEntry(tagId); return entry == null ? null : entry.stringValue(); } @Override - public void set(long tagId, Object value) { + public void store(long tagId, Object value) { if (value == null) { this.remove(tagId); // a null value removes the tag return; @@ -124,7 +124,7 @@ public void set(long tagId, Object value) { } @Override - public void set(long tagId, CharSequence value) { + public void store(long tagId, CharSequence value) { if (value == null) { this.remove(tagId); // a null value removes the tag return; @@ -133,27 +133,27 @@ public void set(long tagId, CharSequence value) { } @Override - public void set(long tagId, boolean value) { + public void store(long tagId, boolean value) { this.put(Entry.newBooleanEntry(tagId, value)); } @Override - public void set(long tagId, int value) { + public void store(long tagId, int value) { this.put(Entry.newIntEntry(tagId, value)); } @Override - public void set(long tagId, long value) { + public void store(long tagId, long value) { this.put(Entry.newLongEntry(tagId, value)); } @Override - public void set(long tagId, float value) { + public void store(long tagId, float value) { this.put(Entry.newFloatEntry(tagId, value)); } @Override - public void set(long tagId, double value) { + public void store(long tagId, double value) { this.put(Entry.newDoubleEntry(tagId, value)); } diff --git a/internal-api/src/main/java/datadog/trace/api/HashStore.java b/internal-api/src/main/java/datadog/trace/api/HashStore.java index 3b746cb5244..4629a064d18 100644 --- a/internal-api/src/main/java/datadog/trace/api/HashStore.java +++ b/internal-api/src/main/java/datadog/trace/api/HashStore.java @@ -17,6 +17,9 @@ * custom tag names (asserted): a known tag is resolved to its id first, and goes to the {@link * IdStore}. * + *

Values move by {@code store}, as through {@link SpanStorage}; entries by {@code getEntry}, + * {@code put} and {@code remove}. + * *

Created lazily by its owner -- a {@link TagMap}, or a span in the tracer core -- on the first * custom write ({@link #create}); until then the owner borrows its parent's custom store, if any, * read-only. No locking: guarded by the owner's lock. @@ -118,7 +121,7 @@ int estimateSize() { } /** The custom tag {@code tag}'s entry, reading through to the parent chain. */ - public Entry get(String tag) { + public Entry getEntry(String tag) { assert KnownTagCodec.keyOf(tag) == 0 : "'" + tag + "' is a known tag; look it up by its id"; return this.get(tag, Entry.bucketHash(Entry.customHash(tag))); } @@ -139,10 +142,10 @@ private Entry get(String tag, int hash) { } /* - * Sets the custom tag {@code tag}, which must not be a known tag's name. + * Stores the custom tag {@code tag}, which must not be a known tag's name. */ - public void set(String tag, Object value) { + public void store(String tag, Object value) { if (value == null) { this.remove(tag); // a null value removes the tag return; @@ -150,7 +153,7 @@ public void set(String tag, Object value) { this.put(Entry.newAnyEntry(tag, value)); } - public void set(String tag, CharSequence value) { + public void store(String tag, CharSequence value) { if (value == null) { this.remove(tag); // a null value removes the tag return; @@ -158,23 +161,23 @@ public void set(String tag, CharSequence value) { this.put(Entry.newObjectEntry(tag, value)); } - public void set(String tag, boolean value) { + public void store(String tag, boolean value) { this.put(Entry.newBooleanEntry(tag, value)); } - public void set(String tag, int value) { + public void store(String tag, int value) { this.put(Entry.newIntEntry(tag, value)); } - public void set(String tag, long value) { + public void store(String tag, long value) { this.put(Entry.newLongEntry(tag, value)); } - public void set(String tag, float value) { + public void store(String tag, float value) { this.put(Entry.newFloatEntry(tag, value)); } - public void set(String tag, double value) { + public void store(String tag, double value) { this.put(Entry.newDoubleEntry(tag, value)); } diff --git a/internal-api/src/main/java/datadog/trace/api/IdStore.java b/internal-api/src/main/java/datadog/trace/api/IdStore.java index d9e2f1bdf86..c9d7fd5feff 100644 --- a/internal-api/src/main/java/datadog/trace/api/IdStore.java +++ b/internal-api/src/main/java/datadog/trace/api/IdStore.java @@ -15,6 +15,9 @@ *

A span in the tracer core owns its id store directly ({@link #create}); a {@link TagMap} owns * one too. * + *

Values move by {@code load} and {@code store}, as through {@link SpanStorage}; entries by + * {@code getEntry}, {@code put} and {@code remove}. + * *

{@link IdStoreFactory} builds every store, so the JVM loads one implementation and class * hierarchy analysis devirtualizes every call on an {@code IdStore} without a type guard; nothing * that is always loaded may extend this class. No locking: guarded by the owner's lock. @@ -60,32 +63,32 @@ static IllegalArgumentException notKnown(long tagId) { * {@link #getEntryReader}'s value, boxed if necessary, or null -- for a caller that needs only * the value, so a store that keeps known tags without an Entry per tag can answer without one. */ - public abstract Object getValue(long tagId); + public abstract Object load(long tagId); /** - * {@link #getValue} as a String, or null. A store that keeps an Entry per tag answers with the + * {@link #load} as a String, or null. A store that keeps an Entry per tag answers with the * Entry's cached string, so a numeric tag read repeatedly, as the serializer does, is formatted * once. */ - public abstract String getString(long tagId); + public abstract String loadString(long tagId); /* - * Sets a known tag without validating its id: the caller has checked KnownTagCodec.isKnown. + * Stores a known tag without validating its id: the caller has checked KnownTagCodec.isKnown. */ - public abstract void set(long tagId, Object value); + public abstract void store(long tagId, Object value); - public abstract void set(long tagId, CharSequence value); + public abstract void store(long tagId, CharSequence value); - public abstract void set(long tagId, boolean value); + public abstract void store(long tagId, boolean value); - public abstract void set(long tagId, int value); + public abstract void store(long tagId, int value); - public abstract void set(long tagId, long value); + public abstract void store(long tagId, long value); - public abstract void set(long tagId, float value); + public abstract void store(long tagId, float value); - public abstract void set(long tagId, double value); + public abstract void store(long tagId, double value); /** * Inserts or replaces a known tag, returning the local entry it replaced (or null). Does not diff --git a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java index 9662c577240..0d35128b933 100644 --- a/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java +++ b/internal-api/src/test/java/datadog/trace/api/IdStoreContractTest.java @@ -56,21 +56,21 @@ private static Map visible(IdStore store) { @MethodSource("stores") void typedSettersRoundTripThroughTheReader(Function newStore) { IdStore store = newStore.apply(null); - store.set(KnownTags.HTTP_ROUTE_ID, (Object) "/users"); - store.set(KnownTags.HTTP_METHOD_ID, (CharSequence) "GET"); - store.set(KnownTags.ERROR_ID, true); - store.set(KnownTags.HTTP_STATUS_CODE_ID, 200); - store.set(KnownTags.PEER_PORT_ID, 5432L); - store.set(KnownTags.DD_AGENT_PSR_ID, 0.5f); - store.set(KnownTags.DB_TYPE_ID, (Object) "postgres"); - store.set(KnownTags.COMPONENT_ID, (CharSequence) "jdbc"); - store.set(KnownTags.SPAN_KIND_ID, (Object) "client"); - store.set(KnownTags.DB_POOL_NAME_ID, (Object) "pool"); - store.set(KnownTags.HTTP_URL_ID, (Object) "http://x"); - store.set(KnownTags.DB_INSTANCE_ID, 2.5d); - - assertEquals("/users", store.getValue(KnownTags.HTTP_ROUTE_ID)); - assertEquals("GET", store.getValue(KnownTags.HTTP_METHOD_ID)); + store.store(KnownTags.HTTP_ROUTE_ID, (Object) "/users"); + store.store(KnownTags.HTTP_METHOD_ID, (CharSequence) "GET"); + store.store(KnownTags.ERROR_ID, true); + store.store(KnownTags.HTTP_STATUS_CODE_ID, 200); + store.store(KnownTags.PEER_PORT_ID, 5432L); + store.store(KnownTags.DD_AGENT_PSR_ID, 0.5f); + store.store(KnownTags.DB_TYPE_ID, (Object) "postgres"); + store.store(KnownTags.COMPONENT_ID, (CharSequence) "jdbc"); + store.store(KnownTags.SPAN_KIND_ID, (Object) "client"); + store.store(KnownTags.DB_POOL_NAME_ID, (Object) "pool"); + store.store(KnownTags.HTTP_URL_ID, (Object) "http://x"); + store.store(KnownTags.DB_INSTANCE_ID, 2.5d); + + assertEquals("/users", store.load(KnownTags.HTTP_ROUTE_ID)); + assertEquals("GET", store.load(KnownTags.HTTP_METHOD_ID)); EntryReader flag = store.getEntryReader(KnownTags.ERROR_ID); assertTrue(flag.booleanValue()); @@ -104,7 +104,7 @@ void typedSettersRoundTripThroughTheReader(Function newStore) @MethodSource("stores") void putAndGetAndPutReturnThePriorValue(Function newStore) { IdStore parent = newStore.apply(null); - parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.store(KnownTags.HTTP_ROUTE_ID, "/inherited"); IdStore store = newStore.apply(parent); assertNull(store.put(TagMap.Entry.newAnyEntry(KnownTags.HTTP_METHOD_ID, "GET"))); @@ -115,7 +115,7 @@ void putAndGetAndPutReturnThePriorValue(Function newStore) { EntryReader inherited = store.getAndPut(TagMap.Entry.newAnyEntry(KnownTags.HTTP_ROUTE_ID, "/local")); assertEquals("/inherited", inherited.objectValue()); - assertEquals("/local", store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("/local", store.load(KnownTags.HTTP_ROUTE_ID)); assertEquals( "/local", store.getAndPut(TagMap.Entry.newAnyEntry(KnownTags.HTTP_ROUTE_ID, "/again")).objectValue()); @@ -127,23 +127,23 @@ void putAndGetAndPutReturnThePriorValue(Function newStore) { @MethodSource("stores") void removingAnInheritedTagHidesItUntilItIsSetAgain(Function newStore) { IdStore parent = newStore.apply(null); - parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); - parent.set(KnownTags.COMPONENT_ID, "okhttp"); + parent.store(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.store(KnownTags.COMPONENT_ID, "okhttp"); IdStore store = newStore.apply(parent); - store.set(KnownTags.HTTP_METHOD_ID, "GET"); + store.store(KnownTags.HTTP_METHOD_ID, "GET"); assertTrue(store.hasParent()); assertEquals(3, store.size()); assertEquals("/inherited", store.remove(KnownTags.HTTP_ROUTE_ID).objectValue()); - assertNull(store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertNull(store.load(KnownTags.HTTP_ROUTE_ID)); assertNull(store.remove(KnownTags.HTTP_ROUTE_ID), "already hidden"); assertEquals(2, store.size()); - assertEquals("/inherited", parent.getValue(KnownTags.HTTP_ROUTE_ID), "parent untouched"); + assertEquals("/inherited", parent.load(KnownTags.HTTP_ROUTE_ID), "parent untouched"); // a local value that also hides an inherited one is removed with its tombstone in place - store.set(KnownTags.COMPONENT_ID, "local"); + store.store(KnownTags.COMPONENT_ID, "local"); assertEquals("local", store.remove(KnownTags.COMPONENT_ID).objectValue()); - assertNull(store.getValue(KnownTags.COMPONENT_ID)); + assertNull(store.load(KnownTags.COMPONENT_ID)); assertEquals("GET", store.remove(KnownTags.HTTP_METHOD_ID).objectValue()); assertNull(store.remove(KnownTags.HTTP_METHOD_ID), "absent"); @@ -151,8 +151,8 @@ void removingAnInheritedTagHidesItUntilItIsSetAgain(Function n assertTrue(store.isDefinitelyEmpty() || store.estimateSize() >= 0); // setting a hidden tag again revives it - store.set(KnownTags.HTTP_ROUTE_ID, "/revived"); - assertEquals("/revived", store.getValue(KnownTags.HTTP_ROUTE_ID)); + store.store(KnownTags.HTTP_ROUTE_ID, "/revived"); + assertEquals("/revived", store.load(KnownTags.HTTP_ROUTE_ID)); assertEquals(1, store.size()); assertFalse(store.isEmpty()); store.checkIntegrity(); @@ -168,14 +168,14 @@ void sizeAndEmptinessCountWhatIsVisible(Function newStore) { assertFalse(empty.hasParent()); IdStore parent = newStore.apply(null); - parent.set(KnownTags.HTTP_ROUTE_ID, "/a"); - parent.set(KnownTags.HTTP_METHOD_ID, "GET"); + parent.store(KnownTags.HTTP_ROUTE_ID, "/a"); + parent.store(KnownTags.HTTP_METHOD_ID, "GET"); IdStore child = newStore.apply(parent); assertFalse(child.isEmpty(), "inherits visible tags"); assertFalse(child.isDefinitelyEmpty()); - child.set(KnownTags.HTTP_METHOD_ID, "POST"); // shadows - child.set(KnownTags.COMPONENT_ID, "okhttp"); + child.store(KnownTags.HTTP_METHOD_ID, "POST"); // shadows + child.store(KnownTags.COMPONENT_ID, "okhttp"); assertEquals(3, child.size()); assertTrue(child.estimateSize() >= 3); @@ -190,12 +190,12 @@ void sizeAndEmptinessCountWhatIsVisible(Function newStore) { @MethodSource("stores") void everyIterationSeesEachVisibleTagOnce(Function newStore) { IdStore parent = newStore.apply(null); - parent.set(KnownTags.HTTP_ROUTE_ID, "/a"); - parent.set(KnownTags.HTTP_METHOD_ID, "GET"); - parent.set(KnownTags.COMPONENT_ID, "okhttp"); + parent.store(KnownTags.HTTP_ROUTE_ID, "/a"); + parent.store(KnownTags.HTTP_METHOD_ID, "GET"); + parent.store(KnownTags.COMPONENT_ID, "okhttp"); IdStore store = newStore.apply(parent); - store.set(KnownTags.HTTP_METHOD_ID, "POST"); // shadows the parent's - store.set(KnownTags.SPAN_KIND_ID, "client"); + store.store(KnownTags.HTTP_METHOD_ID, "POST"); // shadows the parent's + store.store(KnownTags.SPAN_KIND_ID, "client"); store.remove(KnownTags.COMPONENT_ID); // hides the parent's Map expected = new HashMap<>(); @@ -225,19 +225,19 @@ void everyIterationSeesEachVisibleTagOnce(Function newStore) { @MethodSource("stores") void aCopyIsIndependent(Function newStore) { IdStore parent = newStore.apply(null); - parent.set(KnownTags.HTTP_ROUTE_ID, "/a"); + parent.store(KnownTags.HTTP_ROUTE_ID, "/a"); IdStore store = newStore.apply(parent); - store.set(KnownTags.HTTP_METHOD_ID, "GET"); + store.store(KnownTags.HTTP_METHOD_ID, "GET"); store.remove(KnownTags.HTTP_ROUTE_ID); IdStore copy = store.copy(); - copy.set(KnownTags.HTTP_METHOD_ID, "POST"); - copy.set(KnownTags.HTTP_ROUTE_ID, "/copy"); + copy.store(KnownTags.HTTP_METHOD_ID, "POST"); + copy.store(KnownTags.HTTP_ROUTE_ID, "/copy"); - assertEquals("GET", store.getValue(KnownTags.HTTP_METHOD_ID)); - assertNull(store.getValue(KnownTags.HTTP_ROUTE_ID), "the original keeps its tombstone"); - assertEquals("POST", copy.getValue(KnownTags.HTTP_METHOD_ID)); - assertEquals("/copy", copy.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("GET", store.load(KnownTags.HTTP_METHOD_ID)); + assertNull(store.load(KnownTags.HTTP_ROUTE_ID), "the original keeps its tombstone"); + assertEquals("POST", copy.load(KnownTags.HTTP_METHOD_ID)); + assertEquals("/copy", copy.load(KnownTags.HTTP_ROUTE_ID)); copy.checkIntegrity(); } @@ -245,27 +245,27 @@ void aCopyIsIndependent(Function newStore) { @MethodSource("stores") void putAllLocalMergesAnotherStoresTags(Function newStore) { IdStore source = newStore.apply(null); - source.set(KnownTags.HTTP_ROUTE_ID, "/a"); - source.set(KnownTags.HTTP_METHOD_ID, "GET"); + source.store(KnownTags.HTTP_ROUTE_ID, "/a"); + source.store(KnownTags.HTTP_METHOD_ID, "GET"); IdStore empty = newStore.apply(null); empty.putAllLocal(source); // nothing here yet assertEquals(visible(source), visible(empty)); IdStore merged = newStore.apply(null); - merged.set(KnownTags.HTTP_METHOD_ID, "POST"); - merged.set(KnownTags.COMPONENT_ID, "okhttp"); + merged.store(KnownTags.HTTP_METHOD_ID, "POST"); + merged.store(KnownTags.COMPONENT_ID, "okhttp"); merged.putAllLocal(source); - assertEquals("GET", merged.getValue(KnownTags.HTTP_METHOD_ID), "the source wins"); - assertEquals("okhttp", merged.getValue(KnownTags.COMPONENT_ID)); + assertEquals("GET", merged.load(KnownTags.HTTP_METHOD_ID), "the source wins"); + assertEquals("okhttp", merged.load(KnownTags.COMPONENT_ID)); assertEquals(3, merged.size()); IdStore parent = newStore.apply(null); - parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.store(KnownTags.HTTP_ROUTE_ID, "/inherited"); IdStore tombstoned = newStore.apply(parent); tombstoned.remove(KnownTags.HTTP_ROUTE_ID); tombstoned.putAllLocal(source); // replaces the tombstone - assertEquals("/a", tombstoned.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("/a", tombstoned.load(KnownTags.HTTP_ROUTE_ID)); assertEquals(2, tombstoned.size()); newStore.apply(null).putAllLocal(newStore.apply(null)); // an empty source is a no-op @@ -277,8 +277,8 @@ void putAllLocalMergesAnotherStoresTags(Function newStore) { @MethodSource("stores") void fillsCopyTheLocalTags(Function newStore) { IdStore store = newStore.apply(null); - store.set(KnownTags.HTTP_ROUTE_ID, "/a"); - store.set(KnownTags.HTTP_STATUS_CODE_ID, 200); + store.store(KnownTags.HTTP_ROUTE_ID, "/a"); + store.store(KnownTags.HTTP_STATUS_CODE_ID, 200); Map map = new HashMap<>(); store.fillMapLocal(map); @@ -297,10 +297,10 @@ void growsPastItsFirstCapacity(Function newStore) throws Excep List ids = knownIds(); int n = Math.min(40, ids.size()); for (int i = 0; i < n; ++i) { - store.set(ids.get(i), "v" + i); + store.store(ids.get(i), "v" + i); } for (int i = 0; i < n; ++i) { - assertEquals("v" + i, store.getValue(ids.get(i))); + assertEquals("v" + i, store.load(ids.get(i))); } assertEquals(n, store.size()); assertTrue(store.computeSize() > 0); @@ -317,15 +317,15 @@ void putAllLocalMergesOverlappingStoresWithCollidingTags(Function newStore) { IdStore parent = newStore.apply(null); - parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.store(KnownTags.HTTP_ROUTE_ID, "/inherited"); IdStore store = newStore.apply(parent); store.remove(KnownTags.HTTP_ROUTE_ID); // hidden, so there is no visible prior value assertNull(store.getAndPut(TagMap.Entry.newAnyEntry(KnownTags.HTTP_ROUTE_ID, "/revived"))); - assertEquals("/revived", store.getValue(KnownTags.HTTP_ROUTE_ID)); + assertEquals("/revived", store.load(KnownTags.HTTP_ROUTE_ID)); assertEquals(1, store.size()); store.checkIntegrity(); } @@ -349,16 +349,16 @@ void getAndPutRevivesARemovedInheritedTag(Function newStore) { @MethodSource("stores") void readsGoThroughEveryAncestor(Function newStore) { IdStore grandparent = newStore.apply(null); - grandparent.set(KnownTags.HTTP_ROUTE_ID, "/grandparent"); - grandparent.set(KnownTags.COMPONENT_ID, "okhttp"); + grandparent.store(KnownTags.HTTP_ROUTE_ID, "/grandparent"); + grandparent.store(KnownTags.COMPONENT_ID, "okhttp"); IdStore parent = newStore.apply(grandparent); - parent.set(KnownTags.HTTP_METHOD_ID, "GET"); + parent.store(KnownTags.HTTP_METHOD_ID, "GET"); parent.remove(KnownTags.COMPONENT_ID); IdStore store = newStore.apply(parent); - assertEquals("/grandparent", store.getValue(KnownTags.HTTP_ROUTE_ID)); - assertEquals("GET", store.getValue(KnownTags.HTTP_METHOD_ID)); - assertNull(store.getValue(KnownTags.COMPONENT_ID), "hidden by the parent"); + assertEquals("/grandparent", store.load(KnownTags.HTTP_ROUTE_ID)); + assertEquals("GET", store.load(KnownTags.HTTP_METHOD_ID)); + assertNull(store.load(KnownTags.COMPONENT_ID), "hidden by the parent"); assertEquals(2, store.size()); Map expected = new HashMap<>(); expected.put("http.route", "/grandparent"); @@ -370,48 +370,48 @@ void readsGoThroughEveryAncestor(Function newStore) { @MethodSource("stores") void getStringFormatsTheVisibleValue(Function newStore) { IdStore parent = newStore.apply(null); - parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); - parent.set(KnownTags.COMPONENT_ID, "hidden"); + parent.store(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.store(KnownTags.COMPONENT_ID, "hidden"); IdStore store = newStore.apply(parent); - store.set(KnownTags.PEER_PORT_ID, 5432); - store.set(KnownTags.DB_INSTANCE_ID, 1.5d); + store.store(KnownTags.PEER_PORT_ID, 5432); + store.store(KnownTags.DB_INSTANCE_ID, 1.5d); store.remove(KnownTags.COMPONENT_ID); - assertEquals("5432", store.getString(KnownTags.PEER_PORT_ID)); - assertEquals("1.5", store.getString(KnownTags.DB_INSTANCE_ID)); - assertEquals("/inherited", store.getString(KnownTags.HTTP_ROUTE_ID)); - assertNull(store.getString(KnownTags.COMPONENT_ID), "hidden by the removal"); - assertNull(store.getString(KnownTags.HTTP_METHOD_ID)); + assertEquals("5432", store.loadString(KnownTags.PEER_PORT_ID)); + assertEquals("1.5", store.loadString(KnownTags.DB_INSTANCE_ID)); + assertEquals("/inherited", store.loadString(KnownTags.HTTP_ROUTE_ID)); + assertNull(store.loadString(KnownTags.COMPONENT_ID), "hidden by the removal"); + assertNull(store.loadString(KnownTags.HTTP_METHOD_ID)); assertThrows( - IllegalArgumentException.class, () -> store.getString(KnownTagCodec.makeTagId(9999))); + IllegalArgumentException.class, () -> store.loadString(KnownTagCodec.makeTagId(9999))); } @ParameterizedTest @MethodSource("stores") void settingANullValueRemovesTheTag(Function newStore) { IdStore parent = newStore.apply(null); - parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.store(KnownTags.HTTP_ROUTE_ID, "/inherited"); IdStore store = newStore.apply(parent); - store.set(KnownTags.HTTP_METHOD_ID, "GET"); + store.store(KnownTags.HTTP_METHOD_ID, "GET"); - store.set(KnownTags.HTTP_METHOD_ID, (Object) null); - store.set(KnownTags.HTTP_ROUTE_ID, (CharSequence) null); // hides the inherited tag - assertNull(store.getValue(KnownTags.HTTP_METHOD_ID)); - assertNull(store.getValue(KnownTags.HTTP_ROUTE_ID)); + store.store(KnownTags.HTTP_METHOD_ID, (Object) null); + store.store(KnownTags.HTTP_ROUTE_ID, (CharSequence) null); // hides the inherited tag + assertNull(store.load(KnownTags.HTTP_METHOD_ID)); + assertNull(store.load(KnownTags.HTTP_ROUTE_ID)); assertEquals(0, store.size()); - assertEquals("/inherited", parent.getValue(KnownTags.HTTP_ROUTE_ID), "the parent keeps it"); + assertEquals("/inherited", parent.load(KnownTags.HTTP_ROUTE_ID), "the parent keeps it"); } @ParameterizedTest @MethodSource("stores") void aReaderWithNoEntryIsANoOp(Function newStore) { IdStore store = newStore.apply(null); - store.set(KnownTags.HTTP_ROUTE_ID, "/a"); + store.store(KnownTags.HTTP_ROUTE_ID, "/a"); NoEntryReader empty = new NoEntryReader("http.route", KnownTags.HTTP_ROUTE_ID); assertNull(store.put(empty)); assertNull(store.getAndPut(empty)); - assertEquals("/a", store.getValue(KnownTags.HTTP_ROUTE_ID), "left as it was"); + assertEquals("/a", store.load(KnownTags.HTTP_ROUTE_ID), "left as it was"); assertEquals(1, store.size()); } @@ -419,9 +419,9 @@ void aReaderWithNoEntryIsANoOp(Function newStore) { @MethodSource("stores") void getEntryReturnsAnEntryTheCallerMayKeep(Function newStore) { IdStore parent = newStore.apply(null); - parent.set(KnownTags.HTTP_ROUTE_ID, "/inherited"); + parent.store(KnownTags.HTTP_ROUTE_ID, "/inherited"); IdStore store = newStore.apply(parent); - store.set(KnownTags.HTTP_METHOD_ID, "GET"); + store.store(KnownTags.HTTP_METHOD_ID, "GET"); TagMap.Entry method = store.getEntry(KnownTags.HTTP_METHOD_ID); TagMap.Entry route = store.getEntry(KnownTags.HTTP_ROUTE_ID); @@ -444,7 +444,7 @@ void getEntryReturnsAnEntryTheCallerMayKeep(Function newStore) void anUnknownIdIsRejected(Function newStore) { IdStore store = newStore.apply(null); long unknown = KnownTagCodec.makeTagId(9999); - assertThrows(IllegalArgumentException.class, () -> store.getValue(unknown)); + assertThrows(IllegalArgumentException.class, () -> store.load(unknown)); assertThrows(IllegalArgumentException.class, () -> store.remove(unknown)); }