Skip to content

Commit 299e5da

Browse files
committed
Merge remote-tracking branch 'origin/main' into beeklimt/SDK-2551
2 parents 404e9a1 + 455dacb commit 299e5da

8 files changed

Lines changed: 235 additions & 6 deletions

File tree

contract-tests/data-model/include/data_model/data_model.hpp

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,21 @@ struct ConfigWrapper {
150150

151151
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigWrapper, name, version);
152152

153+
struct ConfigBigSegmentsParams {
154+
std::string callbackUri;
155+
std::optional<uint32_t> userCacheSize;
156+
std::optional<uint64_t> userCacheTimeMs;
157+
std::optional<uint64_t> statusPollIntervalMs;
158+
std::optional<uint64_t> staleAfterMs;
159+
};
160+
161+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigBigSegmentsParams,
162+
callbackUri,
163+
userCacheSize,
164+
userCacheTimeMs,
165+
statusPollIntervalMs,
166+
staleAfterMs);
167+
153168
struct ConfigParams {
154169
std::string credential;
155170
std::optional<uint32_t> startWaitTimeMs;
@@ -164,6 +179,7 @@ struct ConfigParams {
164179
std::optional<ConfigProxyParams> proxy;
165180
std::optional<ConfigHooksParams> hooks;
166181
std::optional<ConfigWrapper> wrapper;
182+
std::optional<ConfigBigSegmentsParams> bigSegments;
167183
};
168184

169185
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams,
@@ -179,7 +195,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams,
179195
tls,
180196
proxy,
181197
hooks,
182-
wrapper);
198+
wrapper,
199+
bigSegments);
183200

