Skip to content

Commit c0acca8

Browse files
committed
feat: Add C bindings for Redis Big Segments store
1 parent 2f888ea commit c0acca8

4 files changed

Lines changed: 274 additions & 2 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/** @file redis_big_segment_store.h
2+
* @brief LaunchDarkly Server-side Redis Big Segments Store C Binding.
3+
*/
4+
// NOLINTBEGIN modernize-use-using
5+
#pragma once
6+
7+
#include <launchdarkly/bindings/c/export.h>
8+
9+
#ifdef __cplusplus
10+
extern "C" {
11+
// only need to export C interface if
12+
// used by C++ source code
13+
#endif
14+
15+
/**
16+
* @brief LDServerBigSegmentsRedisStore is a Big Segments persistent store for
17+
* the Server-Side SDK backed by Redis. It is meant to be passed to the SDK
18+
* via the Big Segments config builder.
19+
*
20+
* Call @ref LDServerBigSegmentsRedisStore_New to obtain a new instance. This
21+
* instance is passed into the SDK's Big Segments configuration.
22+
*
23+
* Example:
24+
* @code
25+
* // Stack allocate the result struct, which will hold the result pointer or
26+
* // an error message.
27+
* struct LDServerBigSegmentsRedisResult result;
28+
*
29+
* // Create the Redis Big Segment store, passing in arguments for the URI,
30+
* // prefix, and pointer to the result.
31+
* if (!LDServerBigSegmentsRedisStore_New("redis://localhost:6379",
32+
* "testprefix", &result)) {
33+
* // On failure, you may print the error message (result.error_message),
34+
* // then exit or return.
35+
* }
36+
*
37+
* // Create the Big Segments builder, taking ownership of the store pointer.
38+
* LDServerBigSegmentsBuilder bs_builder = LDServerBigSegmentsBuilder_New(
39+
* (LDServerBigSegmentStorePtr)result.store);
40+
*
41+
* // Attach the Big Segments builder to the SDK config.
42+
* LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123");
43+
* LDServerConfigBuilder_BigSegments(cfg_builder, bs_builder);
44+
* @endcode
45+
*
46+
* This implementation is backed by <a
47+
* href="https://github.com/sewenew/redis-plus-plus">Redis++</a>, a C++ wrapper
48+
* for the <a href="https://github.com/redis/hiredis">hiredis</a> library.
49+
*/
50+
typedef struct _LDServerBigSegmentsRedisStore* LDServerBigSegmentsRedisStore;
51+
52+
/* Defines the size of the error message buffer in
53+
* LDServerBigSegmentsRedisResult.
54+
*/
55+
#ifndef LDSERVER_BIGSEGMENTS_REDISSTORE_ERROR_MESSAGE_SIZE
56+
#define LDSERVER_BIGSEGMENTS_REDISSTORE_ERROR_MESSAGE_SIZE 256
57+
#endif
58+
59+
/**
60+
* @brief Stores the result of calling @ref LDServerBigSegmentsRedisStore_New.
61+
*
62+
* On successful creation, store will contain a pointer which may be passed
63+
* into the LaunchDarkly SDK's Big Segments configuration.
64+
*
65+
* On failure, error_message contains a NULL-terminated string describing the
66+
* error.
67+
*
68+
* The message may be truncated if it was originally longer than
69+
* error_message's buffer size.
70+
*/
71+
struct LDServerBigSegmentsRedisResult {
72+
LDServerBigSegmentsRedisStore store;
73+
char error_message[LDSERVER_BIGSEGMENTS_REDISSTORE_ERROR_MESSAGE_SIZE];
74+
};
75+
76+
/**
77+
* @brief Creates a new Redis Big Segment store suitable for usage in the SDK's
78+
* Big Segments configuration.
79+
*
80+
* In this system, the SDK will query Redis for Big Segments membership
81+
* as required, with an in-memory cache to reduce the number of queries.
82+
*
83+
* Data is never written back to Redis by the SDK; the LaunchDarkly Relay
84+
* Proxy populates the store.
85+
*
86+
* @param uri Redis URI string. Must not be NULL or empty string.
87+
*
88+
* @param prefix Prefix to use when reading SDK data from Redis. This allows
89+
* multiple SDK environments to coexist in the same database, or for the SDK's
90+
* data to coexist with other unrelated data. Must not be NULL.
91+
*
92+
* @param out_result Pointer to struct where the store pointer or an error
93+
* message should be stored.
94+
*
95+
* @return True if the store was created successfully; out_result->store
96+
* will contain the pointer. The caller must either free the pointer with
97+
* @ref LDServerBigSegmentsRedisStore_Free, OR pass it into the SDK's Big
98+
* Segments configuration which will take ownership (in which case do not
99+
* call @ref LDServerBigSegmentsRedisStore_Free.)
100+
*/
101+
LD_EXPORT(bool)
102+
LDServerBigSegmentsRedisStore_New(
103+
char const* uri,
104+
char const* prefix,
105+
struct LDServerBigSegmentsRedisResult* out_result);
106+
107+
/**
108+
* @brief Frees a Redis Big Segment store pointer. Only necessary to call if
109+
* not passing ownership to the SDK's Big Segments configuration.
110+
*/
111+
LD_EXPORT(void)
112+
LDServerBigSegmentsRedisStore_Free(LDServerBigSegmentsRedisStore store);
113+
114+
#ifdef __cplusplus
115+
}
116+
#endif
117+
118+
// NOLINTEND modernize-use-using

