1818import java .nio .ByteBuffer ;
1919import java .nio .ByteOrder ;
2020import java .nio .charset .StandardCharsets ;
21- import java .util .Arrays ;
2221
2322/**
2423 * Thread-local context for trace/span identification.
2928 */
3029public final class ThreadContext {
3130 private static final int MAX_CUSTOM_SLOTS = 10 ;
31+ // Max UTF-8 byte length for a custom attribute value. Matches the 1-byte length
32+ // field in the OTEP attrs_data entry header; values above this would be truncated
33+ // silently by replaceOtepAttribute, so reject at the entry point instead — keeps
34+ // the attrs_data view and any future diagnostic readers consistent.
35+ private static final int MAX_VALUE_BYTES = 255 ;
3236 private static final int OTEL_MAX_RECORD_SIZE = 640 ;
3337 private static final int SIDECAR_SIZE = MAX_CUSTOM_SLOTS * Integer .BYTES + Long .BYTES ; // 48
34- /**
35- * Total bytes covered by the combined snapshot buffer: OTEP record + tag encodings + LRS.
36- * Used by {@link #snapshot(byte[], int)} / {@link #restore(byte[], int)}.
37- */
38- public static final int SNAPSHOT_SIZE = OTEL_MAX_RECORD_SIZE + SIDECAR_SIZE ; // 688
38+ // Package-private so ScopeStack can size its byte[] scratch.
39+ static final int SNAPSHOT_SIZE = OTEL_MAX_RECORD_SIZE + SIDECAR_SIZE ; // 688
3940 private static final int LRS_OTEP_KEY_INDEX = 0 ;
4041 // LRS is always a fixed 16-hex-char value in attrs_data (zero-padded u64).
4142 // The entry header is 2 bytes (key_index + length), giving 18 bytes total.
@@ -66,18 +67,6 @@ public final class ThreadContext {
6667 private final int [] attrCacheEncodings = new int [CACHE_SIZE ];
6768 private final byte [][] attrCacheBytes = new byte [CACHE_SIZE ][];
6869
69- // Per-slot read-back cache: indexedValueCache[keyIndex] = the String last
70- // successfully written to attrs_data at that slot. Kept in sync by every
71- // write (setContextAttributeDirect) and clear (clearContextAttribute,
72- // setContextDirect). Allows readContextAttribute to return without scanning
73- // attrs_data or allocating on the warm path.
74- // null = not yet scanned; ABSENT = scanned/cleared, known absent; other = cached value.
75- // ABSENT uses new String("") (not "") so it has a unique identity distinct from the
76- // interned empty-string literal. The == check in readContextAttribute relies on that
77- // unique sentinel identity to distinguish "known absent" from actual cached values.
78- private static final String ABSENT = new String ("" );
79- private final String [] indexedValueCache = new String [MAX_CUSTOM_SLOTS ];
80-
8170 // OTEP record field offsets (from packed struct)
8271 private final int validOffset ;
8372 private final int traceIdOffset ;
@@ -198,7 +187,6 @@ public void clearContextAttribute(int keyIndex) {
198187 sidecarBuffer .putInt (keyIndex * Integer .BYTES , 0 );
199188 removeOtepAttribute (otepKeyIndex );
200189 attach ();
201- indexedValueCache [keyIndex ] = ABSENT ;
202190 }
203191
204192 public void copyCustoms (int [] value ) {
@@ -209,8 +197,8 @@ public void copyCustoms(int[] value) {
209197 }
210198
211199 /**
212- * Captures the full record + sidecar state into {@code scratch[offset .. offset+SNAPSHOT_SIZE)}.
213- * Pair with {@link #restore(byte[], int) } for nested-scope propagation.
200+ * Captures the full record + sidecar state into {@code scratch[offset.. offset+SNAPSHOT_SIZE)}.
201+ * Pair with {@link #restore} for nested-scope propagation.
214202 *
215203 * <p>The detach/attach pair is required so the captured {@code valid} byte is always 0. When
216204 * {@link #restore} later memcpys the scratch back, the valid byte is overwritten mid-memcpy;
@@ -229,14 +217,11 @@ public void snapshot(byte[] scratch, int offset) {
229217 * going through {@link #recordBuffer}'s valid flag ({@code ContextApi::get} in native code),
230218 * which is the sole gate for sidecar reads (see {@code thread.h}). The scratch's own valid
231219 * byte is always 0 (enforced in {@link #snapshot}), so the memcpy never transiently republishes.
232- * {@link #indexedValueCache} is invalidated so the next read re-scans the restored attrs_data;
233- * {@code null} denotes "not yet scanned", distinct from {@link #ABSENT}.
234220 */
235221 public void restore (byte [] scratch , int offset ) {
236222 detach ();
237223 combinedBuffer .position (0 );
238224 combinedBuffer .put (scratch , offset , SNAPSHOT_SIZE );
239- Arrays .fill (indexedValueCache , null );
240225 attach ();
241226 }
242227
@@ -255,21 +240,14 @@ public void restore(byte[] scratch, int offset) {
255240 * request IDs, and other per-request-unique strings will exhaust the
256241 * Dictionary and cause attributes to be silently dropped.
257242 *
258- * <p><b>Overflow and orphan encodings:</b> When {@code attrs_data} overflows
259- * (buffer full), the old entry for {@code keyIndex} is compacted out and the
260- * new value cannot be written. Both the sidecar and {@code indexedValueCache}
261- * are cleared so {@link #readContextAttribute} returns {@code null} for this
262- * slot. However, if the value was not already cached in the per-thread encoding
263- * cache, {@code registerConstant0} may have already registered it in the native
264- * Dictionary before the overflow was detected. That Dictionary entry cannot be
265- * removed — the native Dictionary is write-only for the JVM lifetime. The orphan
266- * encoding is harmless: it will not appear in JFR events because the sidecar is
267- * zeroed.
243+ * <p><b>Value size limit.</b> The UTF-8 encoding of {@code value} must fit in
244+ * {@value #MAX_VALUE_BYTES} bytes (the OTEP attrs_data entry length field is one byte).
245+ * Oversized values are rejected up front — they never reach the Dictionary or attrs_data.
268246 *
269247 * @param keyIndex Index into the registered attribute key map (0-based)
270248 * @param value The string value for this attribute
271- * @return true if the attribute was set successfully, false if the
272- * Dictionary is full, attrs_data overflows, or keyIndex is out of range
249+ * @return true if the attribute was set successfully, false if the value is too long,
250+ * the Dictionary is full, attrs_data overflows, or keyIndex is out of range
273251 */
274252 public boolean setContextAttribute (int keyIndex , String value ) {
275253 if (keyIndex < 0 || keyIndex >= MAX_CUSTOM_SLOTS || value == null ) {
@@ -289,19 +267,24 @@ private boolean setContextAttributeDirect(int keyIndex, String value) {
289267 int encoding ;
290268 byte [] utf8 ;
291269 if (value .equals (attrCacheKeys [slot ])) {
270+ // Cache hit — the value was previously validated and cached; no re-check needed.
292271 encoding = attrCacheEncodings [slot ];
293272 utf8 = attrCacheBytes [slot ];
294273 } else {
295- // Cache miss: register in Dictionary, encode UTF-8, cache both.
296- // Allocates byte[] once per unique value; cached for reuse.
274+ // Cache miss: encode UTF-8 and validate size BEFORE touching the Dictionary.
275+ // Rejecting here avoids an orphan Dictionary entry (the native Dictionary is
276+ // write-only for the JVM lifetime and cannot be undone).
277+ utf8 = value .getBytes (StandardCharsets .UTF_8 );
278+ if (utf8 .length > MAX_VALUE_BYTES ) {
279+ return false ;
280+ }
297281 encoding = registerConstant0 (value );
298282 if (encoding < 0 ) {
299283 // Dictionary full: clear sidecar AND remove the OTEP attrs_data entry
300284 // so both views stay consistent (both report no value for this key).
301285 clearContextAttribute (keyIndex );
302286 return false ;
303287 }
304- utf8 = value .getBytes (StandardCharsets .UTF_8 );
305288 attrCacheEncodings [slot ] = encoding ;
306289 attrCacheBytes [slot ] = utf8 ;
307290 attrCacheKeys [slot ] = value ;
@@ -319,7 +302,6 @@ private boolean setContextAttributeDirect(int keyIndex, String value) {
319302 sidecarBuffer .putInt (keyIndex * Integer .BYTES , 0 );
320303 }
321304 attach ();
322- indexedValueCache [keyIndex ] = written ? value : ABSENT ;
323305 return written ;
324306 }
325307
@@ -346,7 +328,6 @@ private void setContextDirect(long localRootSpanId, long spanId, long trHi, long
346328 for (int i = 0 ; i < MAX_CUSTOM_SLOTS ; i ++) {
347329 // i * Integer.BYTES: byte offset into sidecar buffer for int slot i
348330 sidecarBuffer .putInt (i * Integer .BYTES , 0 );
349- indexedValueCache [i ] = ABSENT ;
350331 }
351332 // Reset attrs_data_size to contain only the fixed LRS entry, discarding
352333 // any custom attribute entries written during the previous span.
@@ -382,7 +363,6 @@ private void clearContextDirect() {
382363 writeLrsHex (0 );
383364 for (int i = 0 ; i < MAX_CUSTOM_SLOTS ; i ++) {
384365 sidecarBuffer .putInt (i * Integer .BYTES , 0 );
385- indexedValueCache [i ] = ABSENT ;
386366 }
387367 sidecarBuffer .putLong (lrsSidecarOffset , 0 );
388368 }
@@ -486,19 +466,10 @@ private int compactOtepAttribute(int otepKeyIndex) {
486466 }
487467
488468 /**
489- * Reads a custom attribute value by key index.
490- *
491- * <p>Warm path: O(1), zero-allocation — returns the cached value from
492- * {@code indexedValueCache[keyIndex]} when the slot was populated by a prior write
493- * on this thread. The cache is kept in sync by every write ({@link #setContextAttributeDirect})
494- * and clear ({@link #clearContextAttribute}, {@link #setContextDirect}).
495- *
496- * <p>Cold path: scans {@code attrs_data} on first read after profiler restart
497- * (when the Java cache is unpopulated but the native buffer has pre-existing data).
498- * Populates the cache slot on success to make subsequent reads warm.
499- *
500- * <p>Used by {@code ContextSetter.readContextValue()} to support snapshot/restore of
501- * nested profiling scopes.
469+ * Reads a custom attribute value by key index by scanning {@code attrs_data}.
470+ * Test-only path: in production, the profiler signal handler reads via sidecar
471+ * encoding IDs and the OTEL eBPF profiler reads attrs_data directly — no Java
472+ * reader is on any hot path. Allocates a new String from the UTF-8 bytes on each call.
502473 *
503474 * @param keyIndex 0-based user key index (same as passed to setContextAttribute)
504475 * @return the attribute value string, or null if not set
@@ -507,18 +478,7 @@ public String readContextAttribute(int keyIndex) {
507478 if (keyIndex < 0 || keyIndex >= MAX_CUSTOM_SLOTS ) {
508479 return null ;
509480 }
510- String cached = indexedValueCache [keyIndex ];
511- if (cached == ABSENT ) {
512- return null ;
513- }
514- if (cached != null ) {
515- return cached ;
516- }
517- // Cold path: scan attrs_data (only on first read after session restart).
518- // valid=0 means the record has not been published yet by attach(), or was cleared
519- // by clearContextDirect() without resetting attrs_data_size. Either way there are
520- // no valid user attribute entries — only the LRS prefix may exist. Any future path
521- // that writes user attributes must set valid=1 via attach() first.
481+ // valid=0 → record was detached or never published. No attrs_data to trust.
522482 if (recordBuffer .get (validOffset ) == 0 ) {
523483 return null ;
524484 }
@@ -536,13 +496,10 @@ public String readContextAttribute(int keyIndex) {
536496 for (int i = 0 ; i < len ; i ++) {
537497 bytes [i ] = recordBuffer .get (attrsDataOffset + pos + 2 + i );
538498 }
539- String value = new String (bytes , StandardCharsets .UTF_8 );
540- indexedValueCache [keyIndex ] = value ;
541- return value ;
499+ return new String (bytes , StandardCharsets .UTF_8 );
542500 }
543501 pos += 2 + len ;
544502 }
545- indexedValueCache [keyIndex ] = ABSENT ;
546503 return null ;
547504 }
548505
0 commit comments