From 538459c877779016a9cece92ef42575836a7c58d Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Wed, 1 Jul 2026 17:01:51 -0700 Subject: [PATCH] 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 315b1d166..c6dbe51a4 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,8 +18,11 @@ #include #include +#include +#include #include #include +#include #include using namespace launchdarkly::data_model; @@ -174,3 +178,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); +}