diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ccd002c3dd..d3b2951b242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### API + +* Fix `TraceStateBuilder.remove` corrupting the builder when the same key is removed twice + ([#8613](https://github.com/open-telemetry/opentelemetry-java/pull/8613)) + ## Version 1.64.0 (2026-07-10) ### API diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/ArrayBasedTraceStateBuilder.java b/api/all/src/main/java/io/opentelemetry/api/trace/ArrayBasedTraceStateBuilder.java index 09c3d1e0303..17918342ece 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/ArrayBasedTraceStateBuilder.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/ArrayBasedTraceStateBuilder.java @@ -92,8 +92,10 @@ public TraceStateBuilder remove(String key) { } for (int i = 0; i < reversedEntries.size(); i += 2) { if (reversedEntries.get(i).equals(key)) { - reversedEntries.set(i + 1, null); - numEntries--; + if (reversedEntries.get(i + 1) != null) { + reversedEntries.set(i + 1, null); + numEntries--; + } return this; } } diff --git a/api/all/src/test/java/io/opentelemetry/api/trace/TraceStateTest.java b/api/all/src/test/java/io/opentelemetry/api/trace/TraceStateTest.java index 22a04b4176b..ac95563f5fb 100644 --- a/api/all/src/test/java/io/opentelemetry/api/trace/TraceStateTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/trace/TraceStateTest.java @@ -17,8 +17,10 @@ class TraceStateTest { private static final String FIRST_KEY = "key_1"; private static final String SECOND_KEY = "key_2"; + private static final String THIRD_KEY = "key_3"; private static final String FIRST_VALUE = "value.1"; private static final String SECOND_VALUE = "value.2"; + private static final String THIRD_VALUE = "value.3"; private final TraceState firstTraceState = TraceState.builder().put(FIRST_KEY, FIRST_VALUE).build(); @@ -303,6 +305,32 @@ void removeNotPresent() { .isEqualTo(multiValueTraceState); } + @Test + void removeTwice() { + assertThat(firstTraceState.toBuilder().remove(FIRST_KEY).remove(FIRST_KEY).build()) + .isEqualTo(TraceState.getDefault()); + } + + @Test + void removeTwice_KeepsRemainingEntry() { + assertThat(multiValueTraceState.toBuilder().remove(FIRST_KEY).remove(FIRST_KEY).build().asMap()) + .containsExactly(entry(SECOND_KEY, SECOND_VALUE)); + } + + @Test + void removeTwice_KeepsRemainingEntries() { + assertThat( + TraceState.builder() + .put(FIRST_KEY, FIRST_VALUE) + .put(SECOND_KEY, SECOND_VALUE) + .put(THIRD_KEY, THIRD_VALUE) + .remove(FIRST_KEY) + .remove(FIRST_KEY) + .build() + .asMap()) + .containsExactly(entry(THIRD_KEY, THIRD_VALUE), entry(SECOND_KEY, SECOND_VALUE)); + } + @Test void addAndRemoveEntry() { assertThat(