Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,17 @@ namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluator
/// <inheritdoc/>
internal sealed class FractionalEvaluator : IRule
{
private const int MaxWeight = int.MaxValue; // 2,147,483,647

class FractionalEvaluationDistribution
{
public string variant;
public JsonNode variant;
public int weight;
}

/// <inheritdoc/>
public JsonNode Apply(JsonNode args, EvaluationContext context)
{
// check if we have at least two arguments:
// 1. the property value
// 2. the array containing the buckets

if (args.AsArray().Count == 0)
{
return null;
Expand All @@ -37,7 +35,7 @@ public JsonNode Apply(JsonNode args, EvaluationContext context)
var arg0 = JsonLogic.Apply(args[0], context);

string propertyValue;
if (arg0.GetValueKind() == JsonValueKind.String)
if (arg0?.GetValueKind() == JsonValueKind.String)
{
propertyValue = arg0.ToString();
bucketStartIndex = 1;
Expand All @@ -48,59 +46,106 @@ public JsonNode Apply(JsonNode args, EvaluationContext context)
}

var distributions = new List<FractionalEvaluationDistribution>();
var distributionSum = 0;
long totalWeight = 0;

for (var i = bucketStartIndex; i < args.AsArray().Count; i++)
{
var bucket = JsonLogic.Apply(args[i], context);
var bucketNode = JsonLogic.Apply(args[i], context);

if (!(bucket.GetValueKind() == JsonValueKind.Array))
if (bucketNode == null || bucketNode.GetValueKind() != JsonValueKind.Array)
{
continue;
}

var bucketArr = bucket.AsArray();
var bucketArr = bucketNode.AsArray();

if (!bucketArr.Any())
{
continue;
}

// resolve variant: accept string, number, bool, or null
var variantNode = bucketArr.ElementAt(0);
JsonNode variant;
if (variantNode == null)
{
variant = null;
}
else
{
var kind = variantNode.GetValueKind();
if (kind == JsonValueKind.String
|| kind == JsonValueKind.Number
|| kind == JsonValueKind.True
|| kind == JsonValueKind.False)
{
variant = variantNode;
}
else
{
// unsupported variant type (object, array); skip
continue;
}
}

var weight = 1;

if (bucketArr.Count >= 2 && bucketArr.ElementAt(1).GetValueKind() == JsonValueKind.Number)
if (bucketArr.Count >= 2)
{
weight = bucketArr.ElementAt(1).GetValue<int>();
var weightNode = bucketArr.ElementAt(1);
if (weightNode != null && weightNode.GetValueKind() == JsonValueKind.Number)
{
var weightDouble = weightNode.GetValue<double>();

// weights must be integers within valid range
if (weightDouble != Math.Floor(weightDouble) || weightDouble > MaxWeight)
{
return null;
}

// negative weights can be the result of rollout calculations, so we clamp to 0 rather than returning an error
weight = (int)Math.Max(0, weightDouble);
}
}

distributions.Add(new FractionalEvaluationDistribution
{
variant = bucketArr.ElementAt(0).ToString(),
variant = variant,
weight = weight
});

distributionSum += weight;
totalWeight += weight;
}

// total weight must not exceed MaxInt32
if (totalWeight > MaxWeight || totalWeight == 0)
{
return null;
Comment thread
kylejuliandev marked this conversation as resolved.
}

var valueToDistribute = propertyValue;
var murmur32 = MurmurHash.Create32();
var bytes = Encoding.ASCII.GetBytes(valueToDistribute);
var hashBytes = murmur32.ComputeHash(bytes);
var hash = BitConverter.ToInt32(hashBytes, 0);

var bucketValue = (int)(Math.Abs((float)hash) / Int32.MaxValue * 100);
// treat hash as unsigned 32-bit
var hashUint = BitConverter.ToUInt32(hashBytes, 0);

// high-precision bucketing: map hash to [0, totalWeight)
// (hashUint * totalWeight) >> 32
var bucket = ((ulong)hashUint * (ulong)totalWeight) >> 32;

var rangeEnd = 0.0;
ulong rangeEnd = 0;

foreach (var dist in distributions)
{
rangeEnd += 100 * (dist.weight / (float)distributionSum);
if (bucketValue < rangeEnd)
rangeEnd += (ulong)dist.weight;
if (bucket < rangeEnd)
{
return dist.variant;
}
}

return "";
return null;
}
}
2 changes: 1 addition & 1 deletion src/OpenFeature.Contrib.Providers.Flagd/schemas
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ public void BeforeScenario(ScenarioInfo scenarioInfo, FeatureInfo featureInfo)
var featureTags = featureInfo.Tags;
var tags = new HashSet<string>(scenarioTags.Concat(featureTags));
Skip.If(!tags.Contains("in-process"), "Skipping scenario because it does not have required tag.");
Skip.If(tags.Contains("fractional-v1"), "Skipping legacy fractional bucketing test; v2 algorithm is implemented.");
Skip.If(tags.Contains("operator-errors"), "Skipping operator-errors test; flagd server does not yet fall back to default on operator errors.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ public void BeforeScenario(ScenarioInfo scenarioInfo, FeatureInfo featureInfo)
var featureTags = featureInfo.Tags;
var tags = new HashSet<string>(scenarioTags.Concat(featureTags));
Skip.If(!tags.Contains("rpc"), "Skipping scenario because it does not have required tag.");
Skip.If(tags.Contains("fractional-v1"), "Skipping legacy fractional bucketing test; v2 algorithm is implemented.");
Skip.If(tags.Contains("operator-errors"), "Skipping operator-errors test; flagd server does not yet fall back to default on operator errors.");
}
}
Loading
Loading