diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4044ea3..e005804 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -25,8 +25,8 @@ Download the appropriate binary for your platform from the [releases page](https ```bash # Example: Linux x86_64 -wget https://github.com/Eppo-exp/cpp-sdk/releases/download/v1.0.0/eppoclient-1.0.0-linux-x86_64.tar.gz -tar -xzf eppoclient-1.0.0-linux-x86_64.tar.gz +wget https://github.com/Eppo-exp/cpp-sdk/releases/download/v2.0.0/eppoclient-2.0.0-linux-x86_64.tar.gz +tar -xzf eppoclient-2.0.0-linux-x86_64.tar.gz # Headers are in: include/ # Library is in: lib/libeppoclient.a diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e7a79b..3916645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.0.0] - 2025-12-02 + ### Added +- `EvaluationClient` - lightweight client for flag evaluation without logging - `ConfigurationStore` convenience API for setting configuration by value: - `ConfigurationStore(Configuration config)` - constructor accepting Configuration by value - `setConfiguration(Configuration config)` - setter accepting Configuration by value @@ -25,17 +28,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Collect all parsing errors instead of failing on first error - Enable partial success handling (return value with warnings) - New `ParseResult` template for structured error reporting during parsing +- Support for building without exceptions via `-fno-exceptions` flag +- Abort-free configuration parsing - all parsing operations can now fail gracefully ### Changed - **BREAKING**: `ConfigurationStore::getConfiguration()` now returns `std::shared_ptr` instead of `Configuration` by value - Eliminates expensive configuration copies on every flag evaluation - **BREAKING**: `Configuration` constructors now take parameters by value for better performance +- **BREAKING**: SDK now uses RE2 regex library instead of `std::regex` + - RE2 is not vulnerable to ReDoS (Regular Expression Denial of Service) attacks + - RE2 works without exceptions, enabling exception-free builds + - This change improves security and reliability - `ConfigurationStore` now uses atomic operations instead of mutex internally for better performance - **BREAKING**: Parsing functions now report errors instead of silently skipping invalid entries: - `parseConfigResponse()` and `parseBanditResponse()` return `ParseResult` with error collection - Use the new `parseConfiguration()` convenience function for simplified error handling - Errors are aggregated and returned rather than causing silent data loss +- **BREAKING**: client.getBoolAssignment renamed to client.getBooleanAssignment for consistency with getBooleanAssignmentDetails ### Removed @@ -63,4 +73,5 @@ Initial release of the Eppo C++ SDK. - Example applications for flags and bandits - Comprehensive documentation and README +[2.0.0]: https://github.com/Eppo-exp/cpp-sdk/releases/tag/v2.0.0 [1.0.0]: https://github.com/Eppo-exp/cpp-sdk/releases/tag/v1.0.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 505e3df..b5af2a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,6 +203,9 @@ if(EPPOCLIENT_BUILD_EXAMPLES) add_executable(assignment_details examples/assignment_details.cpp) target_link_libraries(assignment_details PRIVATE eppoclient) + + add_executable(manual_sync examples/manual_sync.cpp) + target_link_libraries(manual_sync PRIVATE eppoclient) endif() # Optional: Build tests diff --git a/Makefile b/Makefile index 76867ec..c756db4 100644 --- a/Makefile +++ b/Makefile @@ -86,6 +86,7 @@ examples: @echo " make run-bandits" @echo " make run-flags" @echo " make run-assignment-details" + @echo " make run-manual-sync" # Run example binaries .PHONY: run-bandits @@ -103,6 +104,11 @@ run-assignment-details: examples @echo "Running assignment_details example..." @cd examples && ../$(BUILD_DIR)/assignment_details +.PHONY: run-manual-sync +run-manual-sync: examples + @echo "Running manual_sync example..." + @cd examples && ../$(BUILD_DIR)/manual_sync + # Clean build artifacts .PHONY: clean clean: @@ -169,6 +175,7 @@ help: @echo " run-bandits - Build and run the bandits example" @echo " run-flag-assignments - Build and run the flag_assignments example" @echo " run-assignment-details - Build and run the assignment_details example" + @echo " run-manual-sync - Build and run the manual_sync example" @echo " format - Format all C++ source files with clang-format" @echo " format-check - Check if files are properly formatted (CI-friendly)" @echo " clean - Remove build artifacts" diff --git a/README.md b/README.md index a06ba03..138ddcb 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ include(FetchContent) FetchContent_Declare( eppoclient GIT_REPOSITORY https://github.com/Eppo-exp/cpp-sdk.git - GIT_TAG v1.0.0 # Use the latest version + GIT_TAG v2.0.0 # Use the latest version ) FetchContent_MakeAvailable(eppoclient) @@ -147,20 +147,38 @@ if (!result.hasValue()) { } // Create and initialize the configuration store -eppoclient::ConfigurationStore configStore; -configStore.setConfiguration(std::move(*result.value)); +auto configStore = std::make_shared(); +configStore->setConfiguration(std::move(*result.value)); -// Create the client (configStore must outlive client) +// Create the client eppoclient::EppoClient client(configStore); ``` +#### Alternative: Initialize ConfigurationStore with Configuration + +You can also initialize the `ConfigurationStore` directly with a `Configuration` object using one of the convenience constructors: + +```cpp +// Option 1: Pass Configuration by value +auto configStore = std::make_shared(std::move(*result.value)); + +// Option 2: Pass Configuration as shared_ptr +auto config = std::make_shared(std::move(*result.value)); +auto configStore = std::make_shared(config); + +// Both options create a ConfigurationStore with the configuration already set +eppoclient::EppoClient client(configStore); +``` + +This is more concise than the two-step approach and is useful when you have your configuration ready at initialization time. + ### 2. Evaluate Feature Flags Once initialized, you can evaluate feature flags for different types: ```cpp // Boolean flag -bool showNewFeature = client.getBoolAssignment( +bool showNewFeature = client.getBooleanAssignment( "new-feature-flag", // flag key "user-123", // subject key attributes, // subject attributes @@ -247,6 +265,8 @@ public: // Create client with loggers auto assignmentLogger = std::make_shared(); auto applicationLogger = std::make_shared(); +auto configStore = std::make_shared(); +// ... (after configStore->setConfiguration()) eppoclient::EppoClient client( configStore, @@ -267,7 +287,6 @@ Here's a complete example showing flag evaluation with logging: int main() { // Initialize configuration - eppoclient::ConfigurationStore configStore; std::string configJson = "..."; // Your JSON config string auto result = eppoclient::parseConfiguration(configJson); if (!result.hasValue()) { @@ -277,7 +296,9 @@ int main() { } return 1; } - configStore.setConfiguration(std::move(*result.value)); + + auto configStore = std::make_shared(); + configStore->setConfiguration(std::move(*result.value)); // Create loggers auto assignmentLogger = std::make_shared(); @@ -298,7 +319,7 @@ int main() { attributes["is_premium"] = true; // Evaluate a feature flag - bool showNewUI = client.getBoolAssignment( + bool showNewUI = client.getBooleanAssignment( "new-ui-rollout", "user-abc-123", attributes, @@ -341,8 +362,8 @@ if (!result.hasValue()) { return 1; } -eppoclient::ConfigurationStore configStore; -configStore.setConfiguration(std::move(*result.value)); +auto configStore = std::make_shared(); +configStore->setConfiguration(std::move(*result.value)); // Create bandit logger to track bandit actions class MyBanditLogger : public eppoclient::BanditLogger { @@ -439,7 +460,6 @@ Here's a complete example from `examples/bandits.cpp` showing bandit-powered car int main() { // Load configuration - eppoclient::ConfigurationStore configStore; std::string flagConfigJson = "..."; // Your flag config JSON std::string banditModelsJson = "..."; // Your bandit models JSON auto result = eppoclient::parseConfiguration(flagConfigJson, banditModelsJson); @@ -450,7 +470,9 @@ int main() { } return 1; } - configStore.setConfiguration(std::move(*result.value)); + + auto configStore = std::make_shared(); + configStore->setConfiguration(std::move(*result.value)); // Create loggers auto assignmentLogger = std::make_shared(); @@ -509,6 +531,9 @@ See the **Getting Detailed Error Information** section below for more refined er ### Error Handling Behavior ```cpp +auto configStore = std::make_shared(); +// ... (after configStore->setConfiguration()) + eppoclient::EppoClient client( configStore, assignmentLogger, @@ -520,7 +545,7 @@ eppoclient::Attributes attributes; // If the flag doesn't exist, returns the default value (false) // and logs an info message through applicationLogger -bool result = client.getBoolAssignment( +bool result = client.getBooleanAssignment( "non-existent-flag", "user-123", attributes, @@ -530,7 +555,7 @@ bool result = client.getBoolAssignment( // If parameters are invalid (e.g., empty subject key), // returns the default value and logs an error -bool result2 = client.getBoolAssignment( +bool result2 = client.getBooleanAssignment( "my-flag", "", // Empty subject key attributes, @@ -566,12 +591,14 @@ public: }; auto logger = std::make_shared(); +auto configStore = std::make_shared(); +// ... (after configStore->setConfiguration()) eppoclient::EppoClient client(configStore, nullptr, nullptr, logger); ``` ### Getting Detailed Error Information -For more granular error handling, use the `*Details()` variants of assignment functions (such as `getBoolAssignmentDetails()`, `getStringAssignmentDetails()`, etc.). These functions return evaluation details that include: +For more granular error handling, use the `*Details()` variants of assignment functions (such as `getBooleanAssignmentDetails()`, `getStringAssignmentDetails()`, etc.). These functions return evaluation details that include: 1. **Flag evaluation code**: Indicates why a particular assignment was made or what error occurred 2. **Flag evaluation details**: Contains specific error messages when errors are encountered @@ -580,7 +607,7 @@ For more granular error handling, use the `*Details()` variants of assignment fu eppoclient::Attributes attributes; // Use the *Details function to get evaluation information -auto result = client.getBoolAssignmentDetails( +auto result = client.getBooleanAssignmentDetails( "my-flag", "user-123", attributes, @@ -649,7 +676,6 @@ Always ensure these preconditions are met to avoid assertion failures. int main() { // Initialize client with application logger - eppoclient::ConfigurationStore configStore; std::string configJson = "..."; // Your JSON config string auto result = eppoclient::parseConfiguration(configJson); if (!result.hasValue()) { @@ -659,7 +685,9 @@ int main() { } return 1; } - configStore.setConfiguration(std::move(*result.value)); + + auto configStore = std::make_shared(); + configStore->setConfiguration(std::move(*result.value)); auto applicationLogger = std::make_shared(); eppoclient::EppoClient client( @@ -673,7 +701,7 @@ int main() { attributes["company_id"] = std::string("42"); // SDK handles errors gracefully - no exceptions thrown - bool isEnabled = client.getBoolAssignment( + bool isEnabled = client.getBooleanAssignment( "new-checkout-flow", "user-123", attributes, @@ -730,107 +758,157 @@ if (result.evaluationDetails.has_value()) { ``` All assignment methods have corresponding `*Details()` variants: -- `getBoolAssignmentDetails()` +- `getBooleanAssignmentDetails()` - `getStringAssignmentDetails()` - `getNumericAssignmentDetails()` - `getIntegerAssignmentDetails()` -- `getJSONAssignmentDetails()` -- `getSerializedJSONAssignmentDetails()` +- `getJsonAssignmentDetails()` +- `getSerializedJsonAssignmentDetails()` - `getBanditActionDetails()` For more information on debugging flag assignments and using evaluation details, see the [Eppo SDK debugging documentation](https://docs.geteppo.com/sdks/sdk-features/debugging-flag-assignment#allocation-evaluation-scenarios). You can find working examples in [examples/assignment_details.cpp](https://github.com/Eppo-exp/cpp-sdk/blob/main/examples/assignment_details.cpp). -## Thread Safety and Concurrency +## EvaluationClient vs EppoClient + +The SDK provides two client classes for different use cases: + +### EppoClient (Recommended for Most Users) + +`EppoClient` is the high-level client that manages configuration storage and provides optional logging: + +```cpp +auto configStore = std::make_shared(); +configStore->setConfiguration(config); + +// Loggers are optional (can be nullptr) +eppoclient::EppoClient client( + configStore, + assignmentLogger, // optional + banditLogger, // optional + applicationLogger // optional +); + +bool result = client.getBooleanAssignment("flag-key", "user-123", attrs, false); +``` + +**Benefits:** +- Works with `ConfigurationStore` for easy configuration updates +- Optional loggers (can pass `nullptr`) +- Simpler API for most use cases + +### EvaluationClient (Advanced Use Cases) + +`EvaluationClient` is the low-level evaluation engine designed to **separate evaluation logic from state management**. Use this approach for more manual control over synchronization. + +```cpp +const Configuration& config = ...; // Must outlive EvaluationClient +MyAssignmentLogger assignmentLogger; +MyBanditLogger banditLogger; +MyApplicationLogger applicationLogger; + +eppoclient::EvaluationClient evaluationClient( + config, + assignmentLogger, // required reference + banditLogger, // required reference + applicationLogger // required reference +); + +bool result = evaluationClient.getBooleanAssignment("flag-key", "user-123", attrs, false); +``` -The Eppo C++ SDK is **not thread-safe by design**. If you need to use the SDK from multiple threads, you are responsible for providing appropriate synchronization mechanisms in your application. +**Design Philosophy:** -### Key Points +`EvaluationClient` was introduced to provide maximum flexibility and performance: -- `ConfigurationStore::setConfiguration()` is not thread-safe - updating configuration while reading requires external synchronization -- `ConfigurationStore::getConfiguration()` returns a `std::shared_ptr` with thread-safe reference counting -- Retrieved configurations are immutable (`const`) and safe to use concurrently once obtained -- `EppoClient` is not thread-safe - concurrent flag evaluations require external synchronization -- The caller is responsible for implementing any required synchronization when updating configuration +- **Zero synchronization overhead**: Takes configuration and loggers by reference with no shared pointers or mutex locking +- **Cheap construction**: Extremely lightweight to create and destroy instances +- **Flexible synchronization strategies**: Instead of forcing a one-size-fits-all locking approach (like protecting the entire client with a mutex), you can implement your own synchronization strategy around `ConfigurationStore` +- **Parallel evaluation**: Enables efficient concurrent evaluations—you can guard only the cheap `shared_ptr` copying operation when retrieving configuration, then evaluate in parallel +- **Custom configuration management**: Allows building your own configuration management system without being constrained by `ConfigurationStore`'s internal implementation -### Single-Threaded Usage (No Synchronization Required) +**When to use EvaluationClient:** +- You need maximum performance with custom synchronization strategies +- You want to evaluate flags in parallel across multiple threads with minimal locking +- You want direct control over the `Configuration` object lifetime -If your application evaluates flags from a single thread, no special synchronization is needed: +**Important notes:** +- All parameters (configuration and loggers) are passed by reference and must outlive the `EvaluationClient` instance +- All loggers are required (not optional) +- You're responsible for managing the `Configuration` lifetime and any necessary synchronization + +**For most applications, use `EppoClient`**. Only use `EvaluationClient` if you need the advanced control and performance characteristics it provides. + +For a complete working example of using `EvaluationClient` with manual synchronization, see [examples/manual_sync.cpp](https://github.com/Eppo-exp/cpp-sdk/blob/main/examples/manual_sync.cpp). ```cpp -eppoclient::ConfigurationStore configStore; -configStore.setConfiguration(eppoclient::Configuration(config)); +auto configStore = std::make_shared(); +configStore->setConfiguration(std::move(*result.value)); + +// Create thread-safe loggers +auto assignmentLogger = std::make_shared(); eppoclient::EppoClient client(configStore, assignmentLogger); -// All flag evaluations happen on the same thread - safe -bool feature1 = client.getBoolAssignment("flag1", "user-123", attrs, false); -bool feature2 = client.getBoolAssignment("flag2", "user-123", attrs, false); +// ✅ Safe to call from multiple threads without any additional synchronization +// Thread 1: +bool feature1 = client.getBooleanAssignment("flag1", "user-123", attrs, false); + +// Thread 2: +bool feature2 = client.getBooleanAssignment("flag2", "user-456", attrs, false); + +// Thread 3: +std::string variant = client.getStringAssignment("flag3", "user-789", attrs, "default"); ``` -### Multi-Threaded Usage (Synchronization Required) +### Updating Configuration -If you need to evaluate flags from multiple threads or update configuration while reading, you must provide synchronization: +Configuration updates are also thread-safe and can happen concurrently with flag evaluations: ```cpp -#include -#include +// Thread 1: Evaluating flags +bool result = client.getBooleanAssignment("flag", "user", attrs, false); -// Wrap the configuration store and client with a mutex -class ThreadSafeEppoClient { -private: - eppoclient::ConfigurationStore configStore_; - std::unique_ptr client_; - mutable std::mutex mutex_; +// Thread 2: Updating configuration (safe!) +eppoclient::Configuration newConfig = ...; +configStore->setConfiguration(newConfig); -public: - ThreadSafeEppoClient( - std::shared_ptr assignmentLogger = nullptr, - std::shared_ptr banditLogger = nullptr, - std::shared_ptr applicationLogger = nullptr - ) : client_(std::make_unique( - configStore_, assignmentLogger, banditLogger, applicationLogger)) {} - - // Thread-safe configuration update - void updateConfiguration(const eppoclient::Configuration& config) { - std::lock_guard lock(mutex_); - configStore_.setConfiguration(config); - } +// Subsequent evaluations on Thread 1 will use the new configuration +``` - // Thread-safe flag evaluation - bool getBoolAssignment( - const std::string& flagKey, - const std::string& subjectKey, - const eppoclient::Attributes& attributes, - bool defaultValue - ) { - std::lock_guard lock(mutex_); - return client_->getBoolAssignment(flagKey, subjectKey, attributes, defaultValue); - } +### Advanced: EvaluationClient for Maximum Performance - // Add other methods as needed... -}; +For advanced use cases requiring maximum performance, you can use `EvaluationClient` directly with custom synchronization strategies. This approach avoids creating temporary objects on each evaluation: -// Usage: -ThreadSafeEppoClient client(assignmentLogger); +```cpp +// Get configuration once (thread-safe) +auto config = configStore->getConfiguration(); -// Can now safely call from multiple threads -bool result1 = client.getBoolAssignment("flag1", "user-123", attrs, false); -bool result2 = client.getBoolAssignment("flag2", "user-456", attrs, false); +// Create long-lived EvaluationClient (cheap, no locking) +eppoclient::EvaluationClient evaluationClient(*config, assignmentLogger, + banditLogger, applicationLogger); + +// Evaluate many flags without any locking overhead +bool result1 = evaluationClient.getBooleanAssignment("flag1", "user", attrs, false); +bool result2 = evaluationClient.getBooleanAssignment("flag2", "user", attrs, false); +// ... thousands more evaluations ... ``` +For a complete example of this advanced pattern, see [examples/manual_sync.cpp](https://github.com/Eppo-exp/cpp-sdk/blob/main/examples/manual_sync.cpp). + ### Design Philosophy -This design choice allows: -- **Maximum flexibility**: Applications can choose their preferred synchronization strategy (mutexes, read-write locks, lock-free structures, etc.) -- **Zero overhead for single-threaded applications**: No unnecessary locking when concurrency isn't needed -- **Better integration**: The SDK adapts to your application's existing concurrency model rather than imposing its own +The SDK's thread-safety design provides: +- **Zero synchronization overhead** - No mutexes during flag evaluation +- **Immutable configurations** - Safe concurrent access without locking +- **Atomic configuration updates** - Updates don't block ongoing evaluations +- **Simple API** - No need for wrapper classes or manual locking in most cases ### Important Notes - `ConfigurationStore` must outlive any `EppoClient` instances that reference it -- Configuration objects retrieved via `getConfiguration()` use `std::shared_ptr` and remain valid even if the store is updated -- Only `setConfiguration()` needs synchronization when called concurrently with flag evaluations -- Logger interfaces (`AssignmentLogger`, `BanditLogger`, `ApplicationLogger`) should be thread-safe if used concurrently +- Configuration objects retrieved via `getConfiguration()` remain valid even if the store is updated +- Logger interfaces must be thread-safe if used from multiple threads (use mutexes in logger implementations if needed) +- `EvaluationClient` instances are lightweight and cheap to create per-evaluation if needed ## Additional Resources @@ -838,4 +916,5 @@ This design choice allows: - See [examples/flag_assignments.cpp](https://github.com/Eppo-exp/cpp-sdk/blob/main/examples/flag_assignments.cpp) for feature flag examples - See [examples/bandits.cpp](https://github.com/Eppo-exp/cpp-sdk/blob/main/examples/bandits.cpp) for contextual bandit examples - See [examples/assignment_details.cpp](https://github.com/Eppo-exp/cpp-sdk/blob/main/examples/assignment_details.cpp) for evaluation details examples +- See [examples/manual_sync.cpp](https://github.com/Eppo-exp/cpp-sdk/blob/main/examples/manual_sync.cpp) for advanced usage with EvaluationClient and manual synchronization diff --git a/RELEASING.md b/RELEASING.md index 8429b7b..bfe149a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -16,8 +16,7 @@ Before creating a release, ensure: - [ ] All tests pass (`make test`) - [ ] Examples compile and run correctly - [ ] Documentation is up to date -- [ ] CHANGELOG has been updated (if maintained) -- [ ] Version number follows semantic versioning +- [ ] CHANGELOG has been updated - [ ] Version is updated in `src/version.hpp` ## Creating a Release diff --git a/examples/flag_assignments.cpp b/examples/flag_assignments.cpp index 133a6a6..85b40ba 100644 --- a/examples/flag_assignments.cpp +++ b/examples/flag_assignments.cpp @@ -126,7 +126,7 @@ int main() { eppoclient::Attributes attributes1; attributes1["company_id"] = "42"; bool result1 = - client.getBoolAssignment("boolean-false-assignment", "my-subject", attributes1, false); + client.getBooleanAssignment("boolean-false-assignment", "my-subject", attributes1, false); if (result1) { std::cout << "Hello Universe!" << std::endl; } else { @@ -138,7 +138,7 @@ int main() { eppoclient::Attributes attributes2; attributes2["should_disable_feature"] = false; bool result2 = - client.getBoolAssignment("boolean-false-assignment", "my-subject", attributes2, false); + client.getBooleanAssignment("boolean-false-assignment", "my-subject", attributes2, false); if (result2) { std::cout << "Hello Universe!" << std::endl; } else { @@ -150,7 +150,7 @@ int main() { eppoclient::Attributes attributes3; attributes3["should_disable_feature"] = true; bool result3 = - client.getBoolAssignment("boolean-false-assignment", "my-subject", attributes3, false); + client.getBooleanAssignment("boolean-false-assignment", "my-subject", attributes3, false); if (result3) { std::cout << "Hello Universe!" << std::endl; } else { diff --git a/examples/manual_sync.cpp b/examples/manual_sync.cpp new file mode 100644 index 0000000..2985ac8 --- /dev/null +++ b/examples/manual_sync.cpp @@ -0,0 +1,296 @@ +#include +#include +#include +#include +#include +#include "../src/client.hpp" +#include "../src/evaluation_client.hpp" + +/** + * Example: Using EvaluationClient with Manual Synchronization + * + * This example demonstrates how to use EvaluationClient for advanced use cases where + * you need full control over configuration management and synchronization strategy. + * + * Key concepts demonstrated: + * 1. Using EvaluationClient instead of EppoClient for maximum control + * 2. Implementing custom synchronization around ConfigurationStore + * 3. Separating configuration retrieval from evaluation for better parallelism + * 4. Managing Configuration lifetime explicitly + * + * When to use EvaluationClient: + * - You need maximum performance with custom synchronization strategies + * - You're building a custom configuration management system + * - You want to evaluate flags in parallel with minimal locking + * - You need direct control over Configuration object lifetime + * + * For most applications, use EppoClient instead - it provides a simpler API + * with built-in configuration management and optional loggers. + */ + +// Simple console-based assignment logger +class ConsoleAssignmentLogger : public eppoclient::AssignmentLogger { +public: + void logAssignment(const eppoclient::AssignmentEvent& event) override { + std::cout << "\n=== Assignment Log ===" << std::endl; + std::cout << "Feature Flag: " << event.featureFlag << std::endl; + std::cout << "Variation: " << event.variation << std::endl; + std::cout << "Subject: " << event.subject << std::endl; + std::cout << "Timestamp: " << event.timestamp << std::endl; + std::cout << "=====================\n" << std::endl; + } +}; + +// Simple console-based bandit logger +class ConsoleBanditLogger : public eppoclient::BanditLogger { +public: + void logBanditAction(const eppoclient::BanditEvent& event) override { + std::cout << "\n=== Bandit Action Log ===" << std::endl; + std::cout << "Flag Key: " << event.flagKey << std::endl; + std::cout << "Action: " << event.action << std::endl; + std::cout << "Action Probability: " << event.actionProbability << std::endl; + std::cout << "Subject: " << event.subject << std::endl; + std::cout << "========================\n" << std::endl; + } +}; + +// Simple console-based application logger +class ConsoleApplicationLogger : public eppoclient::ApplicationLogger { +public: + void debug(const std::string& message) override { + std::cout << "[DEBUG] " << message << std::endl; + } + + void info(const std::string& message) override { + std::cout << "[INFO] " << message << std::endl; + } + + void warn(const std::string& message) override { + std::cout << "[WARN] " << message << std::endl; + } + + void error(const std::string& message) override { + std::cerr << "[ERROR] " << message << std::endl; + } +}; + +// Helper function to load flags configuration from JSON file +bool loadFlagsConfiguration(const std::string& filepath, eppoclient::Configuration& config) { + std::ifstream file(filepath); + if (!file.is_open()) { + std::cerr << "Failed to open flags configuration file: " << filepath << std::endl; + return false; + } + + std::string configJson((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + + auto result = eppoclient::parseConfiguration(configJson); + + if (!result.hasValue()) { + std::cerr << "Failed to parse configuration:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } + return false; + } + + config = std::move(*result.value); + return true; +} + +/** + * Custom Configuration Manager with Manual Synchronization + * + * This class demonstrates how to implement your own synchronization strategy + * around ConfigurationStore. The key insight is that you only need to protect + * the getConfiguration() call with a mutex - the actual flag evaluation can + * happen in parallel without any locking because Configuration is immutable. + * + * This approach provides better performance than protecting the entire + * EvaluationClient with a mutex, especially when evaluating many flags. + */ +class ManualSyncConfigManager { +private: + std::shared_ptr configStore_; + mutable std::mutex mutex_; + +public: + ManualSyncConfigManager() : configStore_(std::make_shared()) {} + + /** + * Thread-safe configuration update + * + * Note: ConfigurationStore::setConfiguration() is internally thread-safe + * (uses atomic operations), so we technically don't need the mutex here. + * However, we include it for demonstration purposes and to show a complete + * synchronization pattern. + */ + void updateConfiguration(const eppoclient::Configuration& config) { + std::lock_guard lock(mutex_); + std::cout << "[ConfigManager] Updating configuration..." << std::endl; + configStore_->setConfiguration(config); + std::cout << "[ConfigManager] Configuration updated successfully" << std::endl; + } + + /** + * Thread-safe configuration retrieval + * + * This is the critical operation that needs protection. We use the mutex + * only to safely copy the shared_ptr, which is very fast. After we have + * the shared_ptr, we can release the lock and evaluate flags in parallel. + */ + std::shared_ptr getConfiguration() const { + std::lock_guard lock(mutex_); + return configStore_->getConfiguration(); + } +}; + +int main() { + std::cout << "=== EvaluationClient with Manual Synchronization Example ===\n" << std::endl; + + // Step 1: Load initial configuration + std::cout << "Step 1: Loading initial configuration from file..." << std::endl; + eppoclient::Configuration initialConfig; + if (!loadFlagsConfiguration("config/flags-v1.json", initialConfig)) { + return 1; + } + std::cout << "Initial configuration loaded successfully\n" << std::endl; + + // Step 2: Create configuration manager with manual synchronization + std::cout << "Step 2: Setting up configuration manager..." << std::endl; + ManualSyncConfigManager configManager; + configManager.updateConfiguration(initialConfig); + std::cout << "" << std::endl; + + // Step 3: Create loggers (required for EvaluationClient) + std::cout << "Step 3: Creating loggers..." << std::endl; + ConsoleAssignmentLogger assignmentLogger; + ConsoleBanditLogger banditLogger; + ConsoleApplicationLogger applicationLogger; + std::cout << "Loggers created\n" << std::endl; + + // Step 4: Demonstrate manual synchronization pattern + std::cout << "Step 4: Evaluating flags with EvaluationClient\n" << std::endl; + std::cout << "--- Approach: Manual synchronization for optimal performance ---" << std::endl; + std::cout << "The pattern is:" << std::endl; + std::cout << "1. Lock mutex briefly to get Configuration shared_ptr (fast!)" << std::endl; + std::cout << "2. Release mutex immediately" << std::endl; + std::cout << "3. Create EvaluationClient with Configuration reference" << std::endl; + std::cout << "4. Evaluate flags without any locking (Configuration is immutable)\n" + << std::endl; + + // Get configuration (this is the only part that needs locking) + std::cout << "[Main] Retrieving configuration from manager..." << std::endl; + auto config = configManager.getConfiguration(); + std::cout << "[Main] Configuration retrieved (mutex released)\n" << std::endl; + + // Create EvaluationClient (cheap operation, no locking needed) + // Note: All parameters are passed by reference and must outlive the client + std::cout << "[Main] Creating EvaluationClient..." << std::endl; + eppoclient::EvaluationClient evaluationClient(*config, assignmentLogger, banditLogger, + applicationLogger); + std::cout << "[Main] EvaluationClient created\n" << std::endl; + + // Step 5: Evaluate flags (no locking needed - Configuration is immutable) + std::cout << "Step 5: Evaluating multiple flags (no mutex contention!)\n" << std::endl; + + // Define subject attributes + eppoclient::Attributes attributes1; + attributes1["should_disable_feature"] = false; + + std::cout << "=== Test 1: Boolean flag evaluation ===" << std::endl; + bool boolResult = evaluationClient.getBooleanAssignment("boolean-false-assignment", + "user-alice", attributes1, false); + std::cout << "Result: " << (boolResult ? "true" : "false") << std::endl; + + std::cout << "\n=== Test 2: String assignment with different subject ===" << std::endl; + eppoclient::Attributes attributes2; + attributes2["should_disable_feature"] = true; + bool boolResult2 = evaluationClient.getBooleanAssignment("boolean-false-assignment", "user-bob", + attributes2, false); + std::cout << "Result: " << (boolResult2 ? "true" : "false") << std::endl; + + std::cout << "\n=== Test 3: JSON assignment ===" << std::endl; + eppoclient::Attributes attributes3; + attributes3["Force Empty"] = "false"; + std::string jsonResult = evaluationClient.getSerializedJSONAssignment( + "json-config-flag", "user-charlie", attributes3, + "{\"integer\": 0, \"string\": \"default\", \"float\": 0.0}"); + + nlohmann::json jsonObj = nlohmann::json::parse(jsonResult); + std::cout << "JSON result: " << jsonResult << std::endl; + std::cout << "Parsed values:" << std::endl; + std::cout << " integer: " << jsonObj["integer"] << std::endl; + std::cout << " string: " << jsonObj["string"] << std::endl; + std::cout << " float: " << jsonObj["float"] << std::endl; + + // Step 6: Demonstrate configuration updates + std::cout << "\n\nStep 6: Demonstrating configuration updates\n" << std::endl; + std::cout << "In a real application, you might periodically fetch new configuration" + << std::endl; + std::cout << "from a server or file and update the ConfigurationStore." << std::endl; + std::cout << "\nSimulating configuration update..." << std::endl; + + // Load configuration again (in real app, this might be new config from server) + eppoclient::Configuration updatedConfig; + if (loadFlagsConfiguration("config/flags-v1.json", updatedConfig)) { + configManager.updateConfiguration(updatedConfig); + std::cout << "Configuration updated! New evaluations will use updated config.\n" + << std::endl; + + // Get new configuration and create new evaluation client + auto newConfig = configManager.getConfiguration(); + eppoclient::EvaluationClient newEvaluationClient(*newConfig, assignmentLogger, banditLogger, + applicationLogger); + + std::cout << "=== Test 4: Evaluation with updated configuration ===" << std::endl; + bool updatedResult = newEvaluationClient.getBooleanAssignment( + "boolean-false-assignment", "user-dave", attributes1, false); + std::cout << "Result with updated config: " << (updatedResult ? "true" : "false") + << std::endl; + } + + // Step 7: Demonstrate evaluation details + std::cout << "\n\nStep 7: Using evaluation details for debugging\n" << std::endl; + std::cout << "EvaluationClient also supports *Details() methods for debugging:" << std::endl; + + auto detailsResult = evaluationClient.getBooleanAssignmentDetails( + "boolean-false-assignment", "user-eve", attributes1, false); + + std::cout << "\n=== Test 5: Boolean assignment with details ===" << std::endl; + std::cout << "Variation: " << (detailsResult.variation ? "true" : "false") << std::endl; + + if (detailsResult.evaluationDetails.has_value()) { + const auto& details = *detailsResult.evaluationDetails; + std::cout << "Evaluation details available:" << std::endl; + std::cout << " Flag Key: " << details.flagKey << std::endl; + std::cout << " Subject Key: " << details.subjectKey << std::endl; + + if (details.flagEvaluationCode.has_value()) { + std::string code = eppoclient::flagEvaluationCodeToString(*details.flagEvaluationCode); + std::cout << " Evaluation Code: " << code << std::endl; + } + + if (!details.flagEvaluationDescription.empty()) { + std::cout << " Description: " << details.flagEvaluationDescription << std::endl; + } + } + + // Summary + std::cout << "\n\n=== Summary ===" << std::endl; + std::cout << "This example demonstrated:" << std::endl; + std::cout << "1. Using EvaluationClient for direct flag evaluation" << std::endl; + std::cout << "2. Implementing custom synchronization around ConfigurationStore" << std::endl; + std::cout << "3. Separating config retrieval (fast, locked) from evaluation (unlocked)" + << std::endl; + std::cout << "4. Updating configuration at runtime" << std::endl; + std::cout << "5. Using evaluation details for debugging" << std::endl; + std::cout << "\nKey benefits of this approach:" << std::endl; + std::cout << "- Maximum performance: minimal locking, parallel evaluation" << std::endl; + std::cout << "- Flexibility: implement your own synchronization strategy" << std::endl; + std::cout << "- Direct control: manage Configuration lifetime explicitly" << std::endl; + std::cout << "\nFor a more automated approach, use EppoClient instead!" << std::endl; + + return 0; +} diff --git a/src/bandit_model.cpp b/src/bandit_model.cpp index 54e1afd..61b67ac 100644 --- a/src/bandit_model.cpp +++ b/src/bandit_model.cpp @@ -1,5 +1,4 @@ #include "bandit_model.hpp" -#include #include "json_utils.hpp" #include "time_utils.hpp" diff --git a/src/client.cpp b/src/client.cpp index 0d437f6..37bbb6a 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -21,11 +21,11 @@ EvaluationClient EppoClient::evaluationClient(const Configuration& config) const return EvaluationClient(config, *assignmentLogger_, *banditLogger_, *applicationLogger_); } -bool EppoClient::getBoolAssignment(const std::string& flagKey, const std::string& subjectKey, - const Attributes& subjectAttributes, bool defaultValue) { +bool EppoClient::getBooleanAssignment(const std::string& flagKey, const std::string& subjectKey, + const Attributes& subjectAttributes, bool defaultValue) { auto config = configurationStore_->getConfiguration(); - return evaluationClient(*config).getBoolAssignment(flagKey, subjectKey, subjectAttributes, - defaultValue); + return evaluationClient(*config).getBooleanAssignment(flagKey, subjectKey, subjectAttributes, + defaultValue); } double EppoClient::getNumericAssignment(const std::string& flagKey, const std::string& subjectKey, diff --git a/src/client.hpp b/src/client.hpp index f816a1c..1be1f40 100644 --- a/src/client.hpp +++ b/src/client.hpp @@ -55,7 +55,7 @@ class NoOpBanditLogger : public BanditLogger { * eppoclient::EppoClient client(configStore, nullptr, nullptr, logger); * * // If flag doesn't exist, logs an info message and returns false - * bool result = client.getBoolAssignment("my-flag", "user-123", attrs, false); + * bool result = client.getBooleanAssignment("my-flag", "user-123", attrs, false); * @endcode */ class EppoClient { @@ -76,8 +76,8 @@ class EppoClient { std::shared_ptr applicationLogger = nullptr); // Get boolean assignment - bool getBoolAssignment(const std::string& flagKey, const std::string& subjectKey, - const Attributes& subjectAttributes, bool defaultValue); + bool getBooleanAssignment(const std::string& flagKey, const std::string& subjectKey, + const Attributes& subjectAttributes, bool defaultValue); // Get numeric assignment double getNumericAssignment(const std::string& flagKey, const std::string& subjectKey, diff --git a/src/evalflags.cpp b/src/evalflags.cpp index 2e974b7..9ccc506 100644 --- a/src/evalflags.cpp +++ b/src/evalflags.cpp @@ -1,5 +1,4 @@ #include "evalflags.hpp" -#include #include "third_party/md5_wrapper.h" #include "time_utils.hpp" #include "version.hpp" diff --git a/src/evaluation_client.cpp b/src/evaluation_client.cpp index 33b61d9..3151920 100644 --- a/src/evaluation_client.cpp +++ b/src/evaluation_client.cpp @@ -16,8 +16,10 @@ EvaluationClient::EvaluationClient(const Configuration& configuration, banditLogger_(banditLogger), applicationLogger_(applicationLogger) {} -bool EvaluationClient::getBoolAssignment(const std::string& flagKey, const std::string& subjectKey, - const Attributes& subjectAttributes, bool defaultValue) { +bool EvaluationClient::getBooleanAssignment(const std::string& flagKey, + const std::string& subjectKey, + const Attributes& subjectAttributes, + bool defaultValue) { auto variation = getAssignment(configuration_, flagKey, subjectKey, subjectAttributes, VariationType::BOOLEAN); return extractVariation(variation, flagKey, VariationType::BOOLEAN, defaultValue); diff --git a/src/evaluation_client.hpp b/src/evaluation_client.hpp index 7dc500c..4589b70 100644 --- a/src/evaluation_client.hpp +++ b/src/evaluation_client.hpp @@ -66,7 +66,7 @@ class BanditLogger { * banditLogger, applicationLogger); * * // If flag doesn't exist, logs an info message and returns false - * bool result = evaluationClient.getBoolAssignment("my-flag", "user-123", attrs, false); + * bool result = evaluationClient.getBooleanAssignment("my-flag", "user-123", attrs, false); * @endcode * * @note Most users should use EppoClient instead, which provides a simpler API @@ -78,8 +78,8 @@ class EvaluationClient { BanditLogger& banditLogger, ApplicationLogger& applicationLogger); // Get boolean assignment - bool getBoolAssignment(const std::string& flagKey, const std::string& subjectKey, - const Attributes& subjectAttributes, bool defaultValue); + bool getBooleanAssignment(const std::string& flagKey, const std::string& subjectKey, + const Attributes& subjectAttributes, bool defaultValue); // Get numeric assignment double getNumericAssignment(const std::string& flagKey, const std::string& subjectKey, diff --git a/src/rules.cpp b/src/rules.cpp index 466b8e4..d64d387 100644 --- a/src/rules.cpp +++ b/src/rules.cpp @@ -1,5 +1,4 @@ #include "rules.hpp" -#include #include #include "config_response.hpp" #include "json_utils.hpp" diff --git a/src/version.hpp b/src/version.hpp index 24ca2f1..0bb8ab0 100644 --- a/src/version.hpp +++ b/src/version.hpp @@ -4,7 +4,7 @@ #define EPPO_STRINGIFY_HELPER(x) #x #define EPPO_STRINGIFY(x) EPPO_STRINGIFY_HELPER(x) -#define EPPOCLIENT_VERSION_MAJOR 1 +#define EPPOCLIENT_VERSION_MAJOR 2 #define EPPOCLIENT_VERSION_MINOR 0 #define EPPOCLIENT_VERSION_PATCH 0 #define EPPOCLIENT_VERSION \ diff --git a/test/shared_test_cases/test_bandit_evaluation.cpp b/test/shared_test_cases/test_bandit_evaluation.cpp index 7b0e54c..55bf7e2 100644 --- a/test/shared_test_cases/test_bandit_evaluation.cpp +++ b/test/shared_test_cases/test_bandit_evaluation.cpp @@ -7,7 +7,6 @@ #include #include "../src/bandit_model.hpp" #include "../src/client.hpp" -#include "../src/config_response.hpp" using namespace eppoclient; using json = nlohmann::json; diff --git a/test/shared_test_cases/test_flag_evaluation.cpp b/test/shared_test_cases/test_flag_evaluation.cpp index 29dad2d..65de3ef 100644 --- a/test/shared_test_cases/test_flag_evaluation.cpp +++ b/test/shared_test_cases/test_flag_evaluation.cpp @@ -217,8 +217,8 @@ TEST_CASE("UFC Test Cases - Flag Assignments", "[ufc][flags]") { case VariationType::BOOLEAN: { bool defaultVal = testCase.defaultValue.get(); bool result = - client.getBoolAssignment(testCase.flag, subject.subjectKey, - subject.subjectAttributes, defaultVal); + client.getBooleanAssignment(testCase.flag, subject.subjectKey, + subject.subjectAttributes, defaultVal); assignment = result; break; } diff --git a/test/shared_test_cases/test_flag_performance.cpp b/test/shared_test_cases/test_flag_performance.cpp index 6ea09b9..2baa129 100644 --- a/test/shared_test_cases/test_flag_performance.cpp +++ b/test/shared_test_cases/test_flag_performance.cpp @@ -261,8 +261,8 @@ TEST_CASE("Performance - Flag Evaluation Timing", "[performance][flags]") { switch (testCase.variationType) { case VariationType::BOOLEAN: { bool defaultVal = testCase.defaultValue.get(); - client.getBoolAssignment(testCase.flag, subject.subjectKey, - subject.subjectAttributes, defaultVal); + client.getBooleanAssignment(testCase.flag, subject.subjectKey, + subject.subjectAttributes, defaultVal); break; } case VariationType::STRING: { @@ -315,8 +315,8 @@ TEST_CASE("Performance - Flag Evaluation Timing", "[performance][flags]") { switch (testCase.variationType) { case VariationType::BOOLEAN: { bool defaultVal = testCase.defaultValue.get(); - client.getBoolAssignment(testCase.flag, subject.subjectKey, - subject.subjectAttributes, defaultVal); + client.getBooleanAssignment(testCase.flag, subject.subjectKey, + subject.subjectAttributes, defaultVal); break; } case VariationType::STRING: { diff --git a/test/test_graceful_failure_mode.cpp b/test/test_graceful_failure_mode.cpp index ae5ab3a..7566fe8 100644 --- a/test/test_graceful_failure_mode.cpp +++ b/test/test_graceful_failure_mode.cpp @@ -47,8 +47,8 @@ TEST_CASE("Error handling - returns default values and logs errors", "[error-han Attributes attrs; attrs["test"] = std::string("value"); - SECTION("getBoolAssignment returns default value on empty subject key error") { - bool result = client.getBoolAssignment("test-flag", "", attrs, true); + SECTION("getBooleanAssignment returns default value on empty subject key error") { + bool result = client.getBooleanAssignment("test-flag", "", attrs, true); // Should return default value CHECK(result == true); @@ -177,7 +177,7 @@ TEST_CASE("Error handling - Empty subject key errors", "[error-handling]") { attrs["test"] = std::string("value"); SECTION("Empty subject key returns default and logs error") { - bool result = client.getBoolAssignment("test-flag", "", attrs, true); + bool result = client.getBooleanAssignment("test-flag", "", attrs, true); CHECK(result == true); // Should have logged error @@ -206,7 +206,7 @@ TEST_CASE("Error handling - Empty flag key errors", "[error-handling]") { attrs["test"] = std::string("value"); SECTION("Empty flag key returns default and logs error") { - bool result = client.getBoolAssignment("", "test-subject", attrs, false); + bool result = client.getBooleanAssignment("", "test-subject", attrs, false); CHECK(result == false); // Should have logged error @@ -235,7 +235,7 @@ TEST_CASE("Error handling - Missing flag configuration", "[error-handling]") { attrs["test"] = std::string("value"); SECTION("Missing flag returns default and logs info") { - bool result = client.getBoolAssignment("nonexistent-flag", "test-subject", attrs, true); + bool result = client.getBooleanAssignment("nonexistent-flag", "test-subject", attrs, true); CHECK(result == true); // Should have logged info about missing flag