-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: report prerequisite relations in AllFlagState #19
Changes from all commits
7c37c81
598adcb
3937e3f
7402975
9f5f76f
f64bfa8
4c300ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -156,6 +156,7 @@ public FeatureFlagsState Build() | |
/// </summary> | ||
/// <param name="valid">true if valid, false if invalid (default is valid)</param> | ||
/// <returns>the same builder</returns> | ||
[Obsolete("Unused, construct a FeatureFlagState with valid/invalid state directly")] | ||
public FeatureFlagsStateBuilder Valid(bool valid) | ||
{ | ||
_valid = valid; | ||
|
@@ -167,8 +168,21 @@ public FeatureFlagsStateBuilder Valid(bool valid) | |
/// </summary> | ||
/// <param name="flagKey">the flag key</param> | ||
/// <param name="result">the evaluation result</param> | ||
/// <returns></returns> | ||
/// <returns>the same builder</returns> | ||
public FeatureFlagsStateBuilder AddFlag(string flagKey, EvaluationDetail<LdValue> result) | ||
{ | ||
return AddFlag(flagKey, result, new List<string>()); | ||
} | ||
|
||
|
||
/// <summary> | ||
/// Adds the result of a flag evaluation, including direct prerequisites. | ||
/// </summary> | ||
/// <param name="flagKey">the flag key</param> | ||
/// <param name="result">the evaluation result</param> | ||
/// <param name="prerequisites">the direct prerequisites evaluated for this flag</param> | ||
/// <returns>the same builder</returns> | ||
public FeatureFlagsStateBuilder AddFlag(string flagKey, EvaluationDetail<LdValue> result, List<string> prerequisites) | ||
{ | ||
return AddFlag(flagKey, | ||
result.Value, | ||
|
@@ -177,13 +191,14 @@ public FeatureFlagsStateBuilder AddFlag(string flagKey, EvaluationDetail<LdValue | |
0, | ||
false, | ||
false, | ||
null); | ||
null, | ||
prerequisites); | ||
} | ||
|
||
// This method is defined with internal scope because metadata fields like trackEvents aren't | ||
// relevant to the main external use case for the builder (testing server-side code) | ||
internal FeatureFlagsStateBuilder AddFlag(string flagKey, LdValue value, int? variationIndex, EvaluationReason reason, | ||
int flagVersion, bool flagTrackEvents, bool trackReason, UnixMillisecondTime? flagDebugEventsUntilDate) | ||
int flagVersion, bool flagTrackEvents, bool trackReason, UnixMillisecondTime? flagDebugEventsUntilDate, List<string> prerequisites) | ||
{ | ||
bool flagIsTracked = flagTrackEvents || flagDebugEventsUntilDate != null; | ||
var flag = new FlagState | ||
|
@@ -194,14 +209,15 @@ internal FeatureFlagsStateBuilder AddFlag(string flagKey, LdValue value, int? va | |
Reason = trackReason || (_withReasons && (!_detailsOnlyIfTracked || flagIsTracked)) ? reason : (EvaluationReason?)null, | ||
DebugEventsUntilDate = flagDebugEventsUntilDate, | ||
TrackEvents = flagTrackEvents, | ||
TrackReason = trackReason | ||
TrackReason = trackReason, | ||
Prerequisites = prerequisites | ||
}; | ||
_flags[flagKey] = flag; | ||
return this; | ||
} | ||
} | ||
|
||
internal struct FlagState | ||
internal struct FlagState : IEquatable<FlagState> | ||
{ | ||
internal LdValue Value { get; set; } | ||
internal int? Variation { get; set; } | ||
|
@@ -211,24 +227,38 @@ internal struct FlagState | |
internal UnixMillisecondTime? DebugEventsUntilDate { get; set; } | ||
internal EvaluationReason? Reason { get; set; } | ||
|
||
public override bool Equals(object other) | ||
internal IReadOnlyList<string> Prerequisites { get; set; } | ||
|
||
|
||
public bool Equals(FlagState o) | ||
{ | ||
if (other is FlagState o) | ||
{ | ||
return Variation == o.Variation && | ||
Version == o.Version && | ||
TrackEvents == o.TrackEvents && | ||
TrackReason == o.TrackReason && | ||
DebugEventsUntilDate.Equals(o.DebugEventsUntilDate) && | ||
Object.Equals(Reason, o.Reason); | ||
} | ||
return false; | ||
return Variation == o.Variation && | ||
Version == o.Version && | ||
TrackEvents == o.TrackEvents && | ||
TrackReason == o.TrackReason && | ||
DebugEventsUntilDate.Equals(o.DebugEventsUntilDate) && | ||
Object.Equals(Reason, o.Reason) && | ||
Prerequisites.SequenceEqual(o.Prerequisites); | ||
} | ||
public override bool Equals(object obj) | ||
{ | ||
return obj is FlagState other && Equals(other); | ||
} | ||
|
||
public static bool operator ==(FlagState lhs, FlagState rhs) | ||
{ | ||
return lhs.Equals(rhs); | ||
} | ||
|
||
public static bool operator !=(FlagState lhs, FlagState rhs) | ||
{ | ||
return !(lhs == rhs); | ||
} | ||
|
||
public override int GetHashCode() | ||
{ | ||
return new HashCodeBuilder().With(Variation).With(Version).With(TrackEvents).With(TrackReason). | ||
With(DebugEventsUntilDate).With(Reason).Value; | ||
return new HashCodeBuilder().With(Variation).With(Version).With(TrackEvents).With(TrackReason) | ||
.With(DebugEventsUntilDate).With(Reason).With(Prerequisites).Value; | ||
} | ||
} | ||
|
||
|
@@ -271,6 +301,14 @@ public override void Write(Utf8JsonWriter w, FeatureFlagsState state, JsonSerial | |
w.WritePropertyName("reason"); | ||
EvaluationReasonConverter.WriteJsonValue(meta.Reason.Value, w); | ||
} | ||
if (meta.Prerequisites.Count > 0) { | ||
w.WriteStartArray("prerequisites"); | ||
foreach (var p in meta.Prerequisites) | ||
{ | ||
w.WriteStringValue(p); | ||
} | ||
w.WriteEndArray(); | ||
} | ||
w.WriteEndObject(); | ||
} | ||
w.WriteEndObject(); | ||
|
@@ -282,6 +320,7 @@ public override FeatureFlagsState Read(ref Utf8JsonReader reader, Type typeToCon | |
{ | ||
var valid = true; | ||
var flags = new Dictionary<string, FlagState>(); | ||
|
||
for (var topLevelObj = RequireObject(ref reader); topLevelObj.Next(ref reader);) | ||
{ | ||
var key = topLevelObj.Name; | ||
|
@@ -295,7 +334,15 @@ public override FeatureFlagsState Read(ref Utf8JsonReader reader, Type typeToCon | |
for (var flagsObj = RequireObject(ref reader); flagsObj.Next(ref reader);) | ||
{ | ||
var subKey = flagsObj.Name; | ||
var flag = flags.ContainsKey(subKey) ? flags[subKey] : new FlagState(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Slightly awkward: have to Another option is to make a null list be a possibility. Logically that'd be fine, but.. I'd rather the consumers never need to question if it's null or not. I could be convinced otherwise. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with the constructor idea or something similar. Faced a similar issue in Java wrt to nullity, but luckily the array constructor with capacity 0 uses a static instance under the hood for performance. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Theoretically we should have struct constructors, but not parameter-less constructors. Ideally we would be creating a fully populated readonly struct instead of poking stuff into it. (containing an immutable list of prerequisites, or null) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the way we are deserializing JSON, we'd need to have some kind of builder pattern (or temp variables) to achieve that (creating the readonly struct in one go.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the temp variables are references, then they aren't really temps, the struct holds a reference. Though it maybe it problematic to adjust. It isn't at an assignment level though. Assign to a shared list and then assign to the populated list. |
||
|
||
var flag = flags.ContainsKey(subKey) | ||
? flags[subKey] | ||
: new FlagState | ||
{ | ||
// Most flags have no prerequisites, don't allocate capacity unless we need to. | ||
Prerequisites = new List<string>(0) | ||
kinyoklion marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
|
||
for (var metaObj = RequireObject(ref reader); metaObj.Next(ref reader);) | ||
{ | ||
switch (metaObj.Name) | ||
|
@@ -318,14 +365,39 @@ public override FeatureFlagsState Read(ref Utf8JsonReader reader, Type typeToCon | |
flag.Reason = reader.TokenType == JsonTokenType.Null ? (EvaluationReason?)null : | ||
EvaluationReasonConverter.ReadJsonValue(ref reader); | ||
break; | ||
case "prerequisites": | ||
// Note: there is an assumption in this code that a given flag key could already | ||
// have been seen before: specifically in the "values" section of the data | ||
// (where it's a simple map of flag key -> evaluated value), but *also* if we | ||
// have duplicate flag keys under the $flagState key. | ||
// | ||
// The first case is expected, but the second is not. LaunchDarkly SaaS / SDKs | ||
// should never generate JSON that has duplicate keys. If this did happen, | ||
// we don't want to 'merge' prerequisites in an arbitrary order: it's important | ||
// that they remain the order they were serialized originally. | ||
// | ||
// Therefore, the behavior here is that the last seen value for a key will 'win' | ||
// and overwrite any previous value. | ||
var prereqList = new List<string>(); | ||
for (var prereqs = RequireArray(ref reader); prereqs.Next(ref reader);) | ||
{ | ||
prereqList.Add(reader.GetString()); | ||
|
||
} | ||
flag.Prerequisites = prereqList; | ||
break; | ||
} | ||
} | ||
flags[subKey] = flag; | ||
} | ||
break; | ||
|
||
default: | ||
var flagForValue = flags.ContainsKey(key) ? flags[key] : new FlagState(); | ||
var flagForValue = flags.ContainsKey(key) ? flags[key] : new FlagState | ||
{ | ||
// Most flags have no prerequisites, don't allocate capacity unless we need to. | ||
Prerequisites = new List<string>(0) | ||
}; | ||
flagForValue.Value = LdValueConverter.ReadJsonValue(ref reader); | ||
flags[key] = flagForValue; | ||
break; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Security.Cryptography; | ||
using LaunchDarkly.Logging; | ||
|
@@ -371,8 +372,7 @@ public FeatureFlagsState AllFlagsState(Context context, params FlagsStateOption[ | |
|
||
var builder = new FeatureFlagsStateBuilder(options); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These weren't used at all in this function. They are used internal to the |
||
var clientSideOnly = FlagsStateOption.HasOption(options, FlagsStateOption.ClientSideOnly); | ||
var withReasons = FlagsStateOption.HasOption(options, FlagsStateOption.WithReasons); | ||
var detailsOnlyIfTracked = FlagsStateOption.HasOption(options, FlagsStateOption.DetailsOnlyForTrackedFlags); | ||
|
||
KeyedItems<ItemDescriptor> flags; | ||
try | ||
{ | ||
|
@@ -397,6 +397,11 @@ public FeatureFlagsState AllFlagsState(Context context, params FlagsStateOption[ | |
{ | ||
EvaluatorTypes.EvalResult result = _evaluator.Evaluate(flag, context); | ||
bool inExperiment = EventFactory.IsExperiment(flag, result.Result.Reason); | ||
|
||
var directPrerequisites = result.PrerequisiteEvals.Where( | ||
e => e.FlagKey == flag.Key) | ||
.Select(p => p.PrerequisiteFlag.Key).ToList(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Converting to a list here, rather than leaving as an |
||
|
||
builder.AddFlag( | ||
flag.Key, | ||
result.Result.Value, | ||
|
@@ -405,16 +410,16 @@ public FeatureFlagsState AllFlagsState(Context context, params FlagsStateOption[ | |
flag.Version, | ||
flag.TrackEvents || inExperiment, | ||
inExperiment, | ||
flag.DebugEventsUntilDate | ||
); | ||
flag.DebugEventsUntilDate, | ||
directPrerequisites); | ||
} | ||
catch (Exception e) | ||
{ | ||
LogHelpers.LogException(_evalLog, | ||
string.Format("Exception caught for feature flag \"{0}\" when evaluating all flags", flag.Key), | ||
e); | ||
EvaluationReason reason = EvaluationReason.ErrorReason(EvaluationErrorKind.Exception); | ||
builder.AddFlag(flag.Key, new EvaluationDetail<LdValue>(LdValue.Null, null, reason)); | ||
builder.AddFlag(flag.Key, new EvaluationDetail<LdValue>(LdValue.Null, null, reason), new List<string>()); | ||
} | ||
} | ||
return builder.Build(); | ||
|
@@ -477,7 +482,7 @@ public FeatureFlagsState AllFlagsState(Context context, params FlagsStateOption[ | |
foreach (var prereqEvent in evalResult.PrerequisiteEvals) | ||
{ | ||
_eventProcessor.RecordEvaluationEvent(eventFactory.NewPrerequisiteEvaluationEvent( | ||
prereqEvent.PrerequisiteFlag, context, prereqEvent.Result, prereqEvent.PrerequisiteOfFlagKey)); | ||
prereqEvent.PrerequisiteFlag, context, prereqEvent.Result, prereqEvent.FlagKey)); | ||
} | ||
} | ||
var evalDetail = evalResult.Result; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-nullable prerequisites feel a bit strange to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair. Is that from a "what a library consumer would expect from a iterate-able thing in C#" kind of way, or a "how structs work" way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, it feels strange in the language to me. We have null-state analysis, so we should be able to safely mark things null-able and use them. https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references#null-state-analysis
From a struct perspective, ideally structs only contain other value types, but that isn't always possible. Next is for structs to only contain immutable reference types in addition to value types. Least favorable is this mix, but also not always avoidable. A struct with mostly not references, with value semantics, is better than a class here probably. But it doesn't behave like a struct. In that if you alias it and mutate the list, then both lists mutate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we make an immutable list, and then assign the entire list, then it at least makes it more toward the normal side. It could also be exposed as an
IReadOnlyList<T>
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is blocking though, as it isn't part of the external API.