libs/server-sdk-redis-source/src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ target_sources(${LIBNAME}
1616
redis_source.cpp
1717
redis_big_segment_store.cpp
1818
bindings/redis/redis_source.cpp
19+
bindings/redis/redis_big_segment_store.cpp
1920
)
2021

2122

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#include <launchdarkly/server_side/bindings/c/integrations/redis/redis_big_segment_store.h>
2+
3+
#include <launchdarkly/server_side/integrations/redis/redis_big_segment_store.hpp>
4+
5+
#include <launchdarkly/detail/c_binding_helpers.hpp>
6+
7+
#include <cstring>
8+
9+
using namespace launchdarkly::server_side::integrations;
10+
11+
LD_EXPORT(bool)
12+
LDServerBigSegmentsRedisStore_New(char const* uri,
13+
char const* prefix,
14+
LDServerBigSegmentsRedisResult* out_result) {
15+
LD_ASSERT_NOT_NULL(uri);
16+
LD_ASSERT_NOT_NULL(prefix);
17+
LD_ASSERT_NOT_NULL(out_result);
18+
19+
// Explicitely zero out the exception_msg buffer in case the exception is
20+
// shorter than the buffer.
21+
memset(out_result->error_message, 0,
22+
sizeof(LDServerBigSegmentsRedisResult::error_message));
23+
24+
// Ensure the store pointer isn't garbage.
25+
out_result->store = nullptr;
26+
27+
auto maybe_store = RedisBigSegmentStore::Create(uri, prefix);
28+
if (!maybe_store) {
29+
// Avoid heap allocating another string to pass back to the caller;
30+
// instead, we copy into the buffer and ensure a terminator is present.
31+
// This does mean the message may be truncated.
32+
33+
std::size_t const len = maybe_store.error().copy(
34+
out_result->error_message, sizeof(out_result->error_message) - 1);
35+
out_result->error_message[len] = '\0';
36+
37+
return false;
38+
}
39+
40+
// The pointer is no longer managed and must either be freed by the caller,
41+
// or passed into the SDK which will take ownership.
42+
out_result->store =
43+
reinterpret_cast<LDServerBigSegmentsRedisStore>(maybe_store->release());
44+
45+
return true;
46+
}
47+
48+
LD_EXPORT(void)
49+
LDServerBigSegmentsRedisStore_Free(LDServerBigSegmentsRedisStore store) {
50+
delete reinterpret_cast<RedisBigSegmentStore*>(store);
51+
}

libs/server-sdk-redis-source/tests/c_bindings_test.cpp

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include <gtest/gtest.h>
22

3+
#include <launchdarkly/server_side/bindings/c/integrations/redis/redis_big_segment_store.h>
34
#include <launchdarkly/server_side/bindings/c/integrations/redis/redis_source.h>
45

56
#include <launchdarkly/bindings/c/context_builder.h>
@@ -10,6 +11,10 @@
1011

1112
#include "prefixed_redis_client.hpp"
1213

14+
#include <chrono>
15+
#include <condition_variable>
16+
#include <mutex>
17+
1318
using namespace launchdarkly::data_model;
1419

