Skip to content

Commit af09001

Browse files
committed
Merge remote-tracking branch 'origin/main' into beeklimt/SDK-2551
2 parents 299e5da + 85afca7 commit af09001

5 files changed

Lines changed: 259 additions & 40 deletions

File tree

.github/workflows/server.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,30 @@ jobs:
3636
test_service_port: ${{ env.TEST_SERVICE_PORT }}
3737
token: ${{ secrets.GITHUB_TOKEN }}
3838

39+
contract-tests-fdv2:
40+
runs-on: ubuntu-22.04
41+
env:
42+
TEST_SERVICE_PORT: 8123
43+
TEST_SERVICE_BINARY: ./build/contract-tests/server-contract-tests/server-tests
44+
SUPPRESSION_FILE: contract-tests/server-contract-tests/test-suppressions-fdv2.txt
45+
steps:
46+
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
47+
- uses: ./.github/actions/ci
48+
with:
49+
cmake_target: server-tests
50+
run_tests: false
51+
- name: 'Launch test service as background task'
52+
run: $TEST_SERVICE_BINARY $TEST_SERVICE_PORT 2>&1 &
53+
# https://github.com/launchdarkly/gh-actions/releases/tag/contract-tests-v1.1.0
54+
- uses: launchdarkly/gh-actions/actions/contract-tests@2715574e04448246bc529a23a81766491bbc4aae
55+
with:
56+
test_service_port: ${{ env.TEST_SERVICE_PORT }}
57+
token: ${{ secrets.GITHUB_TOKEN }}
58+
version: v3
59+
branch: v3
60+
debug_logging: 'true'
61+
extra_params: -skip-from=${{ env.SUPPRESSION_FILE }}
62+
3963
contract-tests-curl:
4064
runs-on: ubuntu-22.04
4165
env:
@@ -59,6 +83,31 @@ jobs:
5983
test_service_port: ${{ env.TEST_SERVICE_PORT }}
6084
token: ${{ secrets.GITHUB_TOKEN }}
6185

86+
contract-tests-fdv2-curl:
87+
runs-on: ubuntu-22.04
88+
env:
89+
TEST_SERVICE_PORT: 8123
90+
TEST_SERVICE_BINARY: ./build/contract-tests/server-contract-tests/server-tests
91+
SUPPRESSION_FILE: contract-tests/server-contract-tests/test-suppressions-fdv2.txt
92+
steps:
93+
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
94+
- uses: ./.github/actions/ci
95+
with:
96+
cmake_target: server-tests
97+
run_tests: false
98+
use_curl: true
99+
- name: 'Launch test service as background task'
100+
run: $TEST_SERVICE_BINARY $TEST_SERVICE_PORT 2>&1 &
101+
# https://github.com/launchdarkly/gh-actions/releases/tag/contract-tests-v1.1.0
102+
- uses: launchdarkly/gh-actions/actions/contract-tests@2715574e04448246bc529a23a81766491bbc4aae
103+
with:
104+
test_service_port: ${{ env.TEST_SERVICE_PORT }}
105+
token: ${{ secrets.GITHUB_TOKEN }}
106+
version: v3
107+
branch: v3
108+
debug_logging: 'true'
109+
extra_params: -skip-from=${{ env.SUPPRESSION_FILE }}
110+
62111
build-test-server:
63112
runs-on: ubuntu-22.04
64113
steps:

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

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigTags,
113113
applicationId,
114114
applicationVersion);
115115

116-
enum class HookStage {
117-
BeforeEvaluation,
118-
AfterEvaluation,
119-
AfterTrack
120-
};
116+
enum class HookStage { BeforeEvaluation, AfterEvaluation, AfterTrack };
121117

