Skip to content

Commit 6190fe3

Browse files
committed
Merge remote-tracking branch 'origin/main' into beeklimt/SDK-2102
# Conflicts: # contract-tests/data-model/include/data_model/data_model.hpp # contract-tests/server-contract-tests/src/main.cpp
2 parents 1293152 + 455dacb commit 6190fe3

8 files changed

Lines changed: 233 additions & 5 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
@@ -178,6 +178,21 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigDataSystemParams,
178178
fdv1Fallback,
179179
payloadFilter);
180180

181+
struct ConfigBigSegmentsParams {
182+
std::string callbackUri;
183+
std::optional<uint32_t> userCacheSize;
184+
std::optional<uint64_t> userCacheTimeMs;
185+
std::optional<uint64_t> statusPollIntervalMs;
186+
std::optional<uint64_t> staleAfterMs;
187+
};
188+
189+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigBigSegmentsParams,
190+
callbackUri,
191+
userCacheSize,
192+
userCacheTimeMs,
193+
statusPollIntervalMs,
194+
staleAfterMs);
195+
181196
struct ConfigParams {
182197
std::string credential;
183198
std::optional<uint32_t> startWaitTimeMs;
@@ -193,6 +208,7 @@ struct ConfigParams {
193208
std::optional<ConfigProxyParams> proxy;
194209
std::optional<ConfigHooksParams> hooks;
195210
std::optional<ConfigWrapper> wrapper;
211+
std::optional<ConfigBigSegmentsParams> bigSegments;
196212
};
197213

198214
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams,
@@ -209,7 +225,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams,
209225
tls,
210226
proxy,
211227
hooks,
212-
wrapper);
228+
wrapper,
229+
bigSegments);
213230

214231
struct ContextSingleParams {
215232
std::optional<std::string> kind;
@@ -358,6 +375,15 @@ struct IdentifyEventParams {
358375

359376
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(IdentifyEventParams, context);
360377

378+
struct BigSegmentStoreStatusResponse {
379+
bool available;
380+
bool stale;
381+
};
382+
383+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(BigSegmentStoreStatusResponse,
384+
available,
385+
stale);
386+
361387
enum class Command {
362388
Unknown = -1,
363389
EvaluateFlag,
@@ -366,7 +392,8 @@ enum class Command {
366392
CustomEvent,
367393
FlushEvents,
368394
ContextBuild,
369-
ContextConvert
395+
ContextConvert,
396+
GetBigSegmentStoreStatus
370397
};
371398

372399
NLOHMANN_JSON_SERIALIZE_ENUM(Command,
@@ -377,7 +404,9 @@ NLOHMANN_JSON_SERIALIZE_ENUM(Command,
377404
{Command::CustomEvent, "customEvent"},
378405
{Command::FlushEvents, "flushEvents"},
379406
{Command::ContextBuild, "contextBuild"},
380-
{Command::ContextConvert, "contextConvert"}});
407+
{Command::ContextConvert, "contextConvert"},
408+
{Command::GetBigSegmentStoreStatus,
409+
"getBigSegmentStoreStatus"}});
381410

382411
struct CommandParams {
383412
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: 24 additions & 0 deletions
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>
@@ -262,6 +264,28 @@ std::optional<std::string> EntityManager::create(ConfigParams const& in) {
262264
}
263265
}
264266

267+
if (in.bigSegments) {
268+
auto store = std::make_shared<ContractTestBigSegmentStore>(
269+
in.bigSegments->callbackUri);
270+
auto big_segments = config::builders::BigSegmentsBuilder(store);
271+
if (in.bigSegments->userCacheSize) {
272+
big_segments.ContextCacheSize(*in.bigSegments->userCacheSize);
273+
}
274+
if (in.bigSegments->userCacheTimeMs) {
275+
big_segments.ContextCacheTime(
276+
std::chrono::milliseconds(*in.bigSegments->userCacheTimeMs));
277+
}
278+
if (in.bigSegments->statusPollIntervalMs) {
279+
big_segments.StatusPollInterval(std::chrono::milliseconds(
280+
*in.bigSegments->statusPollIntervalMs));
281+
}
282+
if (in.bigSegments->staleAfterMs) {
283+
big_segments.StaleAfter(
284+
std::chrono::milliseconds(*in.bigSegments->staleAfterMs));
285+
}
286+
config_builder.BigSegments(std::move(big_segments));
287+
}
288+
265289
auto config = config_builder.Build();
266290
if (!config) {
267291
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
@@ -53,6 +53,7 @@ int main(int argc, char* argv[]) {
5353
srv.add_capability("wrapper");
5454
srv.add_capability("instance-id");
5555
srv.add_capability("fdv1-fallback");
56+
srv.add_capability("big-segments");
5657

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

0 commit comments

Comments
 (0)