Skip to content

Commit fc17043

Browse files
committed
feat: add IBigSegmentStore interface + Redis and DynamoDB stores
1 parent a0c2790 commit fc17043

15 files changed

Lines changed: 1022 additions & 8 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/** @file dynamodb_big_segment_store.hpp
2+
* @brief Server-Side DynamoDB Big Segments Store
3+
*/
4+
5+
#pragma once
6+
7+
#include <launchdarkly/server_side/integrations/big_segments/ibig_segment_store.hpp>
8+
#include <launchdarkly/server_side/integrations/dynamodb/options.hpp>
9+
10+
#include <tl/expected.hpp>
11+
12+
#include <memory>
13+
#include <string>
14+
15+
namespace Aws::DynamoDB {
16+
class DynamoDBClient;
17+
}
18+
19+
namespace launchdarkly::server_side::integrations {
20+
21+
/**
22+
* @brief DynamoDBBigSegmentStore is a Big Segments persistent store backed by
23+
* Amazon DynamoDB.
24+
*
25+
* Call DynamoDBBigSegmentStore::Create to obtain a new instance, then pass it
26+
* to the SDK via the Big Segments config builder.
27+
*
28+
* The DynamoDB table must already exist and follow the LaunchDarkly schema:
29+
* a String partition key named `namespace` and a String sort key named `key`.
30+
* The same table can be shared with @ref DynamoDBDataSource — Big Segments
31+
* rows occupy their own partition-key values and do not conflict with
32+
* flag/segment rows. The LaunchDarkly Relay Proxy is responsible for
33+
* populating Big Segments data in this table; this class only reads from it.
34+
*
35+
* This implementation is backed by the AWS SDK for C++.
36+
*/
37+
class DynamoDBBigSegmentStore final : public IBigSegmentStore {
38+
public:
39+
/**
40+
* @brief Creates a new DynamoDBBigSegmentStore, or returns an error if
41+
* construction failed.
42+
*
43+
* @param table_name Name of the DynamoDB table to read from. The table
44+
* must already exist; this class does not create it.
45+
*
46+
* @param prefix Optional namespace prefix. When non-empty, Big Segments
47+
* rows live under partition keys `<prefix>:big_segments_user` and
48+
* `<prefix>:big_segments_metadata`. This allows multiple LaunchDarkly
49+
* environments to share a single table.
50+
*
51+
* @param options Optional AWS DynamoDB client configuration. See
52+
* @ref DynamoDBClientOptions. When defaulted, the AWS SDK resolves
53+
* region, endpoint, and credentials from the standard provider chain
54+
* (environment variables, shared config files, instance metadata).
55+
*
56+
* @return A DynamoDBBigSegmentStore, or an error if construction failed.
57+
*/
58+
static tl::expected<std::unique_ptr<DynamoDBBigSegmentStore>, std::string>
59+
Create(std::string table_name,
60+
std::string prefix,
61+
DynamoDBClientOptions options = {});
62+
63+
[[nodiscard]] GetMembershipResult GetMembership(
64+
std::string const& context_hash) const override;
65+
[[nodiscard]] GetMetadataResult GetMetadata() const override;
66+
67+
~DynamoDBBigSegmentStore() override;
68+
69+
private:
70+
DynamoDBBigSegmentStore(
71+
std::unique_ptr<Aws::DynamoDB::DynamoDBClient> client,
72+
std::string table_name,
73+
std::string prefix);
74+
75+
std::unique_ptr<Aws::DynamoDB::DynamoDBClient> client_;
76+
std::string const table_name_;
77+
std::string const prefix_;
78+
std::string const user_namespace_;
79+
std::string const metadata_namespace_;
80+
};
81+
82+
} // namespace launchdarkly::server_side::integrations

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ target_sources(${LIBNAME}
1414
PRIVATE
1515
${HEADER_LIST}
1616
dynamodb_source.cpp
17+
dynamodb_big_segment_store.cpp
1718
aws_sdk_guard.cpp
1819
client_factory.cpp
1920
)

libs/server-sdk-dynamodb-source/src/dynamodb_attributes.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,16 @@ inline constexpr char kItemAttribute[] = "item";
1818
// {namespace: "myprefix:$inited", key: "myprefix:$inited"}.
1919
inline constexpr char kInitedNamespace[] = "$inited";
2020

