From d9a33a23ecc70146acb9aa55ac7911e3d2689b62 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Wed, 1 Jul 2026 17:01:51 -0700 Subject: [PATCH 1/3] feat: Add C bindings for DynamoDB Big Segments store --- .../dynamodb/dynamodb_big_segment_store.h | 131 ++++++++++++++++++ .../src/CMakeLists.txt | 1 + .../dynamodb/dynamodb_big_segment_store.cpp | 60 ++++++++ .../tests/c_bindings_test.cpp | 101 ++++++++++++++ 4 files changed, 293 insertions(+) create mode 100644 libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_big_segment_store.h create mode 100644 libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_big_segment_store.cpp diff --git a/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_big_segment_store.h b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_big_segment_store.h new file mode 100644 index 000000000..59d676a3a --- /dev/null +++ b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_big_segment_store.h @@ -0,0 +1,131 @@ +/** @file dynamodb_big_segment_store.h + * @brief LaunchDarkly Server-side DynamoDB Big Segments Store 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 LDServerBigSegmentsDynamoDBStore is a Big Segments persistent store + * for the Server-Side SDK backed by Amazon DynamoDB. It is meant to be passed + * to the SDK via the Big Segments config builder. + * + * Call @ref LDServerBigSegmentsDynamoDBStore_New to obtain a new instance. + * This instance is passed into the SDK's Big Segments configuration. + * + * 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 same table can be shared with @ref LDServerLazyLoadDynamoDBSource + * -- Big Segments rows occupy their own partition-key values and do not + * conflict with flag/segment rows. The LaunchDarkly Relay Proxy populates + * Big Segments data in the table; this store 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 LDServerBigSegmentsDynamoDBResult result; + * + * if (!LDServerBigSegmentsDynamoDBStore_New("my-table", "testprefix", options, + * &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 the AWS SDK for C++. + */ +typedef struct _LDServerBigSegmentsDynamoDBStore* + LDServerBigSegmentsDynamoDBStore; + +/* Defines the size of the error message buffer in + * LDServerBigSegmentsDynamoDBResult. + */ +#ifndef LDSERVER_BIGSEGMENTS_DYNAMODBSTORE_ERROR_MESSAGE_SIZE +#define LDSERVER_BIGSEGMENTS_DYNAMODBSTORE_ERROR_MESSAGE_SIZE 256 +#endif + +/** + * @brief Stores the result of calling @ref LDServerBigSegmentsDynamoDBStore_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 LDServerBigSegmentsDynamoDBResult { + LDServerBigSegmentsDynamoDBStore store; + char error_message[LDSERVER_BIGSEGMENTS_DYNAMODBSTORE_ERROR_MESSAGE_SIZE]; +}; + +/** + * @brief Creates a new DynamoDB Big Segment store suitable for usage in the + * SDK's Big Segments configuration. + * + * @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 Big Segments 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 store pointer or an error + * message should be stored. Must not be NULL. + * + * @return True if the store was created successfully; out_result->store + * will contain the pointer. The caller must either free the pointer with + * @ref LDServerBigSegmentsDynamoDBStore_Free, OR pass it into the SDK's Big + * Segments configuration which will take ownership (in which case do not + * call @ref LDServerBigSegmentsDynamoDBStore_Free.) + */ +LD_EXPORT(bool) +LDServerBigSegmentsDynamoDBStore_New( + char const* table_name, + char const* prefix, + LDServerDynamoDBClientOptionsBuilder options, + struct LDServerBigSegmentsDynamoDBResult* out_result); + +/** + * @brief Frees a DynamoDB Big Segment store pointer. Only necessary to call + * if not passing ownership to the SDK's Big Segments configuration. + */ +LD_EXPORT(void) +LDServerBigSegmentsDynamoDBStore_Free(LDServerBigSegmentsDynamoDBStore store); + +#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 983bf2ea6..da013cf99 100644 --- a/libs/server-sdk-dynamodb-source/src/CMakeLists.txt +++ b/libs/server-sdk-dynamodb-source/src/CMakeLists.txt @@ -19,6 +19,7 @@ target_sources(${LIBNAME} client_factory.cpp bindings/dynamodb/client_options.cpp bindings/dynamodb/dynamodb_source.cpp + bindings/dynamodb/dynamodb_big_segment_store.cpp ) diff --git a/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_big_segment_store.cpp b/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_big_segment_store.cpp new file mode 100644 index 000000000..7e48584ec --- /dev/null +++ b/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_big_segment_store.cpp @@ -0,0 +1,60 @@ +#include + +#include +#include + +#include + +#include +#include + +using namespace launchdarkly::server_side::integrations; + +LD_EXPORT(bool) +LDServerBigSegmentsDynamoDBStore_New( + char const* table_name, + char const* prefix, + LDServerDynamoDBClientOptionsBuilder options, + LDServerBigSegmentsDynamoDBResult* 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(LDServerBigSegmentsDynamoDBResult::error_message)); + + // Ensure the store pointer isn't garbage. + out_result->store = nullptr; + + DynamoDBClientOptions opts{}; + if (options != nullptr) { + auto* opts_ptr = reinterpret_cast(options); + opts = *opts_ptr; + LDServerDynamoDBClientOptionsBuilder_Free(options); + } + + auto maybe_store = + DynamoDBBigSegmentStore::Create(table_name, prefix, std::move(opts)); + 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) +LDServerBigSegmentsDynamoDBStore_Free(LDServerBigSegmentsDynamoDBStore store) { + delete reinterpret_cast(store); +} 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 7ab42c3ba..118b4367f 100644 --- a/libs/server-sdk-dynamodb-source/tests/c_bindings_test.cpp +++ b/libs/server-sdk-dynamodb-source/tests/c_bindings_test.cpp @@ -1,5 +1,6 @@ #include +#include #include #include @@ -17,9 +18,12 @@ #include #include +#include +#include #include #include #include +#include #include using namespace launchdarkly::data_model; @@ -190,3 +194,100 @@ TEST(DynamoDBBindings, CanUseInSDKLazyLoadDataSource) { PrefixedDynamoDBClient::DeleteTable(*raw_client, table_name); } + +TEST(DynamoDBBindings, BigSegmentsStorePointerIsStoredOnSuccessfulCreation) { + LDServerBigSegmentsDynamoDBResult result; + ASSERT_TRUE(LDServerBigSegmentsDynamoDBStore_New( + "any-table", "foo", LocalOptionsBuilder(), &result)); + ASSERT_NE(result.store, nullptr); + LDServerBigSegmentsDynamoDBStore_Free(result.store); +} + +TEST(DynamoDBBindings, BigSegmentsStoreAcceptsNullOptions) { + LDServerBigSegmentsDynamoDBResult result; + ASSERT_TRUE(LDServerBigSegmentsDynamoDBStore_New("any-table", "foo", + nullptr, &result)); + ASSERT_NE(result.store, nullptr); + LDServerBigSegmentsDynamoDBStore_Free(result.store); +} + +// End-to-end test that uses an actual DynamoDB (Local) 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(DynamoDBBindings, CanUseInSDKBigSegmentsConfig) { + std::string const table_name = "ld-dynamodb-c-bindings-bs-test"; + std::string const prefix = "testprefix"; + + auto raw_client = MakeRawClient(); + PrefixedDynamoDBClient::DeleteTable(*raw_client, table_name); + PrefixedDynamoDBClient::CreateTable(*raw_client, table_name); + + // 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(); + PrefixedDynamoDBClient(*raw_client, prefix, table_name) + .PutBigSegmentSyncTime(now_ms); + + LDServerBigSegmentsDynamoDBResult result; + ASSERT_TRUE(LDServerBigSegmentsDynamoDBStore_New( + table_name.c_str(), prefix.c_str(), LocalOptionsBuilder(), &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); + + PrefixedDynamoDBClient::DeleteTable(*raw_client, table_name); +} From 204affdaafe74c27fd1977494974d3e404a77cd2 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 24 Jul 2026 14:06:18 -0700 Subject: [PATCH 2/3] docs: Address review comments on DynamoDB Big Segments bindings --- .../c/integrations/dynamodb/dynamodb_big_segment_store.h | 2 ++ .../src/bindings/dynamodb/dynamodb_big_segment_store.cpp | 2 +- .../src/bindings/dynamodb/dynamodb_source.cpp | 2 +- .../src/bindings/redis/redis_big_segment_store.cpp | 2 +- .../server-sdk-redis-source/src/bindings/redis/redis_source.cpp | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_big_segment_store.h b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_big_segment_store.h index 59d676a3a..57114ba30 100644 --- a/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_big_segment_store.h +++ b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_big_segment_store.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/src/bindings/dynamodb/dynamodb_big_segment_store.cpp b/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_big_segment_store.cpp index 7e48584ec..0ec8a0769 100644 --- a/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_big_segment_store.cpp +++ b/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_big_segment_store.cpp @@ -20,7 +20,7 @@ LDServerBigSegmentsDynamoDBStore_New( LD_ASSERT_NOT_NULL(prefix); LD_ASSERT_NOT_NULL(out_result); - // Explicitely zero out the error_message buffer in case the error is + // Explicitly zero out the error_message buffer in case the error is // shorter than the buffer. memset(out_result->error_message, 0, sizeof(LDServerBigSegmentsDynamoDBResult::error_message)); 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 index d0e752e0f..3b8873041 100644 --- a/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_source.cpp +++ b/libs/server-sdk-dynamodb-source/src/bindings/dynamodb/dynamodb_source.cpp @@ -19,7 +19,7 @@ LDServerLazyLoadDynamoDBSource_New(char const* 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 + // Explicitly 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)); 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 index 12b6c4117..3d399e5cf 100644 --- 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 @@ -16,7 +16,7 @@ LDServerBigSegmentsRedisStore_New(char const* uri, LD_ASSERT_NOT_NULL(prefix); LD_ASSERT_NOT_NULL(out_result); - // Explicitely zero out the exception_msg buffer in case the exception is + // Explicitly 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)); diff --git a/libs/server-sdk-redis-source/src/bindings/redis/redis_source.cpp b/libs/server-sdk-redis-source/src/bindings/redis/redis_source.cpp index 3ebd4b4a0..0bbfda389 100644 --- a/libs/server-sdk-redis-source/src/bindings/redis/redis_source.cpp +++ b/libs/server-sdk-redis-source/src/bindings/redis/redis_source.cpp @@ -17,7 +17,7 @@ LDServerLazyLoadRedisSource_New(char const* uri, LD_ASSERT_NOT_NULL(prefix); LD_ASSERT_NOT_NULL(out_result); - // Explicitely zero out the exception_msg buffer in case the exception is + // Explicitly zero out the exception_msg buffer in case the exception is // shorter than the buffer. memset(out_result->error_message, 0, sizeof(LDServerLazyLoadRedisResult::error_message)); From 8978c7730954d4d7816f672a5159b7c319a1103f Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 24 Jul 2026 14:46:39 -0700 Subject: [PATCH 3/3] docs: Address Cursor Bugbot comments on DynamoDB Big Segments bindings --- .../dynamodb/dynamodb_big_segment_store.h | 5 +++++ .../tests/c_bindings_test.cpp | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_big_segment_store.h b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_big_segment_store.h index 57114ba30..dd4169adc 100644 --- a/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_big_segment_store.h +++ b/libs/server-sdk-dynamodb-source/include/launchdarkly/server_side/bindings/c/integrations/dynamodb/dynamodb_big_segment_store.h @@ -80,6 +80,11 @@ typedef struct _LDServerBigSegmentsDynamoDBStore* * * The message may be truncated if it was originally longer than * error_message's buffer size. + * + * The message originates from the underlying AWS SDK and may echo back + * portions of the client configuration, including endpoint and region. + * Callers that surface this message (logs, telemetry, user-facing errors) + * may want to sanitize it accordingly. */ struct LDServerBigSegmentsDynamoDBResult { LDServerBigSegmentsDynamoDBStore store; 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 118b4367f..ee8f8c9f4 100644 --- a/libs/server-sdk-dynamodb-source/tests/c_bindings_test.cpp +++ b/libs/server-sdk-dynamodb-source/tests/c_bindings_test.cpp @@ -211,6 +211,21 @@ TEST(DynamoDBBindings, BigSegmentsStoreAcceptsNullOptions) { LDServerBigSegmentsDynamoDBStore_Free(result.store); } +TEST(DynamoDBBindings, BigSegmentsStoreRejectsPartialCredentials) { + LDServerDynamoDBClientOptionsBuilder opts = + LDServerDynamoDBClientOptionsBuilder_New(); + LDServerDynamoDBClientOptionsBuilder_Region(opts, LocalRegion().c_str()); + LDServerDynamoDBClientOptionsBuilder_Endpoint(opts, + LocalEndpoint().c_str()); + LDServerDynamoDBClientOptionsBuilder_AccessKeyId(opts, "dummy"); + + LDServerBigSegmentsDynamoDBResult result; + ASSERT_FALSE(LDServerBigSegmentsDynamoDBStore_New("any-table", "foo", opts, + &result)); + ASSERT_EQ(result.store, nullptr); + EXPECT_GT(std::strlen(result.error_message), 0u); +} + // End-to-end test that uses an actual DynamoDB (Local) 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