From a94b0ef216294c88d7fa81ed2ac36b1bbc42ef50 Mon Sep 17 00:00:00 2001 From: Jack Shirazi Date: Wed, 25 Mar 2026 20:53:22 +0000 Subject: [PATCH 1/4] Add policy store --- .../contrib/dynamic/policy/PolicyStore.java | 39 ++++++++++++ .../dynamic/policy/TelemetryPolicy.java | 24 ++++++++ .../TraceSamplingRatePolicy.java | 18 ++++++ .../dynamic/policy/PolicyStoreTest.java | 60 +++++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java create mode 100644 dynamic-control/src/test/java/io/opentelemetry/contrib/dynamic/policy/PolicyStoreTest.java diff --git a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java new file mode 100644 index 000000000..c8a1d4856 --- /dev/null +++ b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.dynamic.policy; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Holds the latest validated policy snapshot and reports whether an update changed effective + * configuration. + */ +public final class PolicyStore { + + private volatile List policies = Collections.emptyList(); + + /** + * Replaces the stored policies when the new list is not equal to the current snapshot. + * + * @return {@code true} if the store was updated, {@code false} if the list was equal + */ + public synchronized boolean updatePolicies(List newPolicies) { + Objects.requireNonNull(newPolicies, "newPolicies cannot be null"); + List copy = new ArrayList<>(newPolicies); + if (policies.equals(copy)) { + return false; + } + policies = copy; + return true; + } + + public synchronized List getPolicies() { + return Collections.unmodifiableList(new ArrayList<>(policies)); + } +} diff --git a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/TelemetryPolicy.java b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/TelemetryPolicy.java index 220ca2a09..2d6adbc04 100644 --- a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/TelemetryPolicy.java +++ b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/TelemetryPolicy.java @@ -50,4 +50,28 @@ public TelemetryPolicy(String type) { public String getType() { return type; } + + /** + * Type-only policies ({@link TelemetryPolicy} instances) do not equal typed subclasses that share + * the same {@link #getType() type} string. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof TelemetryPolicy)) { + return false; + } + TelemetryPolicy that = (TelemetryPolicy) obj; + if (that.getClass() != TelemetryPolicy.class || getClass() != TelemetryPolicy.class) { + return false; + } + return type.equals(that.type); + } + + @Override + public int hashCode() { + return Objects.hash(type); + } } diff --git a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/tracesampling/TraceSamplingRatePolicy.java b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/tracesampling/TraceSamplingRatePolicy.java index e0f5e9f0f..bb8590977 100644 --- a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/tracesampling/TraceSamplingRatePolicy.java +++ b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/tracesampling/TraceSamplingRatePolicy.java @@ -6,6 +6,7 @@ package io.opentelemetry.contrib.dynamic.policy.tracesampling; import io.opentelemetry.contrib.dynamic.policy.TelemetryPolicy; +import java.util.Objects; public final class TraceSamplingRatePolicy extends TelemetryPolicy { public static final String POLICY_TYPE = "trace-sampling"; @@ -23,4 +24,21 @@ public TraceSamplingRatePolicy(double probability) { public double getProbability() { return probability; } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof TraceSamplingRatePolicy)) { + return false; + } + TraceSamplingRatePolicy that = (TraceSamplingRatePolicy) obj; + return Double.compare(probability, that.probability) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(probability); + } } diff --git a/dynamic-control/src/test/java/io/opentelemetry/contrib/dynamic/policy/PolicyStoreTest.java b/dynamic-control/src/test/java/io/opentelemetry/contrib/dynamic/policy/PolicyStoreTest.java new file mode 100644 index 000000000..774e5d00f --- /dev/null +++ b/dynamic-control/src/test/java/io/opentelemetry/contrib/dynamic/policy/PolicyStoreTest.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.dynamic.policy; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.contrib.dynamic.policy.tracesampling.TraceSamplingRatePolicy; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +class PolicyStoreTest { + + @Test + void updatePoliciesReturnsTrueOnFirstSet() { + PolicyStore store = new PolicyStore(); + List policies = singletonList(new TraceSamplingRatePolicy(0.5)); + + assertThat(store.updatePolicies(policies)).isTrue(); + assertThat(store.getPolicies()).isEqualTo(policies); + } + + @Test + void updatePoliciesReturnsFalseWhenEqualContent() { + PolicyStore store = new PolicyStore(); + assertThat(store.updatePolicies(singletonList(new TraceSamplingRatePolicy(0.5)))).isTrue(); + assertThat(store.updatePolicies(singletonList(new TraceSamplingRatePolicy(0.5)))).isFalse(); + } + + @Test + void updatePoliciesReturnsTrueWhenProbabilityChanges() { + PolicyStore store = new PolicyStore(); + assertThat(store.updatePolicies(singletonList(new TraceSamplingRatePolicy(0.25)))).isTrue(); + assertThat(store.updatePolicies(singletonList(new TraceSamplingRatePolicy(0.75)))).isTrue(); + assertThat(store.getPolicies()).containsExactly(new TraceSamplingRatePolicy(0.75)); + } + + @Test + void updatePoliciesDetectsListOrderChange() { + PolicyStore store = new PolicyStore(); + List first = + Arrays.asList(new TraceSamplingRatePolicy(0.1), new TraceSamplingRatePolicy(0.2)); + List reordered = + Arrays.asList(new TraceSamplingRatePolicy(0.2), new TraceSamplingRatePolicy(0.1)); + + assertThat(store.updatePolicies(first)).isTrue(); + assertThat(store.updatePolicies(reordered)).isTrue(); + assertThat(store.getPolicies()).isEqualTo(reordered); + } + + @Test + void getPoliciesReturnsEmptyWhenNeverUpdated() { + assertThat(new PolicyStore().getPolicies()).isEqualTo(Collections.emptyList()); + } +} From 9f6440c2590ba1c380637a0a886ce52d7b7cffe7 Mon Sep 17 00:00:00 2001 From: Jack Shirazi Date: Wed, 25 Mar 2026 21:23:05 +0000 Subject: [PATCH 2/4] copilot feedback --- .../contrib/dynamic/policy/PolicyStore.java | 20 +++++++++++++------ .../dynamic/policy/TelemetryPolicy.java | 13 ++++++++++-- .../TraceSamplingRatePolicy.java | 3 +-- .../dynamic/policy/PolicyStoreTest.java | 15 +++++++++++--- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java index c8a1d4856..cc5d1cd9f 100644 --- a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java +++ b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; @@ -16,20 +17,27 @@ */ public final class PolicyStore { - private volatile List policies = Collections.emptyList(); + private List policies = Collections.emptyList(); /** - * Replaces the stored policies when the new list is not equal to the current snapshot. + * Replaces the stored policies when the new snapshot is not equal to the current one. * - * @return {@code true} if the store was updated, {@code false} if the list was equal + *

Input lists are normalized to a set of distinct policies ({@link TelemetryPolicy#equals + * value equality}): duplicates are dropped and only the first occurrence of each policy is + * kept (insertion order). Change detection uses set equality, so list order does not matter. + * That matches telemetry policy semantics where the effective result does not depend on + * processing order (see the telemetry policy OTEP, commutativity / no user-defined ordering + * between policies). + * + * @return {@code true} if the store was updated, {@code false} if the snapshot was unchanged */ public synchronized boolean updatePolicies(List newPolicies) { Objects.requireNonNull(newPolicies, "newPolicies cannot be null"); - List copy = new ArrayList<>(newPolicies); - if (policies.equals(copy)) { + List normalized = new ArrayList<>(new LinkedHashSet<>(newPolicies)); + if (new LinkedHashSet<>(policies).equals(new LinkedHashSet<>(normalized))) { return false; } - policies = copy; + policies = normalized; return true; } diff --git a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/TelemetryPolicy.java b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/TelemetryPolicy.java index 2d6adbc04..965a1e90b 100644 --- a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/TelemetryPolicy.java +++ b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/TelemetryPolicy.java @@ -21,6 +21,14 @@ *

Direct instantiation of this base class is intentionally supported for type-only policy * signals (for example, to indicate policy removal/reset without policy-specific values). * + *

Subclasses: {@link #equals(Object)} on this class returns {@code false} when either + * operand is a typed subclass (not {@code TelemetryPolicy} itself), so a type-only instance never + * equals a value-carrying subclass with the same {@link #getType() type}. Each concrete subclass + * must override both {@code equals} and {@code hashCode} consistently with its fields, and + * obey the {@code equals}/{@code hashCode} contract. That is required for consumers such as {@link + * io.opentelemetry.contrib.dynamic.policy.PolicyStore} that deduplicate and detect changes using + * {@link Object#equals(Object)}. + * * @see io.opentelemetry.contrib.dynamic.policy */ public class TelemetryPolicy { @@ -53,7 +61,8 @@ public String getType() { /** * Type-only policies ({@link TelemetryPolicy} instances) do not equal typed subclasses that share - * the same {@link #getType() type} string. + * the same {@link #getType() type} string. Subclasses must override {@code equals} (and {@code + * hashCode}) for value-based equality; see the class Javadoc. */ @Override public boolean equals(Object obj) { @@ -72,6 +81,6 @@ public boolean equals(Object obj) { @Override public int hashCode() { - return Objects.hash(type); + return type.hashCode(); } } diff --git a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/tracesampling/TraceSamplingRatePolicy.java b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/tracesampling/TraceSamplingRatePolicy.java index bb8590977..f4afcc36c 100644 --- a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/tracesampling/TraceSamplingRatePolicy.java +++ b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/tracesampling/TraceSamplingRatePolicy.java @@ -6,7 +6,6 @@ package io.opentelemetry.contrib.dynamic.policy.tracesampling; import io.opentelemetry.contrib.dynamic.policy.TelemetryPolicy; -import java.util.Objects; public final class TraceSamplingRatePolicy extends TelemetryPolicy { public static final String POLICY_TYPE = "trace-sampling"; @@ -39,6 +38,6 @@ public boolean equals(Object obj) { @Override public int hashCode() { - return Objects.hash(probability); + return Double.hashCode(probability); } } diff --git a/dynamic-control/src/test/java/io/opentelemetry/contrib/dynamic/policy/PolicyStoreTest.java b/dynamic-control/src/test/java/io/opentelemetry/contrib/dynamic/policy/PolicyStoreTest.java index 774e5d00f..949a9d018 100644 --- a/dynamic-control/src/test/java/io/opentelemetry/contrib/dynamic/policy/PolicyStoreTest.java +++ b/dynamic-control/src/test/java/io/opentelemetry/contrib/dynamic/policy/PolicyStoreTest.java @@ -41,7 +41,7 @@ void updatePoliciesReturnsTrueWhenProbabilityChanges() { } @Test - void updatePoliciesDetectsListOrderChange() { + void updatePoliciesReturnsFalseWhenOnlyOrderDiffers() { PolicyStore store = new PolicyStore(); List first = Arrays.asList(new TraceSamplingRatePolicy(0.1), new TraceSamplingRatePolicy(0.2)); @@ -49,8 +49,17 @@ void updatePoliciesDetectsListOrderChange() { Arrays.asList(new TraceSamplingRatePolicy(0.2), new TraceSamplingRatePolicy(0.1)); assertThat(store.updatePolicies(first)).isTrue(); - assertThat(store.updatePolicies(reordered)).isTrue(); - assertThat(store.getPolicies()).isEqualTo(reordered); + assertThat(store.updatePolicies(reordered)).isFalse(); + assertThat(store.getPolicies()).isEqualTo(first); + } + + @Test + void updatePoliciesIgnoresDuplicatePoliciesInInput() { + PolicyStore store = new PolicyStore(); + TraceSamplingRatePolicy p = new TraceSamplingRatePolicy(0.5); + assertThat(store.updatePolicies(Arrays.asList(p, new TraceSamplingRatePolicy(0.5)))).isTrue(); + assertThat(store.getPolicies()).containsExactly(p); + assertThat(store.updatePolicies(singletonList(new TraceSamplingRatePolicy(0.5)))).isFalse(); } @Test From eeb4f3bafb79cdc5719e974c374c7723a5f00537 Mon Sep 17 00:00:00 2001 From: Jack Shirazi Date: Wed, 25 Mar 2026 21:26:11 +0000 Subject: [PATCH 3/4] spotless --- .../contrib/dynamic/policy/PolicyStore.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java index cc5d1cd9f..b973a52c1 100644 --- a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java +++ b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java @@ -23,11 +23,11 @@ public final class PolicyStore { * Replaces the stored policies when the new snapshot is not equal to the current one. * *

Input lists are normalized to a set of distinct policies ({@link TelemetryPolicy#equals - * value equality}): duplicates are dropped and only the first occurrence of each policy is - * kept (insertion order). Change detection uses set equality, so list order does not matter. - * That matches telemetry policy semantics where the effective result does not depend on - * processing order (see the telemetry policy OTEP, commutativity / no user-defined ordering - * between policies). + * value equality}): duplicates are dropped and only the first occurrence of each policy is kept + * (insertion order). Change detection uses set equality, so list order does not matter. That + * matches telemetry policy semantics where the effective result does not depend on processing + * order (see the telemetry policy OTEP, commutativity / no user-defined ordering between + * policies). * * @return {@code true} if the store was updated, {@code false} if the snapshot was unchanged */ From 9c4361b4562775d44faae86482450bd530074816 Mon Sep 17 00:00:00 2001 From: jackshirazi Date: Wed, 25 Mar 2026 21:59:27 +0000 Subject: [PATCH 4/4] Update dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../opentelemetry/contrib/dynamic/policy/PolicyStore.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java index b973a52c1..cd0f2f3d5 100644 --- a/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java +++ b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java @@ -33,11 +33,11 @@ public final class PolicyStore { */ public synchronized boolean updatePolicies(List newPolicies) { Objects.requireNonNull(newPolicies, "newPolicies cannot be null"); - List normalized = new ArrayList<>(new LinkedHashSet<>(newPolicies)); - if (new LinkedHashSet<>(policies).equals(new LinkedHashSet<>(normalized))) { + LinkedHashSet newPolicySet = new LinkedHashSet<>(newPolicies); + if (new LinkedHashSet<>(policies).equals(newPolicySet)) { return false; } - policies = normalized; + policies = new ArrayList<>(newPolicySet); return true; }