184201
struct ContextSingleParams {
185202
std::optional<std::string> kind;
@@ -328,6 +345,15 @@ struct IdentifyEventParams {
328345

329346
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(IdentifyEventParams, context);
330347

348+
struct BigSegmentStoreStatusResponse {
349+
bool available;
350+
bool stale;
351+
};
352+
353+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(BigSegmentStoreStatusResponse,
354+
available,
355+
stale);
356+
331357
enum class Command {
332358
Unknown = -1,
333359
EvaluateFlag,
@@ -336,7 +362,8 @@ enum class Command {
336362
CustomEvent,
337363
FlushEvents,
338364
ContextBuild,
339-
ContextConvert
365+
ContextConvert,
366+
GetBigSegmentStoreStatus
340367
};
341368

342369
NLOHMANN_JSON_SERIALIZE_ENUM(Command,
@@ -347,7 +374,9 @@ NLOHMANN_JSON_SERIALIZE_ENUM(Command,
347374
{Command::CustomEvent, "customEvent"},
348375
{Command::FlushEvents, "flushEvents"},
349376
{Command::ContextBuild, "contextBuild"},
350-
{Command::ContextConvert, "contextConvert"}});
377+
{Command::ContextConvert, "contextConvert"},
378+
{Command::GetBigSegmentStoreStatus,
379+
"getBigSegmentStoreStatus"}});
351380

352381
struct CommandParams {
353382
Command command;

contract-tests/server-contract-tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ add_executable(server-tests
1717
src/entity_manager.cpp
1818
src/client_entity.cpp
1919
src/contract_test_hook.cpp
20+
src/contract_test_big_segment_store.cpp
2021
)
2122

2223
target_link_libraries(server-tests PRIVATE

contract-tests/server-contract-tests/include/client_entity.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class ClientEntity {
2727

2828
tl::expected<nlohmann::json, std::string> Custom(CustomEventParams const&);
2929

30+
tl::expected<nlohmann::json, std::string> GetBigSegmentStoreStatus();
31+
3032
std::unique_ptr<launchdarkly::server_side::Client> client_;
3133
};
3234

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#pragma once
2+
3+
#include <launchdarkly/server_side/integrations/big_segments/ibig_segment_store.hpp>
4+
5+
#include <nlohmann/json.hpp>
6+
7+
#include <tl/expected.hpp>
8+
9+
#include <string>
10+
11+
/**
12+
* A Big Segment store that delegates membership and metadata lookups to the
13+
* contract-test harness over HTTP, mirroring how ContractTestHook posts to the
14+
* harness. Each lookup is a synchronous request/response (unlike the hook's
15+
* fire-and-forget) because IBigSegmentStore must return the result.
16+
*
17+
* Thread-safe: holds only the immutable callback URI and performs each request
18+
* on its own local io_context.
19+
*/
20+
class ContractTestBigSegmentStore
21+
: public launchdarkly::server_side::integrations::IBigSegmentStore {
22+
public:
23+
explicit ContractTestBigSegmentStore(std::string callback_uri);
24+
25+
[[nodiscard]] GetMembershipResult GetMembership(
26+
std::string const& context_hash) const noexcept override;
27+
28+
[[nodiscard]] GetMetadataResult GetMetadata() const noexcept override;
29+
30+
private:
31+
// Synchronous POST of `body` to `<callback_uri><path>`. Returns the parsed
32+
// JSON response on HTTP 200, or an error string (non-200 body, transport
33+
// failure, or parse failure).
34+
[[nodiscard]] tl::expected<nlohmann::json, std::string> Post(
35+
std::string const& path,
36+
nlohmann::json const& body) const noexcept;
37+
38+
std::string const callback_uri_;
39+
};

contract-tests/server-contract-tests/src/client_entity.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
#include <boost/json.hpp>
55

66
#include <launchdarkly/context_builder.hpp>
7-
#include <launchdarkly/serialization/json_context.hpp>
8-
#include <launchdarkly/serialization/json_evaluation_reason.hpp>
97
#include <launchdarkly/detail/serialization/json_primitives.hpp>
108
#include <launchdarkly/detail/serialization/json_value.hpp>
9+
#include <launchdarkly/serialization/json_context.hpp>
10+
#include <launchdarkly/serialization/json_evaluation_reason.hpp>
1111
#include <launchdarkly/server_side/serialization/json_all_flags_state.hpp>
1212
#include <launchdarkly/value.hpp>
1313

@@ -172,6 +172,15 @@ tl::expected<nlohmann::json, std::string> ClientEntity::Custom(
172172
return nlohmann::json{};
173173
}
174174

175+
tl::expected<nlohmann::json, std::string>
176+
ClientEntity::GetBigSegmentStoreStatus() {
177+
auto status = client_->BigSegmentStoreStatus().Status();
178+
BigSegmentStoreStatusResponse resp{};
179+
resp.available = status.IsAvailable();
180+
resp.stale = status.IsStale();
181+
return resp;
182+
}
183+
175184
tl::expected<nlohmann::json, std::string> ClientEntity::EvaluateAll(
176185
EvaluateAllFlagParams const& params) {
177186
EvaluateAllFlagsResponse resp{};
@@ -388,6 +397,8 @@ tl::expected<nlohmann::json, std::string> ClientEntity::Command(
388397
return tl::make_unexpected("contextConvert params must be set");
389398
}
390399
return ContextConvert(*params.contextConvert);
400+
case Command::GetBigSegmentStoreStatus:
401+
return GetBigSegmentStoreStatus();
391402
}
392403
return tl::make_unexpected("unrecognized command");
393404
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#include "contract_test_big_segment_store.hpp"
2+
3+
#include <boost/asio/connect.hpp>
4+
#include <boost/asio/io_context.hpp>
5+
#include <boost/asio/ip/tcp.hpp>
6+
#include <boost/beast/core.hpp>
7+
#include <boost/beast/http.hpp>
8+
#include <boost/url.hpp>
9+
10+
#include <chrono>
11+
#include <optional>
12+
#include <utility>
13+
#include <vector>
14+
15+
namespace beast = boost::beast;
16+
namespace http = beast::http;
17+
namespace net = boost::asio;
18+
using tcp = net::ip::tcp;
19+
20+
using namespace launchdarkly::server_side::integrations;
21+
22+
ContractTestBigSegmentStore::ContractTestBigSegmentStore(
23+
std::string callback_uri)
24+
: callback_uri_(std::move(callback_uri)) {}
25+
26+
tl::expected<nlohmann::json, std::string> ContractTestBigSegmentStore::Post(
27+
std::string const& path,
28+
nlohmann::json const& body) const noexcept {
29+
try {
30+
auto uri_result = boost::urls::parse_uri(callback_uri_);
31+
if (!uri_result) {
32+
return tl::make_unexpected("invalid callback URI: " +
33+
callback_uri_);
34+
}
35+
auto uri = *uri_result;
36+
std::string const host(uri.host());
37+
std::string const port =
38+
uri.has_port() ? std::string(uri.port()) : "80";
39+
std::string base(uri.path());
40+
// The callback URI carries a base path; the sub-path (/getMembership,
41+
// /getMetadata) is appended. Drop any trailing slash to avoid "//".
42+
if (!base.empty() && base.back() == '/') {
43+
base.pop_back();
44+
}
45+
std::string const target = base + path;
46+
47+
net::io_context ioc;
48+
tcp::resolver resolver(ioc);
49+
beast::tcp_stream stream(ioc);
50+
stream.connect(resolver.resolve(host, port));
51+
52+
http::request<http::string_body> req{http::verb::post, target, 11};
53+
req.set(http::field::host, host);
54+
req.set(http::field::user_agent, "cpp-server-sdk-contract-tests");
55+
req.set(http::field::content_type, "application/json");
56+
req.body() = body.dump();
57+
req.prepare_payload();
58+
http::write(stream, req);
59+
60+
beast::flat_buffer buffer;
61+
http::response<http::string_body> res;
62+
http::read(stream, buffer, res);
63+
64+
beast::error_code ec;
65+
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
66+
67+
if (res.result_int() != 200) {
68+
return tl::make_unexpected(res.body());
69+
}
70+
return nlohmann::json::parse(res.body());
71+
} catch (std::exception const& e) {
72+
return tl::make_unexpected(e.what());
73+
}
74+
}
75+
76+
ContractTestBigSegmentStore::GetMembershipResult
77+
ContractTestBigSegmentStore::GetMembership(
78+
std::string const& context_hash) const noexcept {
79+
auto result = Post("/getMembership", {{"contextHash", context_hash}});
80+
if (!result) {
81+
return tl::make_unexpected(result.error());
82+
}
83+
try {
84+
std::vector<std::string> included;
85+
std::vector<std::string> excluded;
86+
auto const it = result->find("values");
87+
if (it != result->end() && it->is_object()) {
88+
for (auto const& [segment_ref, member] : it->items()) {
89+
(member.get<bool>() ? included : excluded)
90+
.push_back(segment_ref);
91+
}
92+
}
93+
return Membership::FromSegmentRefs(included, excluded);
94+
} catch (std::exception const& e) {
95+
return tl::make_unexpected(e.what());
96+
}
97+
}
98+
99+
ContractTestBigSegmentStore::GetMetadataResult
100+
ContractTestBigSegmentStore::GetMetadata() const noexcept {
101+
auto result = Post("/getMetadata", nlohmann::json::object());
102+
if (!result) {
103+
return tl::make_unexpected(result.error());
104+
}
105+
try {
106+
auto const it = result->find("lastUpToDate");
107+
// Absent or zero means the store has never been synchronized.
108+
if (it == result->end() || it->is_null()) {
109+
return std::optional<StoreMetadata>{std::nullopt};
110+
}
111+
auto const millis = it->get<std::uint64_t>();
112+
if (millis == 0) {
113+
return std::optional<StoreMetadata>{std::nullopt};
114+
}
115+
return std::optional<StoreMetadata>{
116+
StoreMetadata{std::chrono::system_clock::time_point{
117+
std::chrono::milliseconds{millis}}}};
118+
} catch (std::exception const& e) {
119+
return tl::make_unexpected(e.what());
120+
}
121+
}

contract-tests/server-contract-tests/src/entity_manager.cpp

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#include "entity_manager.hpp"
2+
#include "contract_test_big_segment_store.hpp"
23
#include "contract_test_hook.hpp"
34

45
#include <launchdarkly/context_builder.hpp>
56
#include <launchdarkly/serialization/json_context.hpp>
7+
#include <launchdarkly/server_side/config/builders/big_segments_builder.hpp>
68
#include <launchdarkly/server_side/config/config_builder.hpp>
79

810
#include <boost/json.hpp>
@@ -140,7 +142,8 @@ std::optional<std::string> EntityManager::create(ConfigParams const& in) {
140142

141143
if (in.hooks) {
142144
for (auto const& hook_config : in.hooks->hooks) {
143-
auto hook = std::make_shared<ContractTestHook>(executor_, hook_config);
145+
auto hook =
146+
std::make_shared<ContractTestHook>(executor_, hook_config);
144147
config_builder.Hooks(hook);
145148
}
146149
}
@@ -154,6 +157,28 @@ std::optional<std::string> EntityManager::create(ConfigParams const& in) {
154157
}
155158
}
156159

160+
if (in.bigSegments) {
161+
auto store = std::make_shared<ContractTestBigSegmentStore>(
162+
in.bigSegments->callbackUri);
163+
auto big_segments = config::builders::BigSegmentsBuilder(store);
164+
if (in.bigSegments->userCacheSize) {
165+
big_segments.ContextCacheSize(*in.bigSegments->userCacheSize);
166+
}
167+
if (in.bigSegments->userCacheTimeMs) {
168+
big_segments.ContextCacheTime(
169+
std::chrono::milliseconds(*in.bigSegments->userCacheTimeMs));
170+
}
171+
if (in.bigSegments->statusPollIntervalMs) {
172+
big_segments.StatusPollInterval(std::chrono::milliseconds(
173+
*in.bigSegments->statusPollIntervalMs));
174+
}
175+
if (in.bigSegments->staleAfterMs) {
176+
big_segments.StaleAfter(
177+
std::chrono::milliseconds(*in.bigSegments->staleAfterMs));
178+
}
179+
config_builder.BigSegments(std::move(big_segments));
180+
}
181+
157182
auto config = config_builder.Build();
158183
if (!config) {
159184
LD_LOG(logger_, LogLevel::kWarn)

contract-tests/server-contract-tests/src/main.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ int main(int argc, char* argv[]) {
5252
srv.add_capability("track-hooks");
5353
srv.add_capability("wrapper");
5454
srv.add_capability("instance-id");
55+
srv.add_capability("big-segments");
5556

5657
net::signal_set signals{ioc, SIGINT, SIGTERM};
5758

0 commit comments

Comments
 (0)