From 92c4a2a57aa0677688e0a538250549d224e105eb Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Wed, 1 Jul 2026 09:36:14 -0700 Subject: [PATCH 1/4] 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); +} From d72a78b02effd0b2e7ce62ff5c1fee0d4d6e1748 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 24 Jul 2026 13:29:50 -0700 Subject: [PATCH 2/4] docs: Include stdbool.h and note error message sanitization --- .../c/integrations/redis/redis_big_segment_store.h | 7 +++++++ 1 file changed, 7 insertions(+) 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 index b0693211f..c6205db2c 100644 --- 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 @@ -6,6 +6,8 @@ #include +#include + #ifdef __cplusplus extern "C" { // only need to export C interface if @@ -67,6 +69,11 @@ typedef struct _LDServerBigSegmentsRedisStore* LDServerBigSegmentsRedisStore; * * The message may be truncated if it was originally longer than * error_message's buffer size. + * + * The message originates from the underlying Redis client and may echo back + * portions of the URI, including query parameters. Callers that surface this + * message (logs, telemetry, user-facing errors) may want to sanitize it + * accordingly. */ struct LDServerBigSegmentsRedisResult { LDServerBigSegmentsRedisStore store; From 2e40afea7eedc6a01d53317706e7f833717963b6 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Wed, 1 Jul 2026 11:24:54 -0700 Subject: [PATCH 3/4] feat: Add C bindings for DynamoDB LazyLoad source --- .../dynamodb/dynamodb_client_options.h | 121 ++++++++++++ .../c/integrations/dynamodb/dynamodb_source.h | 134 +++++++++++++ .../src/CMakeLists.txt | 2 + .../src/bindings/dynamodb/client_options.cpp | 67 +++++++ .../src/bindings/dynamodb/dynamodb_source.cpp | 59 ++++++ .../tests/c_bindings_test.cpp | 176 ++++++++++++++++++ 6 files changed, 559 insertions(+) create mode 100644 libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_client_options.h create mode 100644 libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_source.h create mode 100644 libs/server-sdk-dynamodb-source/src/bindings/dynamodb/client_options.cpp create mode 100644 libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_source.cpp create mode 100644 libs/server-sdk-dynamodb-source/tests/c_bindings_test.cpp diff --git a/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_client_options.h b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_client_options.h new file mode 100644 index 000000000..9f23f4174 --- /dev/null +++ b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_client_options.h @@ -0,0 +1,121 @@ +/** @file dynamodb_client_options.h + * @brief LaunchDarkly Server-side DynamoDB Client Options 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 LDServerDynamoDBClientOptionsBuilder configures the AWS DynamoDB + * client that a LaunchDarkly DynamoDB integration will use. + * + * All fields are optional. When left unset, the AWS SDK's default provider + * chain (environment variables, shared config, EC2/ECS instance metadata) is + * used to resolve the corresponding field. + * + * The builder is passed by handle to a DynamoDB source or store factory, + * which takes ownership and frees it. Callers only need to call + * @ref LDServerDynamoDBClientOptionsBuilder_Free directly if the builder is + * not passed to a factory. + */ +typedef struct _LDServerDynamoDBClientOptionsBuilder* + LDServerDynamoDBClientOptionsBuilder; + +/** + * @brief Creates a new DynamoDB client options builder with all fields unset. + * + * @return A new builder handle. + */ +LD_EXPORT(LDServerDynamoDBClientOptionsBuilder) +LDServerDynamoDBClientOptionsBuilder_New(void); + +/** + * @brief Sets the AWS region for the DynamoDB client. + * + * When unset, the AWS SDK resolves the region via the standard region + * provider chain (environment variables, shared config file, instance + * metadata). + * + * @param b Builder. Must not be NULL. + * @param region Region string (e.g. "us-east-1"). Must not be NULL. + */ +LD_EXPORT(void) +LDServerDynamoDBClientOptionsBuilder_Region( + LDServerDynamoDBClientOptionsBuilder b, + char const* region); + +/** + * @brief Sets a custom endpoint for the DynamoDB client. + * + * Useful when pointing at DynamoDB Local or LocalStack for testing (e.g. + * "http://localhost:8000"). When unset, the AWS SDK uses the standard + * DynamoDB endpoint for the resolved region. + * + * @param b Builder. Must not be NULL. + * @param endpoint Endpoint URL. Must not be NULL. + */ +LD_EXPORT(void) +LDServerDynamoDBClientOptionsBuilder_Endpoint( + LDServerDynamoDBClientOptionsBuilder b, + char const* endpoint); + +/** + * @brief Sets the AWS access key ID for the DynamoDB client. + * + * When none of AccessKeyId, SecretAccessKey, or SessionToken are set, the + * AWS SDK's default credential provider chain is used (environment + * variables, shared credentials file, EC2/ECS roles). + * + * @param b Builder. Must not be NULL. + * @param access_key_id AWS access key ID. Must not be NULL. + */ +LD_EXPORT(void) +LDServerDynamoDBClientOptionsBuilder_AccessKeyId( + LDServerDynamoDBClientOptionsBuilder b, + char const* access_key_id); + +/** + * @brief Sets the AWS secret access key for the DynamoDB client. + * + * @param b Builder. Must not be NULL. + * @param secret_access_key AWS secret access key. Must not be NULL. + */ +LD_EXPORT(void) +LDServerDynamoDBClientOptionsBuilder_SecretAccessKey( + LDServerDynamoDBClientOptionsBuilder b, + char const* secret_access_key); + +/** + * @brief Sets the AWS session token for the DynamoDB client (for temporary + * credentials). + * + * @param b Builder. Must not be NULL. + * @param session_token AWS session token. Must not be NULL. + */ +LD_EXPORT(void) +LDServerDynamoDBClientOptionsBuilder_SessionToken( + LDServerDynamoDBClientOptionsBuilder b, + char const* session_token); + +/** + * @brief Frees a DynamoDB client options builder. Do not call if the builder + * was consumed by a DynamoDB source or store factory. + * + * @param b Builder to free. + */ +LD_EXPORT(void) +LDServerDynamoDBClientOptionsBuilder_Free( + LDServerDynamoDBClientOptionsBuilder b); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_source.h b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_source.h new file mode 100644 index 000000000..bfc5a7557 --- /dev/null +++ b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_source.h @@ -0,0 +1,134 @@ +/** @file dynamodb_source.h + * @brief LaunchDarkly Server-side DynamoDB Source C Binding. + */ +// NOLINTBEGIN modernize-use-using +#pragma once + +#include + +#include + +#ifdef __cplusplus +extern "C" { +// only need to export C interface if +// used by C++ source code +#endif + +/** + * @brief LDServerLazyLoadDynamoDBSource represents a data source for the + * Server-Side SDK backed by Amazon DynamoDB. It is meant to be used in place + * of the standard LaunchDarkly Streaming or Polling data sources. + * + * Call @ref LDServerLazyLoadDynamoDBSource_New to obtain a new instance. + * This instance can be passed into the SDK's DataSystem configuration via + * the LazyLoad builder. + * + * The DynamoDB table must already exist and follow the LaunchDarkly schema: + * a String partition key named `namespace` and a String sort key named + * `key`. The LaunchDarkly Relay Proxy populates the table with this schema; + * this source only reads from it. + * + * Example: + * @code + * // Optional: configure the AWS DynamoDB client. Pass NULL for defaults. + * LDServerDynamoDBClientOptionsBuilder options = + * LDServerDynamoDBClientOptionsBuilder_New(); + * LDServerDynamoDBClientOptionsBuilder_Region(options, "us-east-1"); + * + * // Stack allocate the result struct, which will hold the result pointer or + * // an error message. + * struct LDServerLazyLoadDynamoDBResult result; + * + * if (!LDServerLazyLoadDynamoDBSource_New("my-table", "testprefix", options, + * &result)) { + * // On failure, you may print the error message (result.error_message), + * // then exit or return. + * } + * + * LDServerLazyLoadBuilder lazy_builder = LDServerLazyLoadBuilder_New(); + * LDServerLazyLoadBuilder_SourcePtr( + * lazy_builder, (LDServerLazyLoadSourcePtr)result.source); + * + * LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + * LDServerConfigBuilder_DataSystem_LazyLoad(cfg_builder, lazy_builder); + * @endcode + * + * This implementation is backed by the AWS SDK for C++. + */ +typedef struct _LDServerLazyLoadDynamoDBSource* LDServerLazyLoadDynamoDBSource; + +/* Defines the size of the error message buffer in + * LDServerLazyLoadDynamoDBResult. + */ +#ifndef LDSERVER_LAZYLOAD_DYNAMODBSOURCE_ERROR_MESSAGE_SIZE +#define LDSERVER_LAZYLOAD_DYNAMODBSOURCE_ERROR_MESSAGE_SIZE 256 +#endif + +/** + * @brief Stores the result of calling @ref LDServerLazyLoadDynamoDBSource_New. + * + * On successful creation, source will contain a pointer which may be passed + * into the LaunchDarkly SDK's 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 LDServerLazyLoadDynamoDBResult { + LDServerLazyLoadDynamoDBSource source; + char error_message[LDSERVER_LAZYLOAD_DYNAMODBSOURCE_ERROR_MESSAGE_SIZE]; +}; + +/** + * @brief Creates a new DynamoDB data source suitable for usage in the SDK's + * Lazy Load data system. + * + * In this system, the SDK will query DynamoDB for flag/segment data as + * required, with an in-memory cache to reduce the number of queries. + * + * Data is never written back to DynamoDB by the SDK; the LaunchDarkly Relay + * Proxy populates the table. + * + * @param table_name Name of the DynamoDB table to read from. The table must + * already exist; this function does not create it. Must not be NULL. + * + * @param prefix Prefix to use when reading SDK data from DynamoDB. This + * allows multiple SDK environments to coexist in the same table. Must not + * be NULL. + * + * @param options Optional AWS DynamoDB client configuration. When non-NULL, + * the builder is consumed and freed by this function; do not call + * @ref LDServerDynamoDBClientOptionsBuilder_Free on it afterward. When NULL, + * the AWS SDK's default provider chain is used for region, endpoint, and + * credentials. + * + * @param out_result Pointer to struct where the source pointer or an error + * message should be stored. Must not be NULL. + * + * @return True if the source was created successfully; out_result->source + * will contain the pointer. The caller must either free the pointer with + * @ref LDServerLazyLoadDynamoDBSource_Free, OR pass it into the SDK's + * configuration methods which will take ownership (in which case do not + * call @ref LDServerLazyLoadDynamoDBSource_Free.) + */ +LD_EXPORT(bool) +LDServerLazyLoadDynamoDBSource_New( + char const* table_name, + char const* prefix, + LDServerDynamoDBClientOptionsBuilder options, + struct LDServerLazyLoadDynamoDBResult* out_result); + +/** + * @brief Frees a DynamoDB data source pointer. Only necessary to call if not + * passing ownership to SDK configuration. + */ +LD_EXPORT(void) +LDServerLazyLoadDynamoDBSource_Free(LDServerLazyLoadDynamoDBSource source); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/server-sdk-dynamodb-source/src/CMakeLists.txt b/libs/server-sdk-dynamodb-source/src/CMakeLists.txt index 02f954b07..983bf2ea6 100644 --- a/libs/server-sdk-dynamodb-source/src/CMakeLists.txt +++ b/libs/server-sdk-dynamodb-source/src/CMakeLists.txt @@ -17,6 +17,8 @@ target_sources(${LIBNAME} dynamodb_big_segment_store.cpp aws_sdk_guard.cpp client_factory.cpp + bindings/dynamodb/client_options.cpp + bindings/dynamodb/dynamodb_source.cpp ) diff --git a/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/client_options.cpp b/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/client_options.cpp new file mode 100644 index 000000000..a8f3cf9d1 --- /dev/null +++ b/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/client_options.cpp @@ -0,0 +1,67 @@ +#include + +#include + +#include + +using namespace launchdarkly::server_side::integrations; + +#define TO_OPTIONS(ptr) (reinterpret_cast(ptr)) +#define FROM_OPTIONS(ptr) \ + (reinterpret_cast(ptr)) + +LD_EXPORT(LDServerDynamoDBClientOptionsBuilder) +LDServerDynamoDBClientOptionsBuilder_New(void) { + return FROM_OPTIONS(new DynamoDBClientOptions{}); +} + +LD_EXPORT(void) +LDServerDynamoDBClientOptionsBuilder_Region( + LDServerDynamoDBClientOptionsBuilder b, + char const* region) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(region); + TO_OPTIONS(b)->region = region; +} + +LD_EXPORT(void) +LDServerDynamoDBClientOptionsBuilder_Endpoint( + LDServerDynamoDBClientOptionsBuilder b, + char const* endpoint) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(endpoint); + TO_OPTIONS(b)->endpoint = endpoint; +} + +LD_EXPORT(void) +LDServerDynamoDBClientOptionsBuilder_AccessKeyId( + LDServerDynamoDBClientOptionsBuilder b, + char const* access_key_id) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(access_key_id); + TO_OPTIONS(b)->aws_access_key_id = access_key_id; +} + +LD_EXPORT(void) +LDServerDynamoDBClientOptionsBuilder_SecretAccessKey( + LDServerDynamoDBClientOptionsBuilder b, + char const* secret_access_key) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(secret_access_key); + TO_OPTIONS(b)->aws_secret_access_key = secret_access_key; +} + +LD_EXPORT(void) +LDServerDynamoDBClientOptionsBuilder_SessionToken( + LDServerDynamoDBClientOptionsBuilder b, + char const* session_token) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(session_token); + TO_OPTIONS(b)->aws_session_token = session_token; +} + +LD_EXPORT(void) +LDServerDynamoDBClientOptionsBuilder_Free( + LDServerDynamoDBClientOptionsBuilder b) { + delete TO_OPTIONS(b); +} diff --git a/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_source.cpp b/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_source.cpp new file mode 100644 index 000000000..d0e752e0f --- /dev/null +++ b/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_source.cpp @@ -0,0 +1,59 @@ +#include + +#include +#include + +#include + +#include +#include + +using namespace launchdarkly::server_side::integrations; + +LD_EXPORT(bool) +LDServerLazyLoadDynamoDBSource_New(char const* table_name, + char const* prefix, + LDServerDynamoDBClientOptionsBuilder options, + LDServerLazyLoadDynamoDBResult* out_result) { + LD_ASSERT_NOT_NULL(table_name); + LD_ASSERT_NOT_NULL(prefix); + LD_ASSERT_NOT_NULL(out_result); + + // Explicitely zero out the error_message buffer in case the error is + // shorter than the buffer. + memset(out_result->error_message, 0, + sizeof(LDServerLazyLoadDynamoDBResult::error_message)); + + // Ensure the source pointer isn't garbage. + out_result->source = nullptr; + + DynamoDBClientOptions opts{}; + if (options != nullptr) { + auto* opts_ptr = reinterpret_cast(options); + opts = *opts_ptr; + LDServerDynamoDBClientOptionsBuilder_Free(options); + } + + auto maybe_source = + DynamoDBDataSource::Create(table_name, prefix, std::move(opts)); + if (!maybe_source) { + // 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_source.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->source = reinterpret_cast( + maybe_source->release()); + return true; +} + +LD_EXPORT(void) +LDServerLazyLoadDynamoDBSource_Free(LDServerLazyLoadDynamoDBSource source) { + delete reinterpret_cast(source); +} diff --git a/libs/server-sdk-dynamodb-source/tests/c_bindings_test.cpp b/libs/server-sdk-dynamodb-source/tests/c_bindings_test.cpp new file mode 100644 index 000000000..315b1d166 --- /dev/null +++ b/libs/server-sdk-dynamodb-source/tests/c_bindings_test.cpp @@ -0,0 +1,176 @@ +#include + +#include +#include + +#include +#include +#include + +#include + +#include "aws_sdk_guard.hpp" +#include "prefixed_dynamodb_client.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +using namespace launchdarkly::data_model; +using namespace launchdarkly::server_side::integrations; + +namespace { + +std::string EnvOr(char const* name, std::string const& fallback) { + char const* value = std::getenv(name); + if (value && *value) { + return value; + } + return fallback; +} + +std::string LocalEndpoint() { + return EnvOr("LD_DYNAMODB_TEST_ENDPOINT", "http://localhost:8000"); +} + +std::string LocalRegion() { + return EnvOr("LD_DYNAMODB_TEST_REGION", "us-east-1"); +} + +std::unique_ptr MakeRawClient() { + detail::AwsSdkGuard::Ensure(); + Aws::Client::ClientConfiguration config; + config.region = LocalRegion(); + config.endpointOverride = LocalEndpoint(); + if (config.endpointOverride.rfind("http://", 0) == 0) { + config.scheme = Aws::Http::Scheme::HTTP; + config.verifySSL = false; + } + Aws::Auth::AWSCredentials creds("dummy", "dummy"); + return std::make_unique(creds, config); +} + +LDServerDynamoDBClientOptionsBuilder LocalOptionsBuilder() { + LDServerDynamoDBClientOptionsBuilder opts = + LDServerDynamoDBClientOptionsBuilder_New(); + LDServerDynamoDBClientOptionsBuilder_Region(opts, LocalRegion().c_str()); + LDServerDynamoDBClientOptionsBuilder_Endpoint(opts, + LocalEndpoint().c_str()); + LDServerDynamoDBClientOptionsBuilder_AccessKeyId(opts, "dummy"); + LDServerDynamoDBClientOptionsBuilder_SecretAccessKey(opts, "dummy"); + return opts; +} + +} // namespace + +TEST(DynamoDBBindings, OptionsBuilderCanBeCreatedAndFreed) { + LDServerDynamoDBClientOptionsBuilder opts = + LDServerDynamoDBClientOptionsBuilder_New(); + ASSERT_NE(opts, nullptr); + LDServerDynamoDBClientOptionsBuilder_Region(opts, "us-east-1"); + LDServerDynamoDBClientOptionsBuilder_Endpoint(opts, + "http://localhost:8000"); + LDServerDynamoDBClientOptionsBuilder_AccessKeyId(opts, "id"); + LDServerDynamoDBClientOptionsBuilder_SecretAccessKey(opts, "secret"); + LDServerDynamoDBClientOptionsBuilder_SessionToken(opts, "token"); + LDServerDynamoDBClientOptionsBuilder_Free(opts); +} + +TEST(DynamoDBBindings, LazyLoadSourcePointerIsStoredOnSuccessfulCreation) { + LDServerLazyLoadDynamoDBResult result; + ASSERT_TRUE(LDServerLazyLoadDynamoDBSource_New( + "any-table", "foo", LocalOptionsBuilder(), &result)); + ASSERT_NE(result.source, nullptr); + LDServerLazyLoadDynamoDBSource_Free(result.source); +} + +TEST(DynamoDBBindings, LazyLoadSourceAcceptsNullOptions) { + LDServerLazyLoadDynamoDBResult result; + ASSERT_TRUE(LDServerLazyLoadDynamoDBSource_New("any-table", "foo", nullptr, + &result)); + ASSERT_NE(result.source, nullptr); + LDServerLazyLoadDynamoDBSource_Free(result.source); +} + +// End-to-end test that uses an actual DynamoDB (Local) instance with +// provisioned flag data. The source is passed into the SDK's LazyLoad data +// system, and AllFlags is used to verify that the data is read back from +// DynamoDB correctly through the C binding. +TEST(DynamoDBBindings, CanUseInSDKLazyLoadDataSource) { + std::string const table_name = "ld-dynamodb-c-bindings-test"; + std::string const prefix = "testprefix"; + + auto raw_client = MakeRawClient(); + PrefixedDynamoDBClient::DeleteTable(*raw_client, table_name); + PrefixedDynamoDBClient::CreateTable(*raw_client, table_name); + + PrefixedDynamoDBClient client(*raw_client, prefix, table_name); + Flag flag_a{"foo", 1, false, std::nullopt, {true, false}}; + flag_a.offVariation = 0; + Flag flag_b{"bar", 1, false, std::nullopt, {true, false}}; + flag_b.offVariation = 1; + + client.PutFlag(flag_a); + client.PutFlag(flag_b); + client.Init(); + + LDServerLazyLoadDynamoDBResult result; + ASSERT_TRUE(LDServerLazyLoadDynamoDBSource_New( + table_name.c_str(), prefix.c_str(), LocalOptionsBuilder(), &result)); + + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + + LDServerLazyLoadBuilder lazy_builder = LDServerLazyLoadBuilder_New(); + LDServerLazyLoadBuilder_SourcePtr( + lazy_builder, + reinterpret_cast(result.source)); + LDServerConfigBuilder_DataSystem_LazyLoad(cfg_builder, lazy_builder); + LDServerConfigBuilder_Events_Enabled(cfg_builder, false); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerSDK sdk = LDServerSDK_New(config); + LDServerSDK_Start(sdk, LD_NONBLOCKING, nullptr); + + LDContextBuilder ctx_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(ctx_builder, "cat", "shadow"); + LDContext context = LDContextBuilder_Build(ctx_builder); + + LDAllFlagsState state = + LDServerSDK_AllFlagsState(sdk, context, LD_ALLFLAGSSTATE_DEFAULT); + + ASSERT_TRUE(LDAllFlagsState_Valid(state)); + LDValue all = LDAllFlagsState_Map(state); + ASSERT_EQ(LDValue_Type(all), LDValueType_Object); + + std::unordered_map values; + LDValue_ObjectIter iter; + for (iter = LDValue_ObjectIter_New(all); !LDValue_ObjectIter_End(iter); + LDValue_ObjectIter_Next(iter)) { + char const* key = LDValue_ObjectIter_Key(iter); + auto value_ref = reinterpret_cast( + LDValue_ObjectIter_Value(iter)); + values.emplace(key, *value_ref); + } + + LDValue_ObjectIter_Free(iter); + + std::unordered_map expected = { + {"foo", true}, {"bar", false}}; + ASSERT_EQ(values, expected); + + LDValue_Free(all); + LDAllFlagsState_Free(state); + + LDContext_Free(context); + LDServerSDK_Free(sdk); + + PrefixedDynamoDBClient::DeleteTable(*raw_client, table_name); +} From 54d16d80202013907d37a9331f82af1a4d6556d8 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 24 Jul 2026 13:51:56 -0700 Subject: [PATCH 4/4] docs: Address review comments on DynamoDB LazyLoad bindings --- .../dynamodb/dynamodb_client_options.h | 22 ++++++++++++++----- .../c/integrations/dynamodb/dynamodb_source.h | 2 ++ .../tests/c_bindings_test.cpp | 16 ++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_client_options.h b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_client_options.h index 9f23f4174..bf5433bb6 100644 --- a/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_client_options.h +++ b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_client_options.h @@ -69,9 +69,16 @@ LDServerDynamoDBClientOptionsBuilder_Endpoint( /** * @brief Sets the AWS access key ID for the DynamoDB client. * - * When none of AccessKeyId, SecretAccessKey, or SessionToken are set, the - * AWS SDK's default credential provider chain is used (environment - * variables, shared credentials file, EC2/ECS roles). + * The only valid credential combinations are: + * - none of AccessKeyId, SecretAccessKey, or SessionToken set: the AWS + * SDK's default credential provider chain is used (environment + * variables, shared credentials file, EC2/ECS roles). + * - AccessKeyId + SecretAccessKey (long-lived IAM keys). + * - AccessKeyId + SecretAccessKey + SessionToken (STS temporary + * credentials). + * + * Any other partial combination is rejected when the factory that consumes + * this builder is invoked. * * @param b Builder. Must not be NULL. * @param access_key_id AWS access key ID. Must not be NULL. @@ -82,7 +89,10 @@ LDServerDynamoDBClientOptionsBuilder_AccessKeyId( char const* access_key_id); /** - * @brief Sets the AWS secret access key for the DynamoDB client. + * @brief Sets the AWS secret access key for the DynamoDB client. Must be + * paired with AccessKeyId; see + * @ref LDServerDynamoDBClientOptionsBuilder_AccessKeyId for the valid + * credential combinations. * * @param b Builder. Must not be NULL. * @param secret_access_key AWS secret access key. Must not be NULL. @@ -94,7 +104,9 @@ LDServerDynamoDBClientOptionsBuilder_SecretAccessKey( /** * @brief Sets the AWS session token for the DynamoDB client (for temporary - * credentials). + * credentials). Requires AccessKeyId and SecretAccessKey to also be set; see + * @ref LDServerDynamoDBClientOptionsBuilder_AccessKeyId for the valid + * credential combinations. * * @param b Builder. Must not be NULL. * @param session_token AWS session token. Must not be NULL. diff --git a/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_source.h b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_source.h index bfc5a7557..707be2780 100644 --- a/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_source.h +++ b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_source.h @@ -8,6 +8,8 @@ #include +#include + #ifdef __cplusplus extern "C" { // only need to export C interface if diff --git a/libs/server-sdk-dynamodb-source/tests/c_bindings_test.cpp b/libs/server-sdk-dynamodb-source/tests/c_bindings_test.cpp index 315b1d166..7ab42c3ba 100644 --- a/libs/server-sdk-dynamodb-source/tests/c_bindings_test.cpp +++ b/libs/server-sdk-dynamodb-source/tests/c_bindings_test.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -97,6 +98,21 @@ TEST(DynamoDBBindings, LazyLoadSourceAcceptsNullOptions) { LDServerLazyLoadDynamoDBSource_Free(result.source); } +TEST(DynamoDBBindings, LazyLoadSourceRejectsPartialCredentials) { + LDServerDynamoDBClientOptionsBuilder opts = + LDServerDynamoDBClientOptionsBuilder_New(); + LDServerDynamoDBClientOptionsBuilder_Region(opts, LocalRegion().c_str()); + LDServerDynamoDBClientOptionsBuilder_Endpoint(opts, + LocalEndpoint().c_str()); + LDServerDynamoDBClientOptionsBuilder_AccessKeyId(opts, "dummy"); + + LDServerLazyLoadDynamoDBResult result; + ASSERT_FALSE( + LDServerLazyLoadDynamoDBSource_New("any-table", "foo", opts, &result)); + ASSERT_EQ(result.source, nullptr); + EXPECT_GT(std::strlen(result.error_message), 0u); +} + // End-to-end test that uses an actual DynamoDB (Local) instance with // provisioned flag data. The source is passed into the SDK's LazyLoad data // system, and AllFlags is used to verify that the data is read back from