Skip to content

Commit

Permalink
Implement Unleash Feature Variants
Browse files Browse the repository at this point in the history
  • Loading branch information
aruizs committed Dec 27, 2023
1 parent 0046a6e commit 4bcb334
Show file tree
Hide file tree
Showing 14 changed files with 229 additions and 51 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The below table shows what features the SDKs support or plan to support.
- [x] Unleash context
- [x] Strategy constrains
- [x] Application registration
- [ ] Variants (WIP)
- [x] Variants
- [ ] Usage Metrics
- [ ] Custom stickiness

Expand Down Expand Up @@ -69,6 +69,29 @@ unleashClient.isEnabled("feature.toogle");
unleashClient.isEnabled("feature.toogle", context);
```

### Getting a Variant

```
#include "unleash/context.h"
...
unleash::Context context{"userId"};
auto variant = unleashClient.variant("feature.toogle", context);
...
/*
The variant response is an instance of the following structure:
{
std::string name;
unsigned int weight;
bool enabled;
bool feature_enabled;
std::string payload;
}
*/
```

For more information about variants, see the [Variant documentation](https://docs.getunleash.io/advanced/toggle_variants).


## Integration

### Building with CMake
Expand Down
6 changes: 6 additions & 0 deletions include/unleash/feature.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef UNLEASH_FEATURE_H
#define UNLEASH_FEATURE_H
#include "unleash/strategies/strategy.h"
#include "unleash/variants/variant.h"
#include <memory>
#include <string>
#include <vector>
Expand All @@ -10,12 +11,17 @@ class Context;
class Feature {
public:
Feature(std::string name, std::vector<std::unique_ptr<Strategy>> strategies, bool enable);
void setVariants(std::pair<std::vector<std::unique_ptr<Variant>>, unsigned int> variants);
bool isEnabled(const Context &context) const;
variant_t getVariant(const Context &context) const;

private:
bool checkVariant(const unleash::Variant &variantInput, variant_t &variantResponse, const Context &context) const;

std::string m_name;
bool m_enabled;
std::vector<std::unique_ptr<Strategy>> m_strategies;
std::pair<std::vector<std::unique_ptr<Variant>>, unsigned int> m_variants;
};
} // namespace unleash
#endif //UNLEASH_FEATURE_H
3 changes: 3 additions & 0 deletions include/unleash/unleashclient.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace unleash {

class UnleashClientBuilder;
class Context;
struct variant_t;

class UNLEASH_EXPORT UnleashClient {
public:
Expand All @@ -27,6 +28,8 @@ class UNLEASH_EXPORT UnleashClient {
void initializeClient();
bool isEnabled(const std::string &flag);
bool isEnabled(const std::string &flag, const Context &context);
variant_t variant(const std::string &flag, const Context &context);


private:
UnleashClient(std::string name, std::string url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
// MurmurHash3 was written by Austin Appleby, and is placed in the public
// domain. The author hereby disclaims copyright to this source code.
//-----------------------------------------------------------------------------
#include <stdint.h>
#include <cstdint>
#include <string>

namespace unleash {

void murmurHash3X8632(const void *key, int len, uint32_t seed, void *out);

uint32_t normalizedMurmur3(const std::string &key, uint32_t seed = 0);
uint32_t normalizedMurmur3(const std::string &key, uint32_t modulus = 100, uint32_t seed = 0);


} // namespace unleash
Expand Down
40 changes: 40 additions & 0 deletions include/unleash/variants/variant.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#ifndef UNLEASH_VARIANTS_H
#define UNLEASH_VARIANTS_H
#include <string>
#include <vector>

namespace unleash {

class Context;

struct Override {
std::string contextName;
std::vector<std::string> values;
};

struct variant_t {
std::string name;
unsigned int weight;
bool enabled;
bool feature_enabled;
std::string payload;
};

class Variant {
public:
Variant(std::string name, unsigned int weight, std::string_view payload, std::string_view overrides);
unsigned int getWeight() const { return m_weight; }
const std::string &getName() const { return m_name; }
const std::vector<Override> &getOverrides() const { return m_overrides; }
const std::string &getPayload() const { return m_payload; }

private:
std::string m_name;
unsigned int m_weight;
std::string m_payload;
std::vector<Override> m_overrides;
};
} // namespace unleash


#endif //UNLEASH_VARIANTS_H
35 changes: 18 additions & 17 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@

add_library(
unleash
unleashclient.cpp
feature.cpp
api/cprclient.cpp
strategies/strategy.cpp
strategies/default.cpp
strategies/userwithid.cpp
strategies/applicationhostname.cpp
strategies/flexiblerollout.cpp
strategies/remoteaddress.cpp
strategies/murmur3hash.cpp
strategies/gradualrolloutuserid.cpp
strategies/gradualrolloutsessionid.cpp
strategies/gradualrolloutrandom.cpp)
unleash
unleashclient.cpp
feature.cpp
api/cprclient.cpp
strategies/strategy.cpp
strategies/default.cpp
strategies/userwithid.cpp
strategies/applicationhostname.cpp
strategies/flexiblerollout.cpp
strategies/remoteaddress.cpp
utils/murmur3hash.cpp
strategies/gradualrolloutuserid.cpp
strategies/gradualrolloutsessionid.cpp
strategies/gradualrolloutrandom.cpp
variants/variant.cpp)

add_library(unleash::unleash ALIAS unleash)
target_include_directories(
unleash PUBLIC "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>")
unleash PUBLIC "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>")
target_link_libraries(unleash PRIVATE cpr::cpr
nlohmann_json::nlohmann_json)
nlohmann_json::nlohmann_json)
set_target_properties(unleash PROPERTIES VERSION ${unleash_VERSION}
SOVERSION ${unleash_VERSION_MAJOR})
SOVERSION ${unleash_VERSION_MAJOR})

target_compile_features(unleash PUBLIC cxx_std_17)

Expand Down
49 changes: 49 additions & 0 deletions src/feature.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#include "unleash/feature.h"
#include "unleash/utils/murmur3hash.h"
#include <algorithm>
#include <numeric>

namespace unleash {
Feature::Feature(std::string name, std::vector<std::unique_ptr<Strategy>> strategies, bool enable)
: m_name(std::move(name)), m_enabled(enable), m_strategies(std::move(strategies)) {}

bool Feature::isEnabled(const Context &context) const {
if (m_enabled) {
if (m_strategies.empty()) return true;
Expand All @@ -12,4 +16,49 @@ bool Feature::isEnabled(const Context &context) const {
}
return false;
}

void Feature::setVariants(std::pair<std::vector<std::unique_ptr<Variant>>, unsigned int> variants) {
m_variants = std::move(variants);
}

variant_t Feature::getVariant(const unleash::Context &context) const {
variant_t variant{"disabled", 0, false, false};
if (!isEnabled(context)) { return variant; }

variant.feature_enabled = true;
if (m_variants.first.empty()) { return variant; }

variant.enabled = true;
constexpr uint32_t seed = 86028157;
auto normalizedValue = normalizedMurmur3(m_name + ":" + context.userId, m_variants.second, seed);
unsigned int weight = 0;
for (auto &eachVariant : m_variants.first) {
if (!eachVariant->getOverrides().empty() && checkVariant(*eachVariant, variant, context)) break;

weight += eachVariant->getWeight();
if (normalizedValue <= weight) {
variant.name = eachVariant->getName();
variant.payload = eachVariant->getPayload();
break;
}
}

return variant;
}

bool Feature::checkVariant(const unleash::Variant &variantInput, variant_t &variantResponse,
const unleash::Context &context) const {
if (auto contextIt = std::find_if(variantInput.getOverrides().begin(), variantInput.getOverrides().end(),
[](const Override &o) { return o.contextName == "userId"; });
contextIt != variantInput.getOverrides().end()) {
auto valuesIt = std::find((*contextIt).values.begin(), (*contextIt).values.end(), context.userId);

if (valuesIt != (*contextIt).values.end()) {
variantResponse.name = variantInput.getName();
variantResponse.payload = variantInput.getPayload();
return true;
}
}
return false;
};
} // namespace unleash
2 changes: 1 addition & 1 deletion src/strategies/flexiblerollout.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#include "unleash/strategies/flexiblerollout.h"
#include "unleash/strategies/murmur3hash.h"
#include "unleash/utils/murmur3hash.h"
#include <nlohmann/json.hpp>
#include <random>

Expand Down
2 changes: 1 addition & 1 deletion src/strategies/gradualrolloutsessionid.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#include "unleash/strategies/gradualrolloutsessionid.h"
#include "unleash/strategies/murmur3hash.h"
#include "unleash/utils/murmur3hash.h"
#include <nlohmann/json.hpp>

namespace unleash {
Expand Down
2 changes: 1 addition & 1 deletion src/strategies/gradualrolloutuserid.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#include "unleash/strategies/gradualrolloutuserid.h"
#include "unleash/strategies/murmur3hash.h"
#include "unleash/utils/murmur3hash.h"
#include <nlohmann/json.hpp>

namespace unleash {
Expand Down
38 changes: 34 additions & 4 deletions src/unleashclient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,50 @@ bool UnleashClient::isEnabled(const std::string &flag, const Context &context) {
return false;
}

variant_t UnleashClient::variant(const std::string &flag, const unleash::Context &context) {
variant_t variant{"disabled", 0, false, false};
if (m_isInitialized) {
variant.feature_enabled = isEnabled(flag, context);
if (auto search = m_features.find(flag); search != m_features.end()) {
std::cout << "variant" << std::endl;
return m_features.at(flag).getVariant(context);
}
}
return variant;
}

UnleashClient::featuresMap_t UnleashClient::loadFeatures(std::string_view features) const {
const auto featuresJson = nlohmann::json::parse(features);
featuresMap_t featuresMap;
for (const auto &[key, value] : featuresJson["features"].items()) {
std::vector<std::unique_ptr<Strategy>> m_strategies;
// Load strategies
std::vector<std::unique_ptr<Strategy>> strategies;
for (const auto &[strategyKey, strategyValue] : value["strategies"].items()) {
std::string strategyParameters;
if (strategyValue.contains("parameters")) strategyParameters = strategyValue["parameters"].dump();
std::string strategyConstraints;
if (strategyValue.contains("constraints")) { strategyConstraints = strategyValue["constraints"].dump(); }
m_strategies.push_back(Strategy::createStrategy(strategyValue["name"].get<std::string>(),
strategyParameters, strategyConstraints));
strategies.push_back(Strategy::createStrategy(strategyValue["name"].get<std::string>(), strategyParameters,
strategyConstraints));
}
Feature newFeature(value["name"], std::move(strategies), value["enabled"]);
// Load variants
std::pair<std::vector<std::unique_ptr<Variant>>, unsigned int> variants;
if (value.contains("variants")) {
unsigned int totalWeight = 0;
for (const auto &[variantKey, variantValue] : value["variants"].items()) {
std::string variantPayload;
if (variantValue.contains("payload")) variantPayload = variantValue["payload"].dump();
std::string variantOverrides;
if (variantValue.contains("overrides")) variantOverrides = variantValue["overrides"].dump();
variants.first.push_back(std::make_unique<Variant>(variantValue["name"], variantValue["weight"],
variantPayload, variantOverrides));
totalWeight += variantValue["weight"].get<unsigned int>();
}
variants.second = totalWeight;
newFeature.setVariants(std::move(variants));
}
featuresMap.try_emplace(value["name"], value["name"], std::move(m_strategies), value["enabled"]);
featuresMap.try_emplace(value["name"], std::move(newFeature));
}
return featuresMap;
}
Expand Down
28 changes: 9 additions & 19 deletions src/strategies/murmur3hash.cpp → src/utils/murmur3hash.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "unleash/strategies/murmur3hash.h"
#include "unleash/utils/murmur3hash.h"
#include <iostream>

//-----------------------------------------------------------------------------
// MurmurHash3 was written by Austin Appleby, and is placed in the public
Expand Down Expand Up @@ -69,22 +70,11 @@ FORCE_INLINE uint32_t fmix32(uint32_t h) {
return h;
}

//----------

FORCE_INLINE uint64_t fmix64(uint64_t k) {
k ^= k >> 33;
k *= BIG_CONSTANT(0xff51afd7ed558ccd);
k ^= k >> 33;
k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53);
k ^= k >> 33;

return k;
}

//-----------------------------------------------------------------------------

void murmurHash3X8632(const void *key, int len, uint32_t seed, void *out) {
const uint8_t *data = (const uint8_t *) key;
const auto *data = (const uint8_t *) key;
const int nblocks = len / 4;

uint32_t h1 = seed;
Expand All @@ -95,7 +85,7 @@ void murmurHash3X8632(const void *key, int len, uint32_t seed, void *out) {
//----------
// body

const uint32_t *blocks = (const uint32_t *) (data + nblocks * 4);
const auto *blocks = (const uint32_t *) (data + nblocks * 4);

for (int i = -nblocks; i; i++) {
uint32_t k1 = getblock32(blocks, i);
Expand All @@ -112,7 +102,7 @@ void murmurHash3X8632(const void *key, int len, uint32_t seed, void *out) {
//----------
// tail

const uint8_t *tail = (const uint8_t *) (data + nblocks * 4);
const auto *tail = data + nblocks * 4;

uint32_t k1 = 0;

Expand All @@ -127,7 +117,7 @@ void murmurHash3X8632(const void *key, int len, uint32_t seed, void *out) {
k1 = ROTL32(k1, 15);
k1 *= c2;
h1 ^= k1;
};
}

//----------
// finalization
Expand All @@ -139,10 +129,10 @@ void murmurHash3X8632(const void *key, int len, uint32_t seed, void *out) {
*(uint32_t *) out = h1;
}

uint32_t normalizedMurmur3(const std::string &key, uint32_t seed) {
uint32_t normalizedMurmur3(const std::string &key, uint32_t modulus, uint32_t seed) {
uint32_t murmur3Hash;
murmurHash3X8632(key.c_str(), key.length(), seed, &murmur3Hash);
murmur3Hash %= 100;
murmurHash3X8632(key.c_str(), static_cast<int>(key.length()), seed, &murmur3Hash);
murmur3Hash %= modulus;
murmur3Hash += 1;
return murmur3Hash;
}
Expand Down
Loading

0 comments on commit 4bcb334

Please sign in to comment.