From c5e80d0066f4c794da3137d324c2dbf2ad5e1058 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Fri, 14 Aug 2026 12:52:26 -0700 Subject: [PATCH 1/2] Java --- .../EvaluationExposureDeduper.java | 5 ++++ .../integrations/EvaluationExposureKey.java | 29 +++++++------------ .../EvaluationExposureKeySupplier.java | 3 ++ 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureDeduper.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureDeduper.java index 44b0ec50..12d6a2d3 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureDeduper.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureDeduper.java @@ -8,6 +8,11 @@ * Decides whether a hook should be told about an evaluation, so that repeated evaluations resolving * to the same result do not invoke the hook again within a time window. *

+ * This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning. + * It is experimental. Subclassing it to change which evaluations are deduplicated is supported, but the shape + * it is subclassed through, and the components of {@link EvaluationExposureKey} a subclass reasons about, may + * change. + *

* Deduplication is opt-in per hook: a hook is told about every evaluation until you wrap it in a * {@link DedupingHook}, which is what consults a deduper. * diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureKey.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureKey.java index 17fb2e2a..07a49366 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureKey.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureKey.java @@ -8,6 +8,10 @@ * Identifies the evaluation result a hook is about to be told about, so that an * {@link EvaluationExposureDeduper} can recognize a repeat of it. *

+ * This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning. + * It is experimental. Which components make up the identity of an evaluation is the most likely part to be + * revised, so a deduper subclass that reasons about them may need to change with it. + *

* Two evaluations are the same exposure when every component here matches. The value is included * directly rather than inferred from the variation and version: those are the identity LaunchDarkly * uses to bucket summary events, but neither by itself guarantees that the payload is unchanged. @@ -38,7 +42,7 @@ * experiment does not depend on the window; a hook that reads the reason itself is what can miss such * a change until the window elapses. *

- * Instances are immutable, and their hash code is computed once, the first time one is asked for. + * Instances are immutable. */ public final class EvaluationExposureKey { private final String mobileKeyHash; @@ -48,12 +52,6 @@ public final class EvaluationExposureKey { private final int flagVersion; private final String fullyQualifiedContextKey; - // Computed on demand, because the SDK's own deduper recognizes a repeat by the flag a key belongs - // to and the result it describes, and so never hashes a whole key: only a deduper of your own - // that holds keys in a map or a set does. Races are benign, as every thread computes the same - // value from fields that cannot change. - private int hashCode; - /** * Creates a key with a JSON null value. Prefer the overload accepting {@code value}, since the * variation and version do not by themselves distinguish one result from another. @@ -159,17 +157,12 @@ public boolean equals(Object other) { @Override public int hashCode() { - int hash = hashCode; - if (hash == 0) { - hash = Objects.hashCode(mobileKeyHash); - hash = 31 * hash + Objects.hashCode(flagKey); - hash = 31 * hash + Objects.hashCode(value); - hash = 31 * hash + variation; - hash = 31 * hash + flagVersion; - hash = 31 * hash + Objects.hashCode(fullyQualifiedContextKey); - hashCode = hash; - } - return hash; + int hash = Objects.hashCode(mobileKeyHash); + hash = 31 * hash + Objects.hashCode(flagKey); + hash = 31 * hash + Objects.hashCode(value); + hash = 31 * hash + variation; + hash = 31 * hash + flagVersion; + return 31 * hash + Objects.hashCode(fullyQualifiedContextKey); } @Override diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureKeySupplier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureKeySupplier.java index 2969a133..d6781973 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureKeySupplier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureKeySupplier.java @@ -7,6 +7,9 @@ /** * Builds the key identifying the result an evaluation is about to return. *

+ * This interface is not stable, and not subject to any backwards compatibility guarantees or semantic versioning. + * It is experimental. + *

* The SDK gives one of these to each {@link EvaluationSeriesContext} it builds, so that a hook which * needs the identity of an evaluation can ask for it without the SDK building one for hooks that do * not. {@link DedupingHook} is the hook that needs it. From e4be319fb3e4ce00699378ff0357bd14cb64e2e3 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Mon, 17 Aug 2026 12:37:12 -0700 Subject: [PATCH 2/2] refactor: stop caching the tracked flag's hash code Left from when the deduper held exposure keys in an LRU cache. A query TrackedFlag is built fresh per evaluation, so the field was computed eagerly in the constructor and read once, while HashMap already remembers the hash of the entries it stores. Its equals no longer compares hashes as a fast reject. Co-authored-by: Cursor --- .../android/integrations/EvaluationExposureDeduper.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureDeduper.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureDeduper.java index 78afd169..c50ef684 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureDeduper.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationExposureDeduper.java @@ -117,12 +117,10 @@ public synchronized void reset() { private static final class TrackedFlag { private final String mobileKeyHash; private final String flagKey; - private final int hashCode; TrackedFlag(EvaluationExposureKey key) { this.mobileKeyHash = key.getMobileKeyHash(); this.flagKey = key.getFlagKey(); - this.hashCode = 31 * Objects.hashCode(mobileKeyHash) + Objects.hashCode(flagKey); } @Override @@ -135,14 +133,13 @@ public boolean equals(Object other) { } TrackedFlag o = (TrackedFlag) other; - return hashCode == o.hashCode - && Objects.equals(flagKey, o.flagKey) + return Objects.equals(flagKey, o.flagKey) && Objects.equals(mobileKeyHash, o.mobileKeyHash); } @Override public int hashCode() { - return hashCode; + return 31 * Objects.hashCode(mobileKeyHash) + Objects.hashCode(flagKey); } }