Skip to content

flagd: fractional operator uses wrong bucketing formula — unsigned u32::MAX divisor instead of signed i32::MAX #102

@jonathannorris

Description

@jonathannorris

Summary

The fractional operator's MurmurHash bucketing formula in the Rust in-process provider uses an unsigned division that produces different bucket assignments from every other SDK. Users running the same flag configuration across multiple SDKs will get inconsistent variant assignments for the same targeting key.

Current Behavior

fractional.rs:78-79:

let hash: u32 = murmurhash3_x86_32(bucket_by.as_bytes(), 0);
let bucket = (hash as f64 / u32::MAX as f64) * 100.0;

This divides the raw unsigned 32-bit hash by u32::MAX (4,294,967,295), producing a bucket in [0, 100].

Expected Behavior

All other SDKs cast to signed i32, take the absolute value, and divide by i32::MAX (2,147,483,647):

Go:

hashValue := int32(murmur3.StringSum32(value))
hashRatio := math.Abs(float64(hashValue)) / math.MaxInt32
bucket := hashRatio * 100

Java:

int mmrHash = MurmurHash3.hash32x86(bytes, 0, bytes.length, 0);
float bucket = Math.abs(mmrHash) * 1.0f / Integer.MAX_VALUE * 100;

Python:

hash_ratio = abs(mmh3.hash(bucket_by)) / (2**31 - 1)
bucket = hash_ratio * 100

The Rust implementation should be:

let hash: u32 = murmurhash3_x86_32(bucket_by.as_bytes(), 0);
let signed = hash as i32;
let bucket = (signed as f64).abs() / (i32::MAX as f64) * 100.0;

Impact

For any given (flagKey, targetingKey) pair, the Rust provider will assign a different bucket value than Go/Java/JS/Python/.NET. This means users migrating between SDKs or running multiple SDKs against the same flag configuration will see different variant assignments.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions