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
5 changes: 3 additions & 2 deletions libs/common/include/launchdarkly/context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ class Context final {
* @param ref The reference to the desired attribute.
* @return The attribute Value or a Value representing null.
*/
Value const& Get(std::string const& kind,
launchdarkly::AttributeReference const& ref);
[[nodiscard]] Value const& Get(
std::string const& kind,
launchdarkly::AttributeReference const& ref) const;

/**
* Check if a context is valid.
Expand Down
7 changes: 4 additions & 3 deletions libs/common/include/launchdarkly/detail/c_binding_helpers.hpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#include <launchdarkly/bindings/c/status.h>
#include <cassert>
#include <functional>
#include <launchdarkly/error.hpp>
#include <optional>

#include <tl/expected.hpp>

#include <cassert>
#include <functional>
#include <optional>

namespace launchdarkly {
template <typename T, typename = void>
struct has_result_type : std::false_type {};
Expand Down
4 changes: 2 additions & 2 deletions libs/common/src/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Context::Context(std::map<std::string, launchdarkly::Attributes> attributes)
}

Value const& Context::Get(std::string const& kind,
AttributeReference const& ref) {
AttributeReference const& ref) const {
auto found = attributes_.find(kind);
if (found != attributes_.end()) {
return found->second.Get(ref);
Expand All @@ -64,7 +64,7 @@ std::string Context::make_canonical_key() {
if (kinds_to_keys_.size() == 1) {
if (auto iterator = kinds_to_keys_.find("user");
iterator != kinds_to_keys_.end()) {
return std::string(iterator->second);
return iterator->second;
}
}
std::stringstream stream;
Expand Down
2 changes: 2 additions & 0 deletions libs/server-sdk/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ add_library(${LIBNAME}
evaluation/detail/evaluation_stack.cpp
evaluation/detail/semver_operations.cpp
evaluation/detail/timestamp_operations.cpp
evaluation/evaluation_error.cpp
evaluation/bucketing.cpp
)

if (MSVC OR (NOT BUILD_SHARED_LIBS))
Expand Down
130 changes: 130 additions & 0 deletions libs/server-sdk/src/evaluation/bucketing.cpp
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,
Copy link
Member

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".

Copy link
Contributor Author

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.

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
85 changes: 85 additions & 0 deletions libs/server-sdk/src/evaluation/bucketing.hpp
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
28 changes: 28 additions & 0 deletions libs/server-sdk/src/evaluation/evaluation_error.cpp
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
23 changes: 23 additions & 0 deletions libs/server-sdk/src/evaluation/evaluation_error.hpp
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
Loading