1520
TEST(RedisBindings, SourcePointerIsStoredOnSuccessfulCreation) {
@@ -91,8 +96,8 @@ TEST(RedisBindings, CanUseInSDKLazyLoadDataSource) {
9196

9297
std::unordered_map<std::string, launchdarkly::Value> values;
9398
LDValue_ObjectIter iter;
94-
for (iter = LDValue_ObjectIter_New(all);
95-
!LDValue_ObjectIter_End(iter); LDValue_ObjectIter_Next(iter)) {
99+
for (iter = LDValue_ObjectIter_New(all); !LDValue_ObjectIter_End(iter);
100+
LDValue_ObjectIter_Next(iter)) {
96101
char const* key = LDValue_ObjectIter_Key(iter);
97102
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
98103
auto value_ref = reinterpret_cast<launchdarkly::Value const* const>(
@@ -112,3 +117,100 @@ TEST(RedisBindings, CanUseInSDKLazyLoadDataSource) {
112117
LDContext_Free(context);
113118
LDServerSDK_Free(sdk);
114119
}
120+
121+
TEST(RedisBindings, BigSegmentsStorePointerIsStoredOnSuccessfulCreation) {
122+
LDServerBigSegmentsRedisResult result;
123+
ASSERT_TRUE(LDServerBigSegmentsRedisStore_New("tcp://localhost:1234", "foo",
124+
&result));
125+
ASSERT_NE(result.store, nullptr);
126+
LDServerBigSegmentsRedisStore_Free(result.store);
127+
}
128+
129+
TEST(RedisBindings, BigSegmentsErrorMessageIsPropagatedOnFailure) {
130+
LDServerBigSegmentsRedisResult result;
131+
ASSERT_FALSE(
132+
LDServerBigSegmentsRedisStore_New("totally not a URI", "foo", &result));
133+
ASSERT_STRNE(result.error_message, "");
134+
}
135+
136+
TEST(RedisBindings, BigSegmentsStorePointerIsNullptrOnFailure) {
137+
LDServerBigSegmentsRedisResult result;
138+
ASSERT_FALSE(
139+
LDServerBigSegmentsRedisStore_New("totally not a URI", "foo", &result));
140+
ASSERT_EQ(result.store, nullptr);
141+
}
142+
143+
// This is an end-to-end test that uses an actual Redis instance with
144+
// provisioned Big Segments metadata. The store is passed into the SDK's Big
145+
// Segments configuration, and the SDK's Big Segment store status listener is
146+
// used to verify that the store is reachable and reports available.
147+
TEST(RedisBindings, CanUseInSDKBigSegmentsConfig) {
148+
sw::redis::Redis redis("tcp://localhost:6379");
149+
redis.flushdb();
150+
151+
// Set the store's sync timestamp so the poll reports available.
152+
auto const now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
153+
std::chrono::system_clock::now().time_since_epoch())
154+
.count();
155+
redis.set("testprefix:big_segments_synchronized_on",
156+
std::to_string(now_ms));
157+
158+
LDServerBigSegmentsRedisResult result;
159+
ASSERT_TRUE(LDServerBigSegmentsRedisStore_New("tcp://localhost:6379",
160+
"testprefix", &result));
161+
162+
LDServerBigSegmentsBuilder bs_builder = LDServerBigSegmentsBuilder_New(
163+
reinterpret_cast<LDServerBigSegmentStorePtr>(result.store));
164+
LDServerBigSegmentsBuilder_StatusPollIntervalMs(bs_builder, 50);
165+
166+
LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123");
167+
LDServerConfigBuilder_BigSegments(cfg_builder, bs_builder);
168+
LDServerConfigBuilder_Offline(cfg_builder, true);
169+
170+
LDServerConfig config;
171+
LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config);
172+
ASSERT_TRUE(LDStatus_Ok(status));
173+
174+
LDServerSDK sdk = LDServerSDK_New(config);
175+
176+
std::mutex mu;
177+
std::condition_variable cv;
178+
bool available = false;
179+
180+
struct ListenerCtx {
181+
std::mutex* mu;
182+
std::condition_variable* cv;
183+
bool* available;
184+
};
185+
ListenerCtx ctx{&mu, &cv, &available};
186+
187+
struct LDServerBigSegmentStoreStatusListener listener{};
188+
LDServerBigSegmentStoreStatusListener_Init(&listener);
189+
listener.UserData = &ctx;
190+
listener.StatusChanged =
191+
+[](LDServerBigSegmentStoreStatus s, void* user_data) {
192+
auto* c = static_cast<ListenerCtx*>(user_data);
193+
{
194+
std::lock_guard<std::mutex> lk(*c->mu);
195+
*c->available = LDServerBigSegmentStoreStatus_Available(s);
196+
}
197+
c->cv->notify_all();
198+
};
199+
200+
LDListenerConnection connection =
201+
LDServerSDK_BigSegmentStoreStatus_OnStatusChange(sdk, listener);
202+
ASSERT_NE(connection, nullptr);
203+
204+
LDServerSDK_Start(sdk, LD_NONBLOCKING, nullptr);
205+
206+
{
207+
std::unique_lock<std::mutex> lk(mu);
208+
cv.wait_for(lk, std::chrono::seconds(3), [&] { return available; });
209+
}
210+
211+
EXPECT_TRUE(available);
212+
213+
LDListenerConnection_Disconnect(connection);
214+
LDListenerConnection_Free(connection);
215+
LDServerSDK_Free(sdk);
216+
}

0 commit comments

Comments
 (0)