From 140211b500639686b5c0352112abdf58c70d0ef4 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 1 Oct 2026 13:35:03 -0400 Subject: [PATCH] fix: Use double precision and integer weight sums in rollout bucketing The bucket value hash division is now performed in double precision, and rollout bucket boundaries are computed by summing the integer weights and dividing at each comparison, per the evaluation spec. Accumulating a single-precision sum allowed rounding error to shift bucket boundaries, which could break mutual exclusivity of experiments sharing a layer. Fixes #94 (SDK-1537) --- .../launchdarkly/sdk/server/Evaluator.java | 11 +- .../sdk/server/EvaluatorBucketing.java | 6 +- .../EvaluatorBucketingPrecisionTest.java | 121 ++++++++++++++++++ .../sdk/server/EvaluatorBucketingTest.java | 32 ++--- .../sdk/server/EvaluatorRuleTest.java | 2 +- .../sdk/server/EvaluatorSegmentMatchTest.java | 2 +- .../RolloutRandomizationConsistencyTest.java | 12 +- 7 files changed, 155 insertions(+), 31 deletions(-) create mode 100644 lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingPrecisionTest.java diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java index 845d4aaf..be042b5c 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java @@ -333,7 +333,7 @@ private EvalResult getValueForVariationOrRollout( } else { Rollout rollout = vr.getRollout(); if (rollout != null && !rollout.getVariations().isEmpty()) { - float bucket = computeBucketValue( + double bucket = computeBucketValue( rollout.isExperiment(), rollout.getSeed(), context, @@ -343,13 +343,16 @@ private EvalResult getValueForVariationOrRollout( flag.getSalt() ); boolean contextWasFound = bucket >= 0; // see comment on computeBucketValue - float sum = 0F; + // The weights are summed as integers and divided only at each comparison, rather than + // accumulating a floating-point sum, so that rounding errors cannot accumulate and + // shift the bucket boundaries. + long weightSum = 0; List variations = rollout.getVariations(); // guaranteed non-null int nVariations = variations.size(); for (int i = 0; i < nVariations; i++) { WeightedVariation wv = variations.get(i); - sum += (float) wv.getWeight() / 100000F; - if (bucket < sum) { + weightSum += wv.getWeight(); + if (bucket < weightSum / 100000.0) { variation = wv.getVariation(); inExperiment = vr.getRollout().isExperiment() && !wv.isUntracked() && contextWasFound; break; diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java index 5b55e24e..3a47548c 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java @@ -13,7 +13,7 @@ abstract class EvaluatorBucketing { private EvaluatorBucketing() {} - private static final float LONG_SCALE = (float) 0xFFFFFFFFFFFFFFFL; + private static final double LONG_SCALE = (double) 0xFFFFFFFFFFFFFFFL; // Computes a bucket value for a rollout or experiment. If an error condition prevents // us from computing a valid bucket value, we return 0, which will cause the evaluator @@ -21,7 +21,7 @@ private EvaluatorBucketing() {} // found, in which case we return the special value -1; this similarly will cause the // first bucket to be chosen (since it is less than the end value of the bucket, just // as 0 is), but also tells the evaluator that inExperiment must be set to false. - static float computeBucketValue( + static double computeBucketValue( boolean isExperiment, Integer seed, LDContext context, @@ -67,7 +67,7 @@ static float computeBucketValue( } longVal <<= 4; longVal |= ((hash[7] >> 4) & 0xf); - return (float) longVal / LONG_SCALE; + return (double) longVal / LONG_SCALE; } private static boolean getBucketableStringValue(StringBuilder keyBuilder, LDValue userValue) { diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingPrecisionTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingPrecisionTest.java new file mode 100644 index 00000000..abf8d578 --- /dev/null +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingPrecisionTest.java @@ -0,0 +1,121 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Rollout; +import com.launchdarkly.sdk.server.DataModel.RolloutKind; +import com.launchdarkly.sdk.server.DataModel.WeightedVariation; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static com.launchdarkly.sdk.server.EvaluatorBucketing.computeBucketValue; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_EVALUATOR; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; + +/** + * Regression tests for https://github.com/launchdarkly/java-core/issues/94: bucketing must use + * double-precision arithmetic, and rollout bucket boundaries must be derived from integer weight + * sums rather than an accumulated floating-point sum. Otherwise, rounding errors can shift bucket + * boundaries differently for different flags, breaking mutual exclusivity of experiments that + * share a layer. + */ +@SuppressWarnings("javadoc") +public class EvaluatorBucketingPrecisionTest { + private static final Integer SEED = 682385145; + private static final String CONTEXT_KEY = "2937330902736791534808"; + + // Realistic experiment data from the issue report: 551 weighted variations whose weights sum + // to exactly 100000. Each row is {variation, weight, untracked}. For the context key above, + // the bucket value lands just below a bucket boundary, so any drift in the computed boundary + // assigns the wrong variation. + private static final int[][] WEIGHT_DATA = { + {1, 93, 1}, {0, 5, 0}, {1, 281, 1}, {0, 81, 0}, {1, 227, 1}, {0, 100, 0}, {1, 998, 1}, {0, 100, 0}, {1, 90, 1}, {0, 100, 0}, {1, 22, 1}, {0, 100, 0}, + {1, 114, 1}, {1, 23, 0}, {1, 185, 1}, {1, 22, 0}, {0, 100, 0}, {1, 578, 1}, {0, 24, 0}, {1, 540, 1}, {0, 51, 0}, {1, 163, 1}, {1, 100, 0}, {1, 279, 1}, + {1, 2, 0}, {1, 500, 1}, {0, 100, 0}, {1, 498, 1}, {1, 56, 0}, {1, 210, 1}, {1, 4, 0}, {1, 210, 1}, {1, 100, 0}, {1, 735, 1}, {1, 100, 0}, {1, 69, 1}, + {1, 69, 0}, {1, 600, 1}, {1, 100, 0}, {1, 442, 1}, {1, 24, 0}, {0, 24, 0}, {1, 310, 1}, {0, 23, 0}, {1, 216, 1}, {0, 100, 0}, {1, 92, 1}, {1, 56, 0}, + {1, 100, 0}, {1, 181, 1}, {0, 59, 0}, {1, 727, 1}, {0, 100, 0}, {1, 17, 1}, {0, 66, 0}, {1, 394, 1}, {1, 32, 0}, {1, 139, 1}, {0, 92, 0}, {1, 155, 1}, + {0, 56, 0}, {1, 146, 1}, {1, 5, 0}, {1, 150, 1}, {1, 60, 0}, {1, 12, 0}, {1, 151, 1}, {1, 100, 0}, {1, 116, 1}, {1, 100, 0}, {1, 147, 1}, {0, 100, 0}, + {1, 1591, 1}, {0, 68, 0}, {1, 290, 1}, {0, 17, 0}, {1, 163, 1}, {0, 20, 0}, {1, 120, 1}, {1, 39, 0}, {1, 85, 0}, {1, 181, 1}, {0, 100, 0}, {1, 16, 1}, + {0, 78, 0}, {1, 548, 1}, {0, 23, 0}, {1, 314, 1}, {1, 100, 0}, {1, 312, 1}, {1, 40, 0}, {1, 257, 1}, {1, 72, 0}, {1, 561, 1}, {1, 54, 0}, {1, 572, 1}, + {1, 100, 0}, {1, 86, 1}, {1, 59, 0}, {0, 48, 0}, {1, 466, 1}, {1, 91, 0}, {1, 836, 1}, {1, 15, 0}, {1, 206, 1}, {0, 100, 0}, {1, 1058, 1}, {1, 100, 0}, + {1, 395, 1}, {0, 20, 0}, {1, 307, 1}, {0, 26, 0}, {1, 317, 1}, {1, 100, 0}, {1, 185, 1}, {1, 100, 0}, {1, 74, 1}, {1, 100, 0}, {1, 26, 1}, {0, 39, 0}, + {0, 100, 0}, {1, 499, 1}, {1, 16, 0}, {1, 138, 1}, {0, 13, 0}, {1, 774, 1}, {1, 100, 0}, {1, 43, 1}, {1, 4, 0}, {1, 498, 1}, {1, 100, 0}, {1, 155, 1}, + {1, 40, 0}, {1, 73, 0}, {1, 480, 1}, {1, 16, 0}, {1, 304, 1}, {0, 19, 0}, {1, 158, 1}, {0, 100, 0}, {1, 29, 1}, {1, 100, 0}, {1, 125, 1}, {0, 100, 0}, + {1, 194, 1}, {1, 24, 0}, {1, 554, 1}, {0, 36, 0}, {0, 5, 0}, {1, 101, 1}, {1, 13, 0}, {0, 100, 0}, {1, 365, 1}, {0, 100, 0}, {1, 232, 1}, {1, 21, 0}, + {1, 191, 1}, {0, 100, 0}, {1, 328, 1}, {1, 7, 0}, {0, 100, 0}, {1, 175, 1}, {1, 100, 0}, {1, 32, 1}, {1, 100, 0}, {1, 107, 1}, {0, 100, 0}, {1, 212, 1}, + {1, 72, 0}, {1, 295, 1}, {1, 100, 0}, {1, 4, 1}, {1, 100, 0}, {1, 5, 1}, {0, 41, 0}, {1, 403, 1}, {1, 100, 0}, {1, 283, 1}, {1, 51, 0}, {1, 351, 1}, + {0, 100, 0}, {1, 1024, 1}, {1, 100, 0}, {1, 43, 1}, {1, 84, 0}, {0, 22, 0}, {0, 100, 0}, {1, 5, 1}, {1, 83, 0}, {1, 4, 0}, {1, 44, 0}, {1, 534, 1}, + {0, 48, 0}, {1, 222, 1}, {1, 91, 0}, {1, 215, 1}, {1, 18, 0}, {1, 55, 1}, {1, 18, 0}, {1, 100, 0}, {1, 279, 1}, {0, 100, 0}, {1, 382, 1}, {0, 11, 0}, + {1, 535, 1}, {0, 100, 0}, {1, 226, 1}, {0, 100, 0}, {1, 27, 1}, {0, 100, 0}, {1, 291, 1}, {0, 96, 0}, {1, 139, 1}, {0, 69, 0}, {1, 122, 1}, {1, 89, 0}, + {1, 27, 0}, {1, 211, 1}, {0, 85, 0}, {1, 123, 1}, {0, 15, 0}, {1, 280, 1}, {0, 1, 0}, {1, 237, 1}, {0, 73, 0}, {0, 70, 0}, {1, 479, 1}, {1, 100, 0}, + {1, 42, 1}, {1, 65, 0}, {0, 11, 0}, {1, 143, 1}, {0, 34, 0}, {1, 201, 1}, {1, 60, 0}, {1, 922, 1}, {1, 100, 0}, {1, 363, 1}, {1, 80, 0}, {1, 100, 0}, + {1, 499, 1}, {0, 100, 0}, {1, 271, 1}, {0, 62, 0}, {1, 651, 1}, {1, 100, 0}, {1, 581, 1}, {1, 50, 0}, {0, 98, 0}, {1, 536, 1}, {1, 100, 0}, {1, 220, 1}, + {0, 51, 0}, {1, 120, 1}, {1, 100, 0}, {1, 51, 1}, {0, 100, 0}, {1, 208, 1}, {0, 100, 0}, {1, 13, 1}, {1, 8, 0}, {0, 100, 0}, {1, 141, 1}, {0, 100, 0}, + {1, 556, 1}, {1, 25, 0}, {1, 248, 1}, {0, 20, 0}, {1, 346, 1}, {0, 100, 0}, {1, 208, 1}, {0, 100, 0}, {1, 394, 1}, {0, 100, 0}, {1, 254, 1}, {1, 100, 0}, + {1, 260, 1}, {0, 89, 0}, {0, 84, 0}, {1, 861, 1}, {1, 100, 0}, {1, 138, 1}, {1, 100, 0}, {1, 25, 1}, {0, 85, 0}, {1, 1226, 1}, {0, 5, 0}, {1, 816, 1}, + {1, 100, 0}, {1, 224, 1}, {1, 50, 0}, {1, 226, 1}, {0, 100, 0}, {1, 148, 1}, {1, 100, 0}, {1, 100, 1}, {1, 100, 0}, {1, 133, 1}, {0, 100, 0}, {1, 471, 1}, + {1, 100, 0}, {1, 636, 1}, {1, 100, 0}, {1, 48, 1}, {1, 31, 0}, {1, 254, 1}, {0, 11, 0}, {1, 187, 1}, {0, 100, 0}, {1, 7, 1}, {0, 42, 0}, {1, 847, 1}, + {0, 100, 0}, {1, 16, 1}, {1, 100, 0}, {1, 305, 1}, {1, 100, 0}, {1, 888, 1}, {1, 84, 0}, {1, 947, 1}, {1, 8, 0}, {1, 19, 0}, {1, 907, 1}, {0, 100, 0}, + {1, 449, 1}, {1, 38, 0}, {1, 64, 0}, {1, 1125, 1}, {1, 8, 0}, {0, 100, 0}, {1, 895, 1}, {0, 100, 0}, {1, 137, 1}, {1, 100, 0}, {1, 186, 1}, {0, 100, 0}, + {1, 402, 1}, {1, 59, 0}, {1, 5, 0}, {1, 80, 1}, {1, 82, 0}, {1, 480, 1}, {1, 26, 0}, {1, 94, 1}, {0, 100, 0}, {1, 89, 1}, {0, 100, 0}, {1, 387, 1}, + {1, 100, 0}, {1, 271, 1}, {1, 26, 0}, {0, 36, 0}, {1, 833, 1}, {1, 73, 0}, {1, 397, 1}, {1, 100, 0}, {1, 509, 1}, {0, 100, 0}, {1, 183, 1}, {0, 17, 0}, + {1, 126, 1}, {1, 30, 0}, {1, 370, 1}, {1, 20, 0}, {1, 100, 0}, {1, 58, 1}, {0, 18, 0}, {1, 222, 1}, {1, 100, 0}, {1, 238, 1}, {1, 80, 0}, {0, 100, 0}, + {1, 97, 1}, {1, 60, 0}, {1, 386, 1}, {1, 2, 0}, {1, 100, 0}, {1, 433, 1}, {1, 100, 0}, {1, 21, 1}, {0, 42, 0}, {1, 609, 1}, {0, 100, 0}, {1, 52, 1}, + {0, 46, 0}, {1, 103, 1}, {1, 100, 0}, {1, 1566, 1}, {0, 35, 0}, {1, 220, 1}, {1, 40, 0}, {1, 553, 1}, {1, 100, 0}, {1, 39, 1}, {0, 71, 0}, {1, 75, 1}, + {1, 100, 0}, {1, 132, 1}, {0, 100, 0}, {1, 91, 1}, {1, 12, 0}, {0, 100, 0}, {1, 163, 1}, {0, 41, 0}, {1, 289, 1}, {0, 1, 0}, {1, 831, 1}, {1, 6, 0}, + {1, 358, 1}, {0, 100, 0}, {1, 109, 1}, {1, 93, 0}, {0, 85, 0}, {1, 300, 1}, {0, 100, 0}, {1, 14, 1}, {0, 26, 0}, {1, 2320, 1}, {0, 100, 0}, {1, 202, 1}, + {0, 93, 0}, {1, 141, 1}, {1, 39, 0}, {1, 246, 1}, {0, 68, 0}, {1, 381, 1}, {0, 33, 0}, {1, 733, 1}, {1, 60, 0}, {1, 191, 1}, {1, 100, 0}, {1, 240, 1}, + {1, 8, 0}, {1, 597, 1}, {1, 35, 0}, {1, 125, 1}, {1, 71, 0}, {1, 132, 1}, {1, 45, 0}, {1, 366, 1}, {1, 59, 0}, {0, 25, 0}, {1, 163, 1}, {1, 16, 0}, + {1, 273, 1}, {1, 1, 0}, {0, 100, 0}, {1, 57, 1}, {0, 77, 0}, {1, 179, 1}, {1, 100, 0}, {1, 47, 1}, {1, 60, 0}, {1, 950, 1}, {1, 22, 0}, {1, 887, 1}, + {1, 100, 0}, {1, 681, 1}, {1, 31, 0}, {1, 206, 1}, {1, 100, 0}, {1, 301, 1}, {0, 100, 0}, {1, 54, 1}, {1, 100, 0}, {1, 23, 1}, {0, 100, 0}, {1, 549, 1}, + {0, 100, 0}, {1, 100, 0}, {1, 193, 1}, {0, 100, 0}, {1, 63, 1}, {1, 59, 0}, {1, 345, 1}, {0, 100, 0}, {1, 3, 1}, {1, 86, 0}, {1, 2, 0}, {1, 279, 1}, + {1, 100, 0}, {1, 445, 1}, {0, 13, 0}, {0, 100, 0}, {1, 18, 1}, {1, 24, 0}, {1, 35, 0}, {0, 100, 0}, {1, 213, 1}, {0, 100, 0}, {1, 325, 1}, {0, 100, 0}, + {1, 2, 1}, {0, 100, 0}, {1, 842, 1}, {1, 100, 0}, {1, 46, 1}, {0, 100, 0}, {1, 221, 1}, {1, 100, 0}, {1, 74, 1}, {1, 25, 0}, {1, 211, 1}, {0, 29, 0}, + {0, 100, 0}, {1, 13, 1}, {0, 100, 0}, {1, 90, 1}, {1, 10, 0}, {0, 19, 0}, {0, 13, 0}, {1, 132, 1}, {0, 100, 0}, {1, 185, 1}, {1, 32, 0}, {1, 176, 1}, + {0, 100, 0}, {1, 455, 1}, {1, 6, 0}, {0, 11, 0}, {1, 399, 1}, {1, 13, 0}, {1, 315, 1}, {1, 44, 0}, {1, 100, 0}, {1, 425, 1}, {1, 90, 0}, {1, 30, 0}, + {0, 3, 0}, {1, 116, 1}, {1, 67, 0}, {1, 306, 1}, {1, 100, 0}, {1, 53, 1}, {0, 100, 0}, {1, 1183, 1}, {0, 23, 0}, {1, 259, 1}, {0, 100, 0}, {1, 159, 1}, + {0, 27, 0}, {1, 451, 1}, {1, 24, 0}, {1, 87, 0}, {0, 100, 0}, {1, 109, 1}, {1, 100, 0}, {1, 42, 1}, {0, 100, 0}, {1, 78, 1}, {0, 32, 0} + }; + + @Test + public void bucketValueIsComputedInDoublePrecision() { + double bucket = computeBucketValue(true, SEED, LDContext.create(CONTEXT_KEY), null, "flagkey", null, "salt"); + // The single-precision computation this replaced produced 0.98308945 (off by ~4.4e-9), so + // the tolerance here is chosen to fail for anything less precise than a double. + assertEquals(0.9830894514485481, bucket, 1e-10); + } + + @Test + public void rolloutBoundariesAreComputedFromIntegerWeightSums() { + List variations = new ArrayList<>(); + for (int[] row: WEIGHT_DATA) { + variations.add(new WeightedVariation(row[0], row[1], row[2] != 0)); + } + Rollout rollout = new Rollout(ContextKind.DEFAULT, variations, null, RolloutKind.experiment, SEED); + FeatureFlag flag = ModelBuilders.flagBuilder("flagkey") + .on(true) + .variations(true, false) + .fallthrough(rollout) + .salt("salt") + .build(); + + EvalResult result = BASE_EVALUATOR.evaluate(flag, LDContext.create(CONTEXT_KEY), expectNoPrerequisiteEvals()); + + // The context's bucket value is 0.98308945..., and the cumulative weight through the 536th + // weighted variation (variation 1, untracked) is 98309, so the context belongs in that + // bucket and is not in the experiment. Accumulating a floating-point sum of the weights + // instead drifts that boundary below the bucket value, wrongly placing the context in the + // next bucket (variation 0, tracked) and reporting it as in the experiment. + assertThat(result.getVariationIndex(), equalTo(1)); + assertThat(result.getReason().getKind(), equalTo(EvaluationReason.Kind.FALLTHROUGH)); + assertThat(result.getReason().isInExperiment(), equalTo(false)); + } +} diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java index 72e82c4b..2d87d05f 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java @@ -58,8 +58,8 @@ public void usingSeedIsDifferentThanSalt() { String salt = "salt"; Integer seed = 123; - float bucketValue1 = computeBucketValue(false, noSeed, context, null, flagKey, null, salt); - float bucketValue2 = computeBucketValue(true, seed, context, null, flagKey, null, salt); + double bucketValue1 = computeBucketValue(false, noSeed, context, null, flagKey, null, salt); + double bucketValue2 = computeBucketValue(true, seed, context, null, flagKey, null, salt); assert(bucketValue1 != bucketValue2); } @@ -71,8 +71,8 @@ public void differentSeedsProduceDifferentAssignment() { Integer seed1 = 123; Integer seed2 = 456; - float bucketValue1 = computeBucketValue(true, seed1, context, null, flagKey, null, salt); - float bucketValue2 = computeBucketValue(true, seed2, context, null, flagKey, null, salt); + double bucketValue1 = computeBucketValue(true, seed1, context, null, flagKey, null, salt); + double bucketValue2 = computeBucketValue(true, seed2, context, null, flagKey, null, salt); assert(bucketValue1 != bucketValue2); } @@ -85,8 +85,8 @@ public void flagKeyAndSaltDoNotMatterWhenSeedIsUsed() { String salt2 = "salt2"; Integer seed = 123; - float bucketValue1 = computeBucketValue(true, seed, context, null, flagKey1, null, salt1); - float bucketValue2 = computeBucketValue(true, seed, context, null, flagKey2, null, salt2); + double bucketValue1 = computeBucketValue(true, seed, context, null, flagKey1, null, salt1); + double bucketValue2 = computeBucketValue(true, seed, context, null, flagKey2, null, salt2); assert(bucketValue1 == bucketValue2); } @@ -111,9 +111,9 @@ public void canBucketByIntAttributeSameAsString() { .set("stringattr", "33333") .set("intattr", 33333) .build(); - float resultForString = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("stringattr"), "salt"); - float resultForInt = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("intattr"), "salt"); - assertEquals(resultForString, resultForInt, Float.MIN_VALUE); + double resultForString = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("stringattr"), "salt"); + double resultForInt = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("intattr"), "salt"); + assertEquals(resultForString, resultForInt, 0.0); } @Test @@ -121,8 +121,8 @@ public void cannotBucketByFloatAttribute() { LDContext context = LDContext.builder("key") .set("floatattr", 33.5f) .build(); - float result = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("floatattr"), "salt"); - assertEquals(0f, result, Float.MIN_VALUE); + double result = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("floatattr"), "salt"); + assertEquals(0d, result, 0.0); } @Test @@ -130,8 +130,8 @@ public void cannotBucketByBooleanAttribute() { LDContext context = LDContext.builder("key") .set("boolattr", true) .build(); - float result = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("boolattr"), "salt"); - assertEquals(0f, result, Float.MIN_VALUE); + double result = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("boolattr"), "salt"); + assertEquals(0d, result, 0.0); } @Test @@ -139,11 +139,11 @@ public void optimizedHashText() { LDContext context = LDContext.builder("key") .set("stringattr", "33333") .build(); - float result = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("stringattr"), "salt"); + double result = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("stringattr"), "salt"); String hash = DigestUtils.sha1Hex("key.salt.33333").substring(0, 15); long longVal = Long.parseLong(hash, 16); - float expectedResult = longVal / (float) 0xFFFFFFFFFFFFFFFL; - assertEquals(expectedResult, result, Float.MIN_VALUE); + double expectedResult = longVal / (double) 0xFFFFFFFFFFFFFFFL; + assertEquals(expectedResult, result, 0.0); } private static void assertVariationIndexFromRollout( diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorRuleTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorRuleTest.java index 9712174a..593e3e88 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorRuleTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorRuleTest.java @@ -166,7 +166,7 @@ private static void testRolloutBucketing( ) { String flagKey = "feature"; String salt = "abc"; - float expectedBucketValue = computeBucketValue(false, null, LDContext.create(bucketByValue), null, + double expectedBucketValue = computeBucketValue(false, null, LDContext.create(bucketByValue), null, flagKey, null, salt); int bucketValueAsInt = (int)(expectedBucketValue * 100000); Clause clause = clauseMatchingContext(context); diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorSegmentMatchTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorSegmentMatchTest.java index e47448af..b70ca888 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorSegmentMatchTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorSegmentMatchTest.java @@ -179,7 +179,7 @@ public void rolloutUsesBucketBy() { } private void testRolloutBucketing(String bucketByValue, LDContext context, ContextKind contextKind, AttributeRef bucketBy) { - float expectedBucketValue = computeBucketValue(false, null, LDContext.create(bucketByValue), null, + double expectedBucketValue = computeBucketValue(false, null, LDContext.create(bucketByValue), null, SEGMENT_KEY, null, ARBITRARY_SALT); int bucketValueAsInt = (int)(expectedBucketValue * 100000); Clause clause = clauseMatchingContext(context); diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/RolloutRandomizationConsistencyTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/RolloutRandomizationConsistencyTest.java index 3e9c3750..ba389282 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/RolloutRandomizationConsistencyTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/RolloutRandomizationConsistencyTest.java @@ -83,15 +83,15 @@ private static void assertVariationIndexAndExperimentStateForRollout( @Test public void bucketUserByKeyTest() { LDContext user1 = LDContext.create("userKeyA"); - float point1 = computeBucketValue(false, noSeed, user1, null, "hashKey", null, "saltyA"); + double point1 = computeBucketValue(false, noSeed, user1, null, "hashKey", null, "saltyA"); assertEquals(0.42157587, point1, 0.0000001); LDContext user2 = LDContext.create("userKeyB"); - float point2 = computeBucketValue(false, noSeed, user2, null, "hashKey", null, "saltyA"); + double point2 = computeBucketValue(false, noSeed, user2, null, "hashKey", null, "saltyA"); assertEquals(0.6708485, point2, 0.0000001); LDContext user3 = LDContext.create("userKeyC"); - float point3 = computeBucketValue(false, noSeed, user3, null, "hashKey", null, "saltyA"); + double point3 = computeBucketValue(false, noSeed, user3, null, "hashKey", null, "saltyA"); assertEquals(0.10343106, point3, 0.0000001); } @@ -100,15 +100,15 @@ public void bucketUserWithSeedTest() { Integer seed = 61; LDContext user1 = LDContext.create("userKeyA"); - Float point1 = computeBucketValue(true, seed, user1, null, "hashKey", null, "saltyA"); + double point1 = computeBucketValue(true, seed, user1, null, "hashKey", null, "saltyA"); assertEquals(0.09801207, point1, 0.0000001); LDContext user2 = LDContext.create("userKeyB"); - Float point2 = computeBucketValue(true, seed, user2, null, "hashKey", null, "saltyA"); + double point2 = computeBucketValue(true, seed, user2, null, "hashKey", null, "saltyA"); assertEquals(0.14483777, point2, 0.0000001); LDContext user3 = LDContext.create("userKeyC"); - Float point3 = computeBucketValue(true, seed, user3, null, "hashKey", null, "saltyA"); + double point3 = computeBucketValue(true, seed, user3, null, "hashKey", null, "saltyA"); assertEquals(0.9242641, point3, 0.0000001); }