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..cd0f2f3d5 --- /dev/null +++ b/dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/PolicyStore.java @@ -0,0 +1,47 @@ +/* + * 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.LinkedHashSet; +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 List policies = Collections.emptyList(); + + /** + * 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). + * + * @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"); + LinkedHashSet newPolicySet = new LinkedHashSet<>(newPolicies); + if (new LinkedHashSet<>(policies).equals(newPolicySet)) { + return false; + } + policies = new ArrayList<>(newPolicySet); + 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..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 { @@ -50,4 +58,29 @@ 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. Subclasses must override {@code equals} (and {@code + * hashCode}) for value-based equality; see the class Javadoc. + */ + @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 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 e0f5e9f0f..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 @@ -23,4 +23,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 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 new file mode 100644 index 000000000..949a9d018 --- /dev/null +++ b/dynamic-control/src/test/java/io/opentelemetry/contrib/dynamic/policy/PolicyStoreTest.java @@ -0,0 +1,69 @@ +/* + * 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 updatePoliciesReturnsFalseWhenOnlyOrderDiffers() { + 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)).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 + void getPoliciesReturnsEmptyWhenNeverUpdated() { + assertThat(new PolicyStore().getPolicies()).isEqualTo(Collections.emptyList()); + } +}