122118
NLOHMANN_JSON_SERIALIZE_ENUM(HookStage,
123119
{{HookStage::BeforeEvaluation, "beforeEvaluation"},
@@ -127,7 +123,10 @@ NLOHMANN_JSON_SERIALIZE_ENUM(HookStage,
127123
struct ConfigHookInstance {
128124
std::string name;
129125
std::string callbackUri;
130-
std::optional<std::unordered_map<std::string, std::unordered_map<std::string, nlohmann::json>>> data;
126+
std::optional<
127+
std::unordered_map<std::string,
128+
std::unordered_map<std::string, nlohmann::json>>>
129+
data;
131130
std::optional<std::unordered_map<std::string, std::string>> errors;
132131
};
133132

@@ -150,6 +149,35 @@ struct ConfigWrapper {
150149

151150
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigWrapper, name, version);
152151

152+
struct ConfigDataSynchronizerParams {
153+
std::optional<ConfigStreamingParams> streaming;
154+
std::optional<ConfigPollingParams> polling;
155+
};
156+
157+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigDataSynchronizerParams,
158+
streaming,
159+
polling);
160+
161+
struct ConfigDataInitializerParams {
162+
std::optional<ConfigPollingParams> polling;
163+
};
164+
165+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigDataInitializerParams,
166+
polling);
167+
168+
struct ConfigDataSystemParams {
169+
std::optional<std::vector<ConfigDataInitializerParams>> initializers;
170+
std::optional<std::vector<ConfigDataSynchronizerParams>> synchronizers;
171+
std::optional<ConfigPollingParams> fdv1Fallback;
172+
std::optional<std::string> payloadFilter;
173+
};
174+
175+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigDataSystemParams,
176+
initializers,
177+
synchronizers,
178+
fdv1Fallback,
179+
payloadFilter);
180+
153181
struct ConfigBigSegmentsParams {
154182
std::string callbackUri;
155183
std::optional<uint32_t> userCacheSize;
@@ -171,6 +199,7 @@ struct ConfigParams {
171199
std::optional<bool> initCanFail;
172200
std::optional<ConfigStreamingParams> streaming;
173201
std::optional<ConfigPollingParams> polling;
202+
std::optional<ConfigDataSystemParams> dataSystem;
174203
std::optional<ConfigEventParams> events;
175204
std::optional<ConfigServiceEndpointsParams> serviceEndpoints;
176205
std::optional<ConfigClientSideParams> clientSide;
@@ -188,6 +217,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams,
188217
initCanFail,
189218
streaming,
190219
polling,
220+
dataSystem,
191221
events,
192222
serviceEndpoints,
193223
clientSide,

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

Lines changed: 165 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,48 +12,131 @@
1212
using launchdarkly::LogLevel;
1313
using namespace launchdarkly::server_side;
1414

15-
EntityManager::EntityManager(boost::asio::any_io_executor executor,
16-
launchdarkly::Logger& logger)
17-
: counter_{0}, executor_{std::move(executor)}, logger_{logger} {}
18-
19-
std::optional<std::string> EntityManager::create(ConfigParams const& in) {
20-
std::string id = std::to_string(counter_++);
21-
22-
auto config_builder = ConfigBuilder(in.credential);
23-
24-
// The contract test service sets endpoints in a way that is disallowed
25-
// for users. Specifically, it may set just 1 of the 3 endpoints, whereas
26-
// we require all 3 to be set.
27-
//
28-
// To avoid that error being detected, we must configure the Endpoints
29-
// builder with the 3 default URLs, which we can fetch by just calling Build
30-
// on a new builder. That way when the contract tests set just 1 URL,
31-
// the others have already been "set" so no error occurs.
32-
auto const default_endpoints =
33-
*config::builders::EndpointsBuilder().Build();
15+
namespace {
16+
17+
config::builders::DataSystemBuilder::FDv2 BuildFDv2(
18+
ConfigDataSystemParams const& cfg,
19+
config::builders::EndpointsBuilder* endpoints) {
20+
auto fdv2 = config::builders::DataSystemBuilder::FDv2::Custom();
21+
22+
if (cfg.synchronizers) {
23+
for (auto const& sync : *cfg.synchronizers) {
24+
if (sync.streaming) {
25+
auto s = decltype(fdv2)::Streaming();
26+
if (sync.streaming->baseUri) {
27+
s.BaseUrl(*sync.streaming->baseUri);
28+
}
29+
if (sync.streaming->initialRetryDelayMs) {
30+
s.InitialReconnectDelay(std::chrono::milliseconds(
31+
*sync.streaming->initialRetryDelayMs));
32+
}
33+
fdv2.Synchronizer(std::move(s));
34+
} else if (sync.polling) {
35+
auto p = decltype(fdv2)::Polling();
36+
if (sync.polling->baseUri) {
37+
p.BaseUrl(*sync.polling->baseUri);
38+
}
39+
if (sync.polling->pollIntervalMs) {
40+
p.PollInterval(
41+
std::chrono::duration_cast<std::chrono::seconds>(
42+
std::chrono::milliseconds(
43+
*sync.polling->pollIntervalMs)));
44+
}
45+
fdv2.Synchronizer(std::move(p));
46+
}
47+
}
48+
}
3449

35-
auto& endpoints =
36-
config_builder.ServiceEndpoints()
37-
.EventsBaseUrl(default_endpoints.EventsBaseUrl())
38-
.PollingBaseUrl(default_endpoints.PollingBaseUrl())
39-
.StreamingBaseUrl(default_endpoints.StreamingBaseUrl());
50+
if (cfg.initializers) {
51+
for (auto const& init : *cfg.initializers) {
52+
if (init.polling) {
53+
auto p = decltype(fdv2)::Polling();
54+
if (init.polling->baseUri) {
55+
p.BaseUrl(*init.polling->baseUri);
56+
}
57+
if (init.polling->pollIntervalMs) {
58+
p.PollInterval(
59+
std::chrono::duration_cast<std::chrono::seconds>(
60+
std::chrono::milliseconds(
61+
*init.polling->pollIntervalMs)));
62+
}
63+
fdv2.Initializer(std::move(p));
64+
}
65+
}
66+
}
4067

41-
if (in.serviceEndpoints) {
42-
if (in.serviceEndpoints->streaming) {
43-
endpoints.StreamingBaseUrl(*in.serviceEndpoints->streaming);
68+
using FDv2Builder = config::builders::DataSystemBuilder::FDv2;
69+
if (cfg.fdv1Fallback) {
70+
if (cfg.fdv1Fallback->baseUri) {
71+
endpoints->PollingBaseUrl(*cfg.fdv1Fallback->baseUri);
72+
} else if (cfg.synchronizers && !cfg.synchronizers->empty()) {
73+
// No explicit baseUri: derive from the synchronizers list, matching
74+
// the no-fdv1Fallback branch below.
75+
ConfigDataSynchronizerParams const* selected = nullptr;
76+
for (auto const& sync : *cfg.synchronizers) {
77+
if (sync.polling) {
78+
selected = &sync;
79+
break;
80+
}
81+
}
82+
if (!selected) {
83+
selected = &cfg.synchronizers->front();
84+
}
85+
if (selected->polling && selected->polling->baseUri) {
86+
endpoints->PollingBaseUrl(*selected->polling->baseUri);
87+
} else if (selected->streaming && selected->streaming->baseUri) {
88+
endpoints->PollingBaseUrl(*selected->streaming->baseUri);
89+
}
4490
}
45-
if (in.serviceEndpoints->polling) {
46-
endpoints.PollingBaseUrl(*in.serviceEndpoints->polling);
91+
FDv2Builder::FDv1Polling p;
92+
if (cfg.fdv1Fallback->pollIntervalMs) {
93+
p.PollInterval(std::chrono::duration_cast<std::chrono::seconds>(
94+
std::chrono::milliseconds(*cfg.fdv1Fallback->pollIntervalMs)));
4795
}
48-
if (in.serviceEndpoints->events) {
49-
endpoints.EventsBaseUrl(*in.serviceEndpoints->events);
96+
fdv2.FDv1Fallback(std::move(p));
97+
} else if (cfg.synchronizers && !cfg.synchronizers->empty()) {
98+
// Derive an FDv1 fallback from the synchronizers list: prefer the
99+
// first polling sync, otherwise reuse the first synchronizer's
100+
// baseUri. The fallback is always polling. The fallback reads its
101+
// URL from the global ServiceEndpoints, so set the polling endpoint
102+
// to the selected baseUri.
103+
ConfigDataSynchronizerParams const* selected = nullptr;
104+
for (auto const& sync : *cfg.synchronizers) {
105+
if (sync.polling) {
106+
selected = &sync;
107+
break;
108+
}
50109
}
110+
if (!selected) {
111+
selected = &cfg.synchronizers->front();
112+
}
113+
FDv2Builder::FDv1Polling p;
114+
if (selected->polling) {
115+
if (selected->polling->baseUri) {
116+
endpoints->PollingBaseUrl(*selected->polling->baseUri);
117+
}
118+
if (selected->polling->pollIntervalMs) {
119+
p.PollInterval(std::chrono::duration_cast<std::chrono::seconds>(
120+
std::chrono::milliseconds(
121+
*selected->polling->pollIntervalMs)));
122+
}
123+
} else if (selected->streaming && selected->streaming->baseUri) {
124+
endpoints->PollingBaseUrl(*selected->streaming->baseUri);
125+
}
126+
fdv2.FDv1Fallback(std::move(p));
51127
}
128+
129+
return fdv2;
130+
}
131+
132+
config::builders::DataSystemBuilder::BackgroundSync BuildBackgroundSync(
133+
ConfigParams const& in,
134+
config::builders::EndpointsBuilder* endpoints) {
52135
auto datasystem = config::builders::DataSystemBuilder::BackgroundSync();
53136

54137
if (in.streaming) {
55138
if (in.streaming->baseUri) {
56-
endpoints.StreamingBaseUrl(*in.streaming->baseUri);
139+
endpoints->StreamingBaseUrl(*in.streaming->baseUri);
57140
}
58141
auto streaming = decltype(datasystem)::Streaming();
59142
if (in.streaming->initialRetryDelayMs) {
@@ -68,7 +151,7 @@ std::optional<std::string> EntityManager::create(ConfigParams const& in) {
68151

69152
if (in.polling) {
70153
if (in.polling->baseUri) {
71-
endpoints.PollingBaseUrl(*in.polling->baseUri);
154+
endpoints->PollingBaseUrl(*in.polling->baseUri);
72155
}
73156
if (!in.streaming) {
74157
auto method = decltype(datasystem)::Polling();
@@ -85,7 +168,55 @@ std::optional<std::string> EntityManager::create(ConfigParams const& in) {
85168
}
86169
}
87170

88-
config_builder.DataSystem().Method(std::move(datasystem));
171+
return datasystem;
172+
}
173+
174+
} // namespace
175+
176+
EntityManager::EntityManager(boost::asio::any_io_executor executor,
177+
launchdarkly::Logger& logger)
178+
: counter_{0}, executor_{std::move(executor)}, logger_{logger} {}
179+
180+
std::optional<std::string> EntityManager::create(ConfigParams const& in) {
181+
std::string id = std::to_string(counter_++);
182+
183+
auto config_builder = ConfigBuilder(in.credential);
184+
185+
// The contract test service sets endpoints in a way that is disallowed
186+
// for users. Specifically, it may set just 1 of the 3 endpoints, whereas
187+
// we require all 3 to be set.
188+
//
189+
// To avoid that error being detected, we must configure the Endpoints
190+
// builder with the 3 default URLs, which we can fetch by just calling Build
191+
// on a new builder. That way when the contract tests set just 1 URL,
192+
// the others have already been "set" so no error occurs.
193+
auto const default_endpoints =
194+
*config::builders::EndpointsBuilder().Build();
195+
196+
auto& endpoints =
197+
config_builder.ServiceEndpoints()
198+
.EventsBaseUrl(default_endpoints.EventsBaseUrl())
199+
.PollingBaseUrl(default_endpoints.PollingBaseUrl())
200+
.StreamingBaseUrl(default_endpoints.StreamingBaseUrl());
201+
202+
if (in.serviceEndpoints) {
203+
if (in.serviceEndpoints->streaming) {
204+
endpoints.StreamingBaseUrl(*in.serviceEndpoints->streaming);
205+
}
206+
if (in.serviceEndpoints->polling) {
207+
endpoints.PollingBaseUrl(*in.serviceEndpoints->polling);
208+
}
209+
if (in.serviceEndpoints->events) {
210+
endpoints.EventsBaseUrl(*in.serviceEndpoints->events);
211+
}
212+
}
213+
214+
if (in.dataSystem) {
215+
config_builder.DataSystem().Method(
216+
BuildFDv2(*in.dataSystem, &endpoints));
217+
} else {
218+
config_builder.DataSystem().Method(BuildBackgroundSync(in, &endpoints));
219+
}
89220

90221
auto& event_config = config_builder.Events();
91222

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("fdv1-fallback");
5556
srv.add_capability("big-segments");
5657

5758
net::signal_set signals{ioc, SIGINT, SIGTERM};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# C++ implements TTL-based FDv1 fallback; this scenario tests the older terminal semantics. Tracked in SDK-2527.
2+
streaming/fdv2/FDv1 fallback directive/directive without FDv1 fallback configured halts the data system
3+
4+
# FDv2 public payload filter API is not exposed by the C++ SDK.
5+
streaming/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has no trailing slash/GET
6+
streaming/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has a trailing slash/GET
7+
polling/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has no trailing slash/GET
8+
polling/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has a trailing slash/GET

0 commit comments

Comments
 (0)