21+
// Big Segments schema. Membership rows use partition key
22+
// "{prefix}:big_segments_user" and sort key {context_hash}, with
23+
// "included" / "excluded" String Set attributes naming segment refs. The
24+
// metadata row uses partition key AND sort key both set to
25+
// "{prefix}:big_segments_metadata", with the sync timestamp stored as a
26+
// Number under "synchronizedOn".
27+
inline constexpr char kBigSegmentsUserNamespace[] = "big_segments_user";
28+
inline constexpr char kBigSegmentsMetadataNamespace[] = "big_segments_metadata";
29+
inline constexpr char kBigSegmentsIncludedAttribute[] = "included";
30+
inline constexpr char kBigSegmentsExcludedAttribute[] = "excluded";
31+
inline constexpr char kBigSegmentsSyncTimeAttribute[] = "synchronizedOn";
32+
2133
} // namespace launchdarkly::server_side::integrations::detail
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
#include <launchdarkly/server_side/integrations/dynamodb/dynamodb_big_segment_store.hpp>
2+
3+
#include "aws_sdk_guard.hpp"
4+
#include "client_factory.hpp"
5+
#include "dynamodb_attributes.hpp"
6+
#include "prefix.hpp"
7+
8+
#include <aws/core/utils/Outcome.h>
9+
#include <aws/dynamodb/DynamoDBClient.h>
10+
#include <aws/dynamodb/model/AttributeValue.h>
11+
#include <aws/dynamodb/model/GetItemRequest.h>
12+
13+
#include <cerrno>
14+
#include <cstdint>
15+
#include <cstdlib>
16+
#include <exception>
17+
#include <utility>
18+
19+
namespace launchdarkly::server_side::integrations {
20+
21+
namespace {
22+
23+
using detail::kBigSegmentsExcludedAttribute;
24+
using detail::kBigSegmentsIncludedAttribute;
25+
using detail::kBigSegmentsMetadataNamespace;
26+
using detail::kBigSegmentsSyncTimeAttribute;
27+
using detail::kBigSegmentsUserNamespace;
28+
using detail::kPartitionKey;
29+
using detail::kSortKey;
30+
using detail::PrefixedNamespace;
31+
32+
} // namespace
33+
34+
tl::expected<std::unique_ptr<DynamoDBBigSegmentStore>, std::string>
35+
DynamoDBBigSegmentStore::Create(std::string table_name,
36+
std::string prefix,
37+
DynamoDBClientOptions options) {
38+
try {
39+
detail::AwsSdkGuard::Ensure();
40+
auto maybe_client = detail::BuildDynamoDBClient(options);
41+
if (!maybe_client) {
42+
return tl::make_unexpected(std::move(maybe_client.error()));
43+
}
44+
return std::unique_ptr<DynamoDBBigSegmentStore>(
45+
new DynamoDBBigSegmentStore(std::move(*maybe_client),
46+
std::move(table_name),
47+
std::move(prefix)));
48+
} catch (std::exception const& e) {
49+
return tl::make_unexpected(e.what());
50+
}
51+
}
52+
53+
DynamoDBBigSegmentStore::DynamoDBBigSegmentStore(
54+
std::unique_ptr<Aws::DynamoDB::DynamoDBClient> client,
55+
std::string table_name,
56+
std::string prefix)
57+
: client_(std::move(client)),
58+
table_name_(std::move(table_name)),
59+
prefix_(std::move(prefix)),
60+
user_namespace_(PrefixedNamespace(prefix_, kBigSegmentsUserNamespace)),
61+
metadata_namespace_(
62+
PrefixedNamespace(prefix_, kBigSegmentsMetadataNamespace)) {}
63+
64+
DynamoDBBigSegmentStore::~DynamoDBBigSegmentStore() = default;
65+
66+
IBigSegmentStore::GetMembershipResult DynamoDBBigSegmentStore::GetMembership(
67+
std::string const& context_hash) const {
68+
Aws::DynamoDB::Model::GetItemRequest request;
69+
request.SetTableName(table_name_);
70+
request.SetConsistentRead(true);
71+
request.AddKey(kPartitionKey,
72+
Aws::DynamoDB::Model::AttributeValue{user_namespace_});
73+
request.AddKey(kSortKey,
74+
Aws::DynamoDB::Model::AttributeValue{context_hash});
75+
76+
auto outcome = client_->GetItem(request);
77+
if (!outcome.IsSuccess()) {
78+
return tl::make_unexpected(outcome.GetError().GetMessage());
79+
}
80+
81+
auto const& item = outcome.GetResult().GetItem();
82+
if (item.empty()) {
83+
return std::nullopt;
84+
}
85+
86+
std::vector<std::string> included;
87+
std::vector<std::string> excluded;
88+
89+
if (auto const it = item.find(kBigSegmentsIncludedAttribute);
90+
it != item.end()) {
91+
for (auto const& ref : it->second.GetSS()) {
92+
included.emplace_back(ref);
93+
}
94+
}
95+
if (auto const it = item.find(kBigSegmentsExcludedAttribute);
96+
it != item.end()) {
97+
for (auto const& ref : it->second.GetSS()) {
98+
excluded.emplace_back(ref);
99+
}
100+
}
101+
102+
return Membership::FromSegmentRefs(included, excluded);
103+
}
104+
105+
IBigSegmentStore::GetMetadataResult DynamoDBBigSegmentStore::GetMetadata()
106+
const {
107+
Aws::DynamoDB::Model::GetItemRequest request;
108+
request.SetTableName(table_name_);
109+
request.SetConsistentRead(true);
110+
request.AddKey(kPartitionKey,
111+
Aws::DynamoDB::Model::AttributeValue{metadata_namespace_});
112+
request.AddKey(kSortKey,
113+
Aws::DynamoDB::Model::AttributeValue{metadata_namespace_});
114+
115+
auto outcome = client_->GetItem(request);
116+
if (!outcome.IsSuccess()) {
117+
return tl::make_unexpected(outcome.GetError().GetMessage());
118+
}
119+
120+
auto const& item = outcome.GetResult().GetItem();
121+
if (item.empty()) {
122+
return std::nullopt;
123+
}
124+
125+
auto const it = item.find(kBigSegmentsSyncTimeAttribute);
126+
if (it == item.end()) {
127+
return tl::make_unexpected(
128+
"DynamoDB Big Segments metadata row missing 'synchronizedOn'");
129+
}
130+
131+
auto const& raw = it->second.GetN();
132+
if (raw.empty()) {
133+
return tl::make_unexpected(
134+
"DynamoDB Big Segments 'synchronizedOn' is empty or not type N");
135+
}
136+
137+
errno = 0;
138+
char* end = nullptr;
139+
long long const parsed = std::strtoll(raw.c_str(), &end, 10);
140+
if (errno != 0 || end == raw.c_str() || *end != '\0') {
141+
return tl::make_unexpected(
142+
"DynamoDB Big Segments 'synchronizedOn' is not a valid integer");
143+
}
144+
145+
return StoreMetadata{std::chrono::milliseconds{parsed}};
146+
}
147+
148+
} // namespace launchdarkly::server_side::integrations

0 commit comments

Comments
 (0)