From c0acca88d400bf4be3d4e87d95f428ca2e2bf274 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Wed, 1 Jul 2026 09:36:14 -0700 Subject: [PATCH] feat: Add C bindings for Redis Big Segments store --- .../redis/redis_big_segment_store.h | 118 ++++++++++++++++++ .../src/CMakeLists.txt | 1 + .../redis/redis_big_segment_store.cpp | 51 ++++++++ .../tests/c_bindings_test.cpp | 106 +++++++++++++++- 4 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 libs/server-sdk-redis-source/include/launchdarkly/server_side/bindings/c/integrations/redis/redis_big_segment_store.h create mode 100644 libs/server-sdk-redis-source/src/bindings/redis/redis_big_segment_store.cpp diff --git a/libs/server-sdk-redis-source/include/launchdarkly/server_side/bindings/c/integrations/redis/redis_big_segment_store.h b/libs/server-sdk-redis-source/include/launchdarkly/server_side/bindings/c/integrations/redis/redis_big_segment_store.h new file mode 100644 index 000000000..b0693211f --- /dev/null +++ b/libs/server-sdk-redis-source/include/launchdarkly/server_side/bindings/c/integrations/redis/redis_big_segment_store.h @@ -0,0 +1,118 @@ +/** @file redis_big_segment_store.h + * @brief LaunchDarkly Server-side Redis Big Segments Store C Binding. + */ +// NOLINTBEGIN modernize-use-using +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +// only need to export C interface if +// used by C++ source code +#endif + +/** + * @brief LDServerBigSegmentsRedisStore is a Big Segments persistent store for + * the Server-Side SDK backed by Redis. It is meant to be passed to the SDK + * via the Big Segments config builder. + * + * Call @ref LDServerBigSegmentsRedisStore_New to obtain a new instance. This + * instance is passed into the SDK's Big Segments configuration. + * + * Example: + * @code + * // Stack allocate the result struct, which will hold the result pointer or + * // an error message. + * struct LDServerBigSegmentsRedisResult result; + * + * // Create the Redis Big Segment store, passing in arguments for the URI, + * // prefix, and pointer to the result. + * if (!LDServerBigSegmentsRedisStore_New("redis://localhost:6379", + * "testprefix", &result)) { + * // On failure, you may print the error message (result.error_message), + * // then exit or return. + * } + * + * // Create the Big Segments builder, taking ownership of the store pointer. + * LDServerBigSegmentsBuilder bs_builder = LDServerBigSegmentsBuilder_New( + * (LDServerBigSegmentStorePtr)result.store); + * + * // Attach the Big Segments builder to the SDK config. + * LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + * LDServerConfigBuilder_BigSegments(cfg_builder, bs_builder); + * @endcode + * + * This implementation is backed by Redis++, a C++ wrapper + * for the hiredis library. + */ +typedef struct _LDServerBigSegmentsRedisStore* LDServerBigSegmentsRedisStore; + +/* Defines the size of the error message buffer in + * LDServerBigSegmentsRedisResult. + */ +#ifndef LDSERVER_BIGSEGMENTS_REDISSTORE_ERROR_MESSAGE_SIZE +#define LDSERVER_BIGSEGMENTS_REDISSTORE_ERROR_MESSAGE_SIZE 256 +#endif + +/** + * @brief Stores the result of calling @ref LDServerBigSegmentsRedisStore_New. + * + * On successful creation, store will contain a pointer which may be passed + * into the LaunchDarkly SDK's Big Segments configuration. + * + * On failure, error_message contains a NULL-terminated string describing the + * error. + * + * The message may be truncated if it was originally longer than + * error_message's buffer size. + */ +struct LDServerBigSegmentsRedisResult { + LDServerBigSegmentsRedisStore store; + char error_message[LDSERVER_BIGSEGMENTS_REDISSTORE_ERROR_MESSAGE_SIZE]; +}; + +/** + * @brief Creates a new Redis Big Segment store suitable for usage in the SDK's + * Big Segments configuration. + * + * In this system, the SDK will query Redis for Big Segments membership + * as required, with an in-memory cache to reduce the number of queries. + * + * Data is never written back to Redis by the SDK; the LaunchDarkly Relay + * Proxy populates the store. + * + * @param uri Redis URI string. Must not be NULL or empty string. + * + * @param prefix Prefix to use when reading SDK data from Redis. This allows + * multiple SDK environments to coexist in the same database, or for the SDK's + * data to coexist with other unrelated data. Must not be NULL. + * + * @param out_result Pointer to struct where the store pointer or an error + * message should be stored. + * + * @return True if the store was created successfully; out_result->store + * will contain the pointer. The caller must either free the pointer with + * @ref LDServerBigSegmentsRedisStore_Free, OR pass it into the SDK's Big + * Segments configuration which will take ownership (in which case do not + * call @ref LDServerBigSegmentsRedisStore_Free.) + */ +LD_EXPORT(bool) +LDServerBigSegmentsRedisStore_New( + char const* uri, + char const* prefix, + struct LDServerBigSegmentsRedisResult* out_result); + +/** + * @brief Frees a Redis Big Segment store pointer. Only necessary to call if + * not passing ownership to the SDK's Big Segments configuration. + */ +LD_EXPORT(void) +LDServerBigSegmentsRedisStore_Free(LDServerBigSegmentsRedisStore store); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/server-sdk-redis-source/src/CMakeLists.txt b/libs/server-sdk-redis-source/src/CMakeLists.txt index e459a1dd0..c746f7b5a 100644 --- a/libs/server-sdk-redis-source/src/CMakeLists.txt +++ b/libs/server-sdk-redis-source/src/CMakeLists.txt @@ -16,6 +16,7 @@ target_sources(${LIBNAME} redis_source.cpp redis_big_segment_store.cpp bindings/redis/redis_source.cpp + bindings/redis/redis_big_segment_store.cpp ) diff --git a/libs/server-sdk-redis-source/src/bindings/redis/redis_big_segment_store.cpp b/libs/server-sdk-redis-source/src/bindings/redis/redis_big_segment_store.cpp new file mode 100644 index 000000000..12b6c4117 --- /dev/null +++ b/libs/server-sdk-redis-source/src/bindings/redis/redis_big_segment_store.cpp @@ -0,0 +1,51 @@ +#include + +#include + +#include + +#include + +using namespace launchdarkly::server_side::integrations; + +LD_EXPORT(bool) +LDServerBigSegmentsRedisStore_New(char const* uri, + char const* prefix, + LDServerBigSegmentsRedisResult* out_result) { + LD_ASSERT_NOT_NULL(uri); + LD_ASSERT_NOT_NULL(prefix); + LD_ASSERT_NOT_NULL(out_result); + + // Explicitely zero out the exception_msg buffer in case the exception is + // shorter than the buffer. + memset(out_result->error_message, 0, + sizeof(LDServerBigSegmentsRedisResult::error_message)); + + // Ensure the store pointer isn't garbage. + out_result->store = nullptr; + + auto maybe_store = RedisBigSegmentStore::Create(uri, prefix); + if (!maybe_store) { + // Avoid heap allocating another string to pass back to the caller; + // instead, we copy into the buffer and ensure a terminator is present. + // This does mean the message may be truncated. + + std::size_t const len = maybe_store.error().copy( + out_result->error_message, sizeof(out_result->error_message) - 1); + out_result->error_message[len] = '\0'; + + return false; + } + + // The pointer is no longer managed and must either be freed by the caller, + // or passed into the SDK which will take ownership. + out_result->store = + reinterpret_cast(maybe_store->release()); + + return true; +} + +LD_EXPORT(void) +LDServerBigSegmentsRedisStore_Free(LDServerBigSegmentsRedisStore store) { + delete reinterpret_cast(store); +} diff --git a/libs/server-sdk-redis-source/tests/c_bindings_test.cpp b/libs/server-sdk-redis-source/tests/c_bindings_test.cpp index 3d9b2c26d..193e044e7 100644 --- a/libs/server-sdk-redis-source/tests/c_bindings_test.cpp +++ b/libs/server-sdk-redis-source/tests/c_bindings_test.cpp @@ -1,5 +1,6 @@ #include +#include #include #include @@ -10,6 +11,10 @@ #include "prefixed_redis_client.hpp" +#include +#include +#include + using namespace launchdarkly::data_model; TEST(RedisBindings, SourcePointerIsStoredOnSuccessfulCreation) { @@ -91,8 +96,8 @@ TEST(RedisBindings, CanUseInSDKLazyLoadDataSource) { std::unordered_map values; LDValue_ObjectIter iter; - for (iter = LDValue_ObjectIter_New(all); - !LDValue_ObjectIter_End(iter); LDValue_ObjectIter_Next(iter)) { + for (iter = LDValue_ObjectIter_New(all); !LDValue_ObjectIter_End(iter); + LDValue_ObjectIter_Next(iter)) { char const* key = LDValue_ObjectIter_Key(iter); // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) auto value_ref = reinterpret_cast( @@ -112,3 +117,100 @@ TEST(RedisBindings, CanUseInSDKLazyLoadDataSource) { LDContext_Free(context); LDServerSDK_Free(sdk); } + +TEST(RedisBindings, BigSegmentsStorePointerIsStoredOnSuccessfulCreation) { + LDServerBigSegmentsRedisResult result; + ASSERT_TRUE(LDServerBigSegmentsRedisStore_New("tcp://localhost:1234", "foo", + &result)); + ASSERT_NE(result.store, nullptr); + LDServerBigSegmentsRedisStore_Free(result.store); +} + +TEST(RedisBindings, BigSegmentsErrorMessageIsPropagatedOnFailure) { + LDServerBigSegmentsRedisResult result; + ASSERT_FALSE( + LDServerBigSegmentsRedisStore_New("totally not a URI", "foo", &result)); + ASSERT_STRNE(result.error_message, ""); +} + +TEST(RedisBindings, BigSegmentsStorePointerIsNullptrOnFailure) { + LDServerBigSegmentsRedisResult result; + ASSERT_FALSE( + LDServerBigSegmentsRedisStore_New("totally not a URI", "foo", &result)); + ASSERT_EQ(result.store, nullptr); +} + +// This is an end-to-end test that uses an actual Redis instance with +// provisioned Big Segments metadata. The store is passed into the SDK's Big +// Segments configuration, and the SDK's Big Segment store status listener is +// used to verify that the store is reachable and reports available. +TEST(RedisBindings, CanUseInSDKBigSegmentsConfig) { + sw::redis::Redis redis("tcp://localhost:6379"); + redis.flushdb(); + + // Set the store's sync timestamp so the poll reports available. + auto const now_ms = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + redis.set("testprefix:big_segments_synchronized_on", + std::to_string(now_ms)); + + LDServerBigSegmentsRedisResult result; + ASSERT_TRUE(LDServerBigSegmentsRedisStore_New("tcp://localhost:6379", + "testprefix", &result)); + + LDServerBigSegmentsBuilder bs_builder = LDServerBigSegmentsBuilder_New( + reinterpret_cast(result.store)); + LDServerBigSegmentsBuilder_StatusPollIntervalMs(bs_builder, 50); + + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + LDServerConfigBuilder_BigSegments(cfg_builder, bs_builder); + LDServerConfigBuilder_Offline(cfg_builder, true); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerSDK sdk = LDServerSDK_New(config); + + std::mutex mu; + std::condition_variable cv; + bool available = false; + + struct ListenerCtx { + std::mutex* mu; + std::condition_variable* cv; + bool* available; + }; + ListenerCtx ctx{&mu, &cv, &available}; + + struct LDServerBigSegmentStoreStatusListener listener{}; + LDServerBigSegmentStoreStatusListener_Init(&listener); + listener.UserData = &ctx; + listener.StatusChanged = + +[](LDServerBigSegmentStoreStatus s, void* user_data) { + auto* c = static_cast(user_data); + { + std::lock_guard lk(*c->mu); + *c->available = LDServerBigSegmentStoreStatus_Available(s); + } + c->cv->notify_all(); + }; + + LDListenerConnection connection = + LDServerSDK_BigSegmentStoreStatus_OnStatusChange(sdk, listener); + ASSERT_NE(connection, nullptr); + + LDServerSDK_Start(sdk, LD_NONBLOCKING, nullptr); + + { + std::unique_lock lk(mu); + cv.wait_for(lk, std::chrono::seconds(3), [&] { return available; }); + } + + EXPECT_TRUE(available); + + LDListenerConnection_Disconnect(connection); + LDListenerConnection_Free(connection); + LDServerSDK_Free(sdk); +}