-
Notifications
You must be signed in to change notification settings - Fork 3
feat: context bucketing logic #175
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
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
37f704c
feat: add bucketing logic
cwaldren-ld fce9efc
refactor unecessarily visible functions into implementation
cwaldren-ld 8e65ed2
use encoding namespace for brevity
cwaldren-ld 92741cc
fix ostream operator
cwaldren-ld ffad062
Merge branch 'evaluator' into cw/bucketing
cwaldren-ld File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 4 additions & 3 deletions
7
libs/common/include/launchdarkly/detail/c_binding_helpers.hpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| #include "bucketing.hpp" | ||
|
|
||
| #include <launchdarkly/detail/c_binding_helpers.hpp> | ||
| #include <launchdarkly/encoding/base_16.hpp> | ||
| #include <launchdarkly/encoding/sha_1.hpp> | ||
|
|
||
| #include <sstream> | ||
| #include <string_view> | ||
|
|
||
| namespace launchdarkly::server_side::evaluation { | ||
|
|
||
| double const kBucketScale = static_cast<double>(0x0FFFFFFFFFFFFFFF); | ||
|
|
||
| AttributeReference const& Key(); | ||
|
|
||
| std::optional<ContextHashValue> ContextHash(Value const& value, | ||
| BucketPrefix prefix); | ||
|
|
||
| std::optional<std::string> BucketValue(Value const& value); | ||
|
|
||
| bool IsIntegral(double f); | ||
|
|
||
| BucketPrefix::BucketPrefix(Seed seed) : prefix_(seed) {} | ||
|
|
||
| BucketPrefix::BucketPrefix(std::string key, std::string salt) | ||
| : prefix_(KeyAndSalt{key, salt}) {} | ||
|
|
||
| std::ostream& operator<<(std::ostream& os, BucketPrefix const& prefix) { | ||
| std::visit( | ||
| [&](auto&& arg) { | ||
| using T = std::decay_t<decltype(arg)>; | ||
| if constexpr (std::is_same_v<T, BucketPrefix::KeyAndSalt>) { | ||
| os << arg.key << "." << arg.salt; | ||
| } else if constexpr (std::is_same_v<T, BucketPrefix::Seed>) { | ||
| os << arg; | ||
| } | ||
| }, | ||
| prefix.prefix_); | ||
| return os; | ||
| } | ||
|
|
||
| tl::expected<std::pair<ContextHashValue, RolloutKindLookup>, Error> Bucket( | ||
| Context const& context, | ||
| AttributeReference const& by_attr, | ||
| BucketPrefix const& prefix, | ||
| bool is_experiment, | ||
| std::string const& context_kind) { | ||
| AttributeReference const& ref = is_experiment ? Key() : by_attr; | ||
|
|
||
| if (!ref.Valid()) { | ||
| return tl::make_unexpected(Error::kInvalidAttributeReference); | ||
| } | ||
|
|
||
| Value value = context.Get(context_kind, ref); | ||
|
|
||
| bool is_bucketable = value.Type() == Value::Type::kNumber || | ||
| value.Type() == Value::Type::kString; | ||
|
|
||
| if (is_bucketable) { | ||
| return std::make_pair(ContextHash(value, prefix).value_or(0.0), | ||
| RolloutKindLookup::kPresent); | ||
| } | ||
|
|
||
| auto rollout_context_found = | ||
| std::count(context.Kinds().begin(), context.Kinds().end(), | ||
| context_kind) > 0; | ||
|
|
||
| return std::make_pair(0.0, rollout_context_found | ||
| ? RolloutKindLookup::kPresent | ||
| : RolloutKindLookup::kAbsent); | ||
| } | ||
|
|
||
| AttributeReference const& Key() { | ||
| static AttributeReference const key{"key"}; | ||
| LD_ASSERT(key.Valid()); | ||
| return key; | ||
| } | ||
|
|
||
| std::optional<float> ContextHash(Value const& value, BucketPrefix prefix) { | ||
| using namespace launchdarkly::encoding; | ||
|
|
||
| std::optional<std::string> id = BucketValue(value); | ||
| if (!id) { | ||
| return std::nullopt; | ||
| } | ||
|
|
||
| std::stringstream input; | ||
| input << prefix << "." << *id; | ||
|
|
||
| std::array<unsigned char, SHA_DIGEST_LENGTH> const sha1hash = | ||
| Sha1String(input.str()); | ||
|
|
||
| std::string const sha1hash_hexed = Base16Encode(sha1hash); | ||
|
|
||
| std::string const sha1hash_hexed_first_15 = sha1hash_hexed.substr(0, 15); | ||
|
|
||
| try { | ||
| unsigned long long as_number = | ||
| std::stoull(sha1hash_hexed_first_15.data(), nullptr, /* base */ 16); | ||
|
|
||
| double as_double = static_cast<double>(as_number); | ||
| return as_double / kBucketScale; | ||
|
|
||
| } catch (std::invalid_argument) { | ||
| return std::nullopt; | ||
| } catch (std::out_of_range) { | ||
| return std::nullopt; | ||
| } | ||
| } | ||
|
|
||
| std::optional<std::string> BucketValue(Value const& value) { | ||
| switch (value.Type()) { | ||
| case Value::Type::kString: | ||
| return value.AsString(); | ||
| case Value::Type::kNumber: { | ||
| if (IsIntegral(value.AsDouble())) { | ||
| return std::to_string(value.AsInt()); | ||
| } | ||
| return std::nullopt; | ||
| } | ||
| default: | ||
| return std::nullopt; | ||
| } | ||
| } | ||
|
|
||
| bool IsIntegral(double f) { | ||
| return std::trunc(f) == f; | ||
| } | ||
|
|
||
| } // namespace launchdarkly::server_side::evaluation | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| #pragma once | ||
|
|
||
| #include "evaluation_error.hpp" | ||
|
|
||
| #include <launchdarkly/attribute_reference.hpp> | ||
| #include <launchdarkly/context.hpp> | ||
| #include <launchdarkly/data_model/flag.hpp> | ||
|
|
||
| #include <tl/expected.hpp> | ||
|
|
||
| #include <limits> | ||
| #include <optional> | ||
| #include <ostream> | ||
| #include <string> | ||
| #include <variant> | ||
|
|
||
| namespace launchdarkly::server_side::evaluation { | ||
|
|
||
| enum RolloutKindLookup { | ||
| /* The rollout's context kind was found in the supplied evaluation context. | ||
| */ | ||
| kPresent, | ||
| /* The rollout's context kind was not found in the supplied evaluation | ||
| * context. */ | ||
| kAbsent | ||
| }; | ||
|
|
||
| /** | ||
| * Bucketing is performed by hashing an input string. This string | ||
| * may be comprised of a seed (if the flag rule has a seed) or a combined | ||
| * key/salt pair. | ||
| */ | ||
| class BucketPrefix { | ||
| public: | ||
| struct KeyAndSalt { | ||
| std::string key; | ||
| std::string salt; | ||
| }; | ||
|
|
||
| using Seed = std::int64_t; | ||
|
|
||
| /** | ||
| * Constructs a BucketPrefix from a seed value. | ||
| * @param seed Value of the seed. | ||
| */ | ||
| explicit BucketPrefix(Seed seed); | ||
|
|
||
| /** | ||
| * Constructs a BucketPrefix from a key and salt. | ||
| * @param key Key to use. | ||
| * @param salt Salt to use. | ||
| */ | ||
| BucketPrefix(std::string key, std::string salt); | ||
|
|
||
| friend std::ostream& operator<<(std::ostream& os, | ||
| BucketPrefix const& prefix); | ||
|
|
||
| private: | ||
| std::variant<KeyAndSalt, Seed> prefix_; | ||
| }; | ||
|
|
||
| using ContextHashValue = float; | ||
|
|
||
| /** | ||
| * Computes the context hash value for an attribute in the given context | ||
| * identified by the given attribute reference. The hash value is | ||
| * augmented with the supplied bucket prefix. | ||
| * | ||
| * @param context Context to query. | ||
| * @param by_attr Identifier of the attribute to hash. If is_experiment is true, | ||
| * then "key" will be used regardless of by_attr's value. | ||
| * @param prefix Prefix to use when hashing. | ||
| * @param is_experiment Whether this rollout is an experiment. | ||
| * @param context_kind Which kind to inspect in the context. | ||
| * @return A context hash value and indication of whether or not context_kind | ||
| * was found in the context. | ||
| */ | ||
| tl::expected<std::pair<ContextHashValue, RolloutKindLookup>, Error> Bucket( | ||
| Context const& context, | ||
| AttributeReference const& by_attr, | ||
| BucketPrefix const& prefix, | ||
| bool is_experiment, | ||
| std::string const& context_kind); | ||
|
|
||
| } // namespace launchdarkly::server_side::evaluation |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| #include "evaluation_error.hpp" | ||
|
|
||
| namespace launchdarkly::server_side::evaluation { | ||
|
|
||
| std::ostream& operator<<(std::ostream& out, Error const& err) { | ||
| switch (err) { | ||
| case Error::kCyclicReference: | ||
| out << "CYCLIC_REFERENCE"; | ||
| break; | ||
| case Error::kBigSegmentEncountered: | ||
| out << "BIG_SEGMENT_ENCOUNTERED"; | ||
| break; | ||
| case Error::kInvalidAttributeReference: | ||
| out << "INVALID_ATTRIBUTE_REFERENCE"; | ||
| break; | ||
| case Error::kRolloutMissingVariations: | ||
| out << "ROLLOUT_MISSING_VARIATIONS"; | ||
| break; | ||
| case Error::kUnrecognizedOperator: | ||
| out << "UNRECOGNIZED_OPERATOR"; | ||
| break; | ||
| default: | ||
| out << "UNKNOWN_ERROR"; | ||
| break; | ||
| } | ||
| return out; | ||
| } | ||
| } // namespace launchdarkly::server_side::evaluation |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| #pragma once | ||
|
|
||
| #include <ostream> | ||
|
|
||
| namespace launchdarkly::server_side::evaluation { | ||
|
|
||
| enum class Error { | ||
| /* A cyclic reference was detected within flags or segments. */ | ||
| kCyclicReference, | ||
| /* A big segment was encountered; big segments are not supported in this | ||
| * SDK. */ | ||
| kBigSegmentEncountered, | ||
| /* Encountered an invalid attribute reference. */ | ||
| kInvalidAttributeReference, | ||
| /* A rollout was missing variations. */ | ||
| kRolloutMissingVariations, | ||
| /* An operator was supplied that isn't recognized by this SDK. */ | ||
| kUnrecognizedOperator, | ||
| }; | ||
|
|
||
| std::ostream& operator<<(std::ostream& out, Error const& arr); | ||
|
|
||
| } // namespace launchdarkly::server_side::evaluation |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 assume this is handled elsewhere, maybe in parsing, but when bucketBy is omitted entirely it is also "key".
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.
Yep, it's in the parser.