Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need stdbool.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to note that the error massages may contain things like query parameters and you may want to consider sanitizing them?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/** @file redis_big_segment_store.h
* @brief LaunchDarkly Server-side Redis Big Segments Store C Binding.
*/
// NOLINTBEGIN modernize-use-using
#pragma once

#include <launchdarkly/bindings/c/export.h>

#include <stdbool.h>

#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 <a
* href="https://github.com/sewenew/redis-plus-plus">Redis++</a>, a C++ wrapper
* for the <a href="https://github.com/redis/hiredis">hiredis</a> 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.
*
* 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;
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
1 change: 1 addition & 0 deletions libs/server-sdk-redis-source/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#include <launchdarkly/server_side/bindings/c/integrations/redis/redis_big_segment_store.h>

Check failure on line 1 in libs/server-sdk-redis-source/src/bindings/redis/redis_big_segment_store.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk-redis-source/src/bindings/redis/redis_big_segment_store.cpp:1:10 [clang-diagnostic-error]

'launchdarkly/server_side/bindings/c/integrations/redis/redis_big_segment_store.h' file not found

#include <launchdarkly/server_side/integrations/redis/redis_big_segment_store.hpp>

#include <launchdarkly/detail/c_binding_helpers.hpp>

#include <cstring>

using namespace launchdarkly::server_side::integrations;

LD_EXPORT(bool)
LDServerBigSegmentsRedisStore_New(char const* uri,

Check warning on line 12 in libs/server-sdk-redis-source/src/bindings/redis/redis_big_segment_store.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk-redis-source/src/bindings/redis/redis_big_segment_store.cpp:12:35 [bugprone-easily-swappable-parameters]

2 adjacent parameters of 'LDServerBigSegmentsRedisStore_New' of similar type ('const char *') are easily swapped by mistake
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<LDServerBigSegmentsRedisStore>(maybe_store->release());

return true;
}

LD_EXPORT(void)
LDServerBigSegmentsRedisStore_Free(LDServerBigSegmentsRedisStore store) {
delete reinterpret_cast<RedisBigSegmentStore*>(store);
}
106 changes: 104 additions & 2 deletions libs/server-sdk-redis-source/tests/c_bindings_test.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <gtest/gtest.h>

#include <launchdarkly/server_side/bindings/c/integrations/redis/redis_big_segment_store.h>

Check failure on line 3 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk-redis-source/tests/c_bindings_test.cpp:3:10 [clang-diagnostic-error]

'launchdarkly/server_side/bindings/c/integrations/redis/redis_big_segment_store.h' file not found
#include <launchdarkly/server_side/bindings/c/integrations/redis/redis_source.h>

#include <launchdarkly/bindings/c/context_builder.h>
Expand All @@ -10,10 +11,14 @@

#include "prefixed_redis_client.hpp"

#include <chrono>
#include <condition_variable>
#include <mutex>

using namespace launchdarkly::data_model;

TEST(RedisBindings, SourcePointerIsStoredOnSuccessfulCreation) {
LDServerLazyLoadRedisResult result;

Check warning on line 21 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk-redis-source/tests/c_bindings_test.cpp:21:33 [cppcoreguidelines-init-variables]

variable 'result' is not initialized
ASSERT_TRUE(LDServerLazyLoadRedisSource_New("tcp://localhost:1234", "foo",
&result));
ASSERT_NE(result.source, nullptr);
Expand All @@ -21,7 +26,7 @@
}

TEST(RedisBindings, ErrorMessageIsPropagatedOnFailure) {
LDServerLazyLoadRedisResult result;

Check warning on line 29 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk-redis-source/tests/c_bindings_test.cpp:29:33 [cppcoreguidelines-init-variables]

variable 'result' is not initialized
ASSERT_FALSE(
LDServerLazyLoadRedisSource_New("totally not a URI", "foo", &result));
// Note: this test might begin failing if the Redis++ library ever returns
Expand All @@ -31,7 +36,7 @@
}

TEST(RedisBindings, SourcePointerIsNullptrOnFailure) {
LDServerLazyLoadRedisResult result;

Check warning on line 39 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk-redis-source/tests/c_bindings_test.cpp:39:33 [cppcoreguidelines-init-variables]

variable 'result' is not initialized
ASSERT_FALSE(
LDServerLazyLoadRedisSource_New("totally not a URI", "foo", &result));
ASSERT_EQ(result.source, nullptr);
Expand All @@ -55,7 +60,7 @@
client.PutFlag(flag_b);
client.Init();

LDServerLazyLoadRedisResult result;

Check warning on line 63 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk-redis-source/tests/c_bindings_test.cpp:63:33 [cppcoreguidelines-init-variables]

variable 'result' is not initialized
ASSERT_TRUE(LDServerLazyLoadRedisSource_New("tcp://localhost:6379",
"testprefix", &result));

Expand All @@ -64,13 +69,13 @@
LDServerLazyLoadBuilder lazy_builder = LDServerLazyLoadBuilder_New();
LDServerLazyLoadBuilder_SourcePtr(
lazy_builder,
reinterpret_cast<LDServerLazyLoadSourcePtr>(result.source));

Check warning on line 72 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk-redis-source/tests/c_bindings_test.cpp:72:9 [cppcoreguidelines-pro-type-reinterpret-cast]

do not use reinterpret_cast
LDServerConfigBuilder_DataSystem_LazyLoad(cfg_builder, lazy_builder);
LDServerConfigBuilder_Events_Enabled(
cfg_builder, false); // Don't want outbound connection to
// LD in test.

LDServerConfig config;

Check warning on line 78 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk-redis-source/tests/c_bindings_test.cpp:78:20 [cppcoreguidelines-init-variables]

variable 'config' is not initialized
LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config);
ASSERT_TRUE(LDStatus_Ok(status));

Expand All @@ -89,13 +94,13 @@

ASSERT_EQ(LDValue_Type(all), LDValueType_Object);

std::unordered_map<std::string, launchdarkly::Value> values;

Check warning on line 97 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk-redis-source/tests/c_bindings_test.cpp:97:58 [cppcoreguidelines-init-variables]

variable 'values' is not initialized
LDValue_ObjectIter iter;

Check warning on line 98 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk-redis-source/tests/c_bindings_test.cpp:98:24 [cppcoreguidelines-init-variables]

variable 'iter' is not initialized
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<launchdarkly::Value const* const>(

Check warning on line 103 in libs/server-sdk-redis-source/tests/c_bindings_test.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk-redis-source/tests/c_bindings_test.cpp:103:9 [readability-qualified-auto]

'auto value_ref' can be declared as 'const auto *value_ref'
LDValue_ObjectIter_Value(iter));
values.emplace(key, *value_ref);
}
Expand All @@ -112,3 +117,100 @@
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::milliseconds>(
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<LDServerBigSegmentStorePtr>(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<ListenerCtx*>(user_data);
{
std::lock_guard<std::mutex> 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<std::mutex> lk(mu);
cv.wait_for(lk, std::chrono::seconds(3), [&] { return available; });
}

EXPECT_TRUE(available);

LDListenerConnection_Disconnect(connection);
LDListenerConnection_Free(connection);
LDServerSDK_Free(sdk);
}
Loading