Skip to content
Draft
1 change: 1 addition & 0 deletions contract-tests/server-contract-tests/src/main.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "server.hpp"

Check failure on line 1 in contract-tests/server-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/server-contract-tests/src/main.cpp:1:10 [clang-diagnostic-error]

'server.hpp' file not found

#include <launchdarkly/logging/console_backend.hpp>

Expand All @@ -18,7 +18,7 @@
using launchdarkly::LogLevel;

int main(int argc, char* argv[]) {
launchdarkly::Logger logger{

Check warning on line 21 in contract-tests/server-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/server-contract-tests/src/main.cpp:21:26 [cppcoreguidelines-init-variables]

variable 'logger' is not initialized
std::make_unique<ConsoleBackend>("server-contract-tests")};

std::string const default_port = "8123";
Expand All @@ -31,8 +31,8 @@
try {
net::io_context ioc{1};

auto const p = boost::lexical_cast<unsigned short>(port);

Check warning on line 34 in contract-tests/server-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/server-contract-tests/src/main.cpp:34:20 [readability-identifier-length]

variable name 'p' is too short, expected at least 3 characters
server srv{ioc, "0.0.0.0", p, logger};

Check warning on line 35 in contract-tests/server-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

contract-tests/server-contract-tests/src/main.cpp:35:16 [cppcoreguidelines-init-variables]

variable 'srv' is not initialized

srv.add_capability("server-side");
srv.add_capability("strongly-typed");
Expand All @@ -50,6 +50,7 @@
srv.add_capability("client-prereq-events");
srv.add_capability("evaluation-hooks");
srv.add_capability("track-hooks");
srv.add_capability("hook-environment-id");
srv.add_capability("wrapper");
srv.add_capability("instance-id");
srv.add_capability("fdv1-fallback");
Expand Down
1 change: 1 addition & 0 deletions libs/server-sdk/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ target_sources(${LIBNAME}
data_components/serialization_adapters/json_deserializer.cpp
data_components/serialization_adapters/json_destination.hpp
data_components/serialization_adapters/json_destination.cpp
data_systems/environment_id_header.hpp
data_systems/background_sync/detail/payload_filter_validation/payload_filter_validation.hpp
data_systems/background_sync/detail/payload_filter_validation/payload_filter_validation.cpp
data_systems/background_sync/sources/polling/polling_data_source.hpp
Expand Down
13 changes: 7 additions & 6 deletions libs/server-sdk/src/client_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
std::vector<std::unique_ptr<data_interfaces::IFDv2InitializerFactory>>
initializer_factories;
for (auto const& initializer : cfg.initializers) {
initializer_factories.push_back(

Check warning on line 97 in libs/server-sdk/src/client_impl.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk/src/client_impl.cpp:97:9 [performance-inefficient-vector-operation]

'push_back' is called inside a loop; consider pre-allocating the container capacity before the loop
std::make_unique<data_systems::FDv2PollingInitializerFactory>(
executor, logger, endpoints, http_properties, initializer));
}
Expand Down Expand Up @@ -220,7 +220,7 @@
bool IsFlagPresent(
std::shared_ptr<data_model::FlagDescriptor> const& flag_desc);

ClientImpl::ClientImpl(Config config, std::string const& version)

Check warning on line 223 in libs/server-sdk/src/client_impl.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk/src/client_impl.cpp:223:31 [performance-unnecessary-value-param]

the parameter 'config' is copied for each invocation but only used as a const reference; consider making it a const reference
: config_(config),
http_properties_(
config::builders::HttpPropertiesBuilder(config.HttpProperties())
Expand All @@ -238,7 +238,7 @@
logger_(MakeLogger(config.Logging())),
ioc_(kAsioConcurrencyHint),
work_(boost::asio::make_work_guard(ioc_)),
status_manager_(),

Check warning on line 241 in libs/server-sdk/src/client_impl.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk/src/client_impl.cpp:241:7 [readability-redundant-member-init]

initializer for member 'status_manager_' is redundant
data_system_(MakeDataSystem(http_properties_,
config_,
ioc_.get_executor(),
Expand All @@ -251,7 +251,7 @@
big_segment_store_(
config_.BigSegments()
? std::make_shared<data_components::BigSegmentStoreWrapper>(
*config_.BigSegments(),

Check warning on line 254 in libs/server-sdk/src/client_impl.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk/src/client_impl.cpp:254:22 [bugprone-unchecked-optional-access]

unchecked access to optional value
ioc_.get_executor(),
logger_)
: nullptr),
Expand Down Expand Up @@ -287,10 +287,10 @@
}

std::future<bool> ClientImpl::StartAsync() {
auto pr = std::make_shared<std::promise<bool>>();

Check warning on line 290 in libs/server-sdk/src/client_impl.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk/src/client_impl.cpp:290:10 [readability-identifier-length]

variable name 'pr' is too short, expected at least 3 characters
auto fut = pr->get_future();

status_manager_.OnDataSourceStatusChangeEx([this, pr](auto _) {

Check warning on line 293 in libs/server-sdk/src/client_impl.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk/src/client_impl.cpp:293:64 [readability-identifier-length]

parameter name '_' is too short, expected at least 3 characters
if (data_system_->Initialized()) {
pr->set_value(true);
return true; /* delete this change listener since the
Expand Down Expand Up @@ -328,7 +328,7 @@
// system to fetch them all at once up-front. This may be a no-op
// depending on the data system (e.g. if the segments are all already in
// memory.)
auto _ = data_system_->AllSegments();

Check warning on line 331 in libs/server-sdk/src/client_impl.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/server-sdk/src/client_impl.cpp:331:10 [readability-identifier-length]

variable name '_' is too short, expected at least 3 characters

for (auto const& [key, v] : all_flags) {
if (!v || !v->item) {
Expand Down Expand Up @@ -378,8 +378,9 @@
// In this SDK the data is type-safe, and will be enqueued, so it makes
// minimal functional difference.
if (!config_.Hooks().empty()) {
hooks::TrackSeriesContext series_context(
ctx, event_name, metric_value, data, hook_context, std::nullopt);
hooks::TrackSeriesContext series_context(ctx, event_name, metric_value,
data, hook_context,
data_system_->EnvironmentId());
hooks::ExecuteAfterTrack(config_.Hooks(), series_context, logger_);
}

Expand Down Expand Up @@ -487,7 +488,7 @@
if (!config_.Hooks().empty()) {
hooks::EvaluationSeriesContext series_context(
key, context, default_value, method_name, hook_context,
std::nullopt);
data_system_->EnvironmentId());
// Executor only created if there are hooks.
executor.emplace(config_.Hooks(), logger_);
executor->BeforeEvaluation(series_context);
Expand All @@ -501,7 +502,7 @@
if (executor) {
hooks::EvaluationSeriesContext series_context(
key, context, default_value, method_name, hook_context,
std::nullopt);
data_system_->EnvironmentId());
executor->AfterEvaluation(series_context, detail);
}

Expand All @@ -523,7 +524,7 @@
if (executor) {
hooks::EvaluationSeriesContext series_context(
key, context, default_value, method_name, hook_context,
std::nullopt);
data_system_->EnvironmentId());
executor->AfterEvaluation(series_context, detail);
}

Expand All @@ -539,7 +540,7 @@
if (executor) {
hooks::EvaluationSeriesContext series_context(
key, context, default_value, method_name, hook_context,
std::nullopt);
data_system_->EnvironmentId());
executor->AfterEvaluation(series_context, detail);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ void ChangeNotifier::Apply(
}
}

void ChangeNotifier::SetEnvironmentId(std::string environment_id) {
sink_.SetEnvironmentId(std::move(environment_id));
}

bool ChangeNotifier::HasListeners() const {
std::lock_guard lock{signal_mutex_};
return !signals_.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class ChangeNotifier final : public data_interfaces::ITransactionalDestination,
data_model::SegmentDescriptor segment) override;
void Apply(data_model::ChangeSet<data_interfaces::ChangeSetData> change_set)
override;
void SetEnvironmentId(std::string environment_id) override;

[[nodiscard]] std::string const& Identity() const override;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ void MemoryStore::Upsert(std::string const& key,
std::make_shared<data_model::SegmentDescriptor>(std::move(segment));
}

void MemoryStore::SetEnvironmentId(std::string environment_id) {
std::lock_guard lock{data_mutex_};
environment_id_ = std::move(environment_id);
}

std::optional<std::string> MemoryStore::EnvironmentId() const {
std::lock_guard lock{data_mutex_};
if (!initialized_) {
return std::nullopt;
}
return environment_id_;
}

bool MemoryStore::RemoveFlag(std::string const& key) {
std::lock_guard lock{data_mutex_};
return flags_.erase(key) == 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>

Expand Down Expand Up @@ -43,6 +44,14 @@ class MemoryStore final : public data_interfaces::IStore,
void Upsert(std::string const& key,
data_model::SegmentDescriptor segment) override;

void SetEnvironmentId(std::string environment_id) override;

/**
* @return The environment ID reported by LaunchDarkly, if any has been
* received.
*/
[[nodiscard]] std::optional<std::string> EnvironmentId() const;

bool RemoveFlag(std::string const& key);

bool RemoveSegment(std::string const& key);
Expand All @@ -66,6 +75,7 @@ class MemoryStore final : public data_interfaces::IStore,
std::shared_ptr<data_model::SegmentDescriptor>>
segments_;
bool initialized_ = false;
std::optional<std::string> environment_id_;
mutable std::mutex data_mutex_;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ void JsonDestination::Upsert(std::string const& key,
dest_.Upsert(Kinds::Segment, key, Serialize(key, segment)));
}

void JsonDestination::SetEnvironmentId(std::string) {}

void JsonDestination::LogUpsertResult(
std::string const& key,
std::string const& data_type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ class JsonDestination final : public data_interfaces::IDestination {
void Upsert(std::string const& key,
data_model::SegmentDescriptor segment) override;

/**
* @brief No-op; serialized destinations do not store the environment ID.
*/
void SetEnvironmentId(std::string environment_id) override;

/**
* @return Identity of this destination. Used in logs.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ class IDestination {
virtual void Upsert(std::string const& key,
data_model::SegmentDescriptor segment) = 0;

/**
* \brief Record the environment ID that LaunchDarkly reported alongside
* the data. Destinations which do not track it ignore the value.
* \param environment_id The environment ID.
*/
virtual void SetEnvironmentId(std::string environment_id) = 0;

/**
* \return Identity of the destination. Used in logs.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ struct FDv2SourceResult {
* Set if the underlying transport observed an FDv1 fallback directive.
*/
std::optional<FDv1FallbackDirective> fdv1_fallback;

/**
* Set if the underlying transport reported the environment ID (e.g. an
* X-LD-EnvID response header).
*/
std::optional<std::string> environment_id;
};

} // namespace launchdarkly::server_side::data_interfaces
9 changes: 9 additions & 0 deletions libs/server-sdk/src/data_interfaces/system/idata_system.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

#include "../store/istore.hpp"

#include <optional>
#include <string>

namespace launchdarkly::server_side::data_interfaces {

/**
Expand All @@ -21,6 +24,12 @@ class IDataSystem : public IStore {
*/
virtual void Initialize() = 0;

/**
* @return The environment ID reported by LaunchDarkly alongside the data,
* if the system has received one.
*/
[[nodiscard]] virtual std::optional<std::string> EnvironmentId() const = 0;

virtual ~IDataSystem() override = default;
IDataSystem(IDataSystem const& item) = delete;
IDataSystem(IDataSystem&& item) = delete;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ std::string const& BackgroundSync::Identity() const {
return id;
}

std::optional<std::string> BackgroundSync::EnvironmentId() const {
return store_.EnvironmentId();
}

std::shared_ptr<data_model::FlagDescriptor> BackgroundSync::GetFlag(
std::string const& key) const {
return store_.GetFlag(key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ class BackgroundSync final : public data_interfaces::IDataSystem {

bool Initialized() const override;

std::optional<std::string> EnvironmentId() const override;

private:
data_components::MemoryStore store_;
data_components::ChangeNotifier change_notifier_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
#include <launchdarkly/encoding/base_64.hpp>
#include <launchdarkly/network/http_error_messages.hpp>

#include <launchdarkly/serialization/json_flag.hpp>
#include <launchdarkly/detail/serialization/json_primitives.hpp>
#include <launchdarkly/serialization/json_flag.hpp>
#include <launchdarkly/serialization/json_sdk_data_set.hpp>
#include <launchdarkly/server_side/data_source_status.hpp>

#include <launchdarkly/server_side/config/builders/all_builders.hpp>

#include "../../../environment_id_header.hpp"
#include "../../detail/payload_filter_validation/payload_filter_validation.hpp"

#include <boost/json.hpp>
Expand Down Expand Up @@ -102,6 +103,10 @@ void PollingDataSource::DoPoll() {
}

void PollingDataSource::HandlePollResult(network::HttpResult const& res) {
if (!res.IsError() && (res.Status() == 200 || res.Status() == 304)) {
ReportEnvironmentId(*sink_, res.Headers());
}

auto header_etag = res.Headers().find("etag");
bool has_etag = header_etag != res.Headers().end();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <launchdarkly/network/http_requester.hpp>

#include "../../../environment_id_header.hpp"
#include "../../detail/payload_filter_validation/payload_filter_validation.hpp"

#include <boost/asio/any_io_executor.hpp>
Expand Down Expand Up @@ -47,6 +48,7 @@ void StreamingDataSource::StartAsync(
data_model::SDKDataSet const* bootstrap_data) {
boost::ignore_unused(bootstrap_data);

sink_ = dest;
event_handler_.emplace(*dest, logger_, status_manager_);

status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing);
Expand Down Expand Up @@ -125,16 +127,27 @@ void StreamingDataSource::StartAsync(

auto weak_self = weak_from_this();

client_builder.on_response(
[weak_self](boost::beast::http::response_header<> const& headers) {
auto self = weak_self.lock();
if (!self || headers.result_int() != 200) {
return;
}
ReportEnvironmentId(*self->sink_, headers);
});

client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) {
if (auto self = weak_self.lock()) {
auto status =
self->event_handler_->HandleMessage(event.type(), event.data());
if (status == DataSourceEventHandler::MessageStatus::kInvalidMessage) {
if (status ==
DataSourceEventHandler::MessageStatus::kInvalidMessage) {
// Invalid data received - restart the connection with backoff
// to get a fresh stream. The backoff mechanism prevents rapid
// reconnection attempts.
LD_LOG(self->logger_, LogLevel::kWarn)
<< "Received invalid data from stream, restarting connection";
<< "Received invalid data from stream, restarting "
"connection";
if (self->client_) {
Comment on lines +143 to 151

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why were these lines changed?

self->client_->async_restart("invalid data in stream");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ class StreamingDataSource final

config::built::BackgroundSyncConfig::StreamingConfig streaming_config_;

// Destination for all data obtained via streaming. Set by StartAsync.
data_interfaces::IDestination* sink_ = nullptr;

std::shared_ptr<sse::Client> client_;
};
} // namespace launchdarkly::server_side::data_systems
44 changes: 44 additions & 0 deletions libs/server-sdk/src/data_systems/environment_id_header.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

#include <optional>
#include <string>
#include <utility>

#include <boost/beast/http/fields.hpp>

#include <launchdarkly/network/http_requester.hpp>

namespace launchdarkly::server_side::data_systems {

inline constexpr char const* kEnvironmentIdHeader = "X-LD-EnvID";

inline std::optional<std::string> ReadEnvironmentId(
network::HttpResult::HeadersType const& headers) {
auto const it = headers.find(kEnvironmentIdHeader);
if (it == headers.end() || it->second.empty()) {
return std::nullopt;
}
return it->second;
}

inline std::optional<std::string> ReadEnvironmentId(
boost::beast::http::fields const& headers) {
auto const it = headers.find(kEnvironmentIdHeader);
if (it == headers.end() || it->value().empty()) {
return std::nullopt;
}
return std::string(it->value().data(), it->value().size());
}

/**
* Reports the environment ID from the given response headers to the given
* destination, if the headers contain a non-empty environment ID.
*/
template <typename Destination, typename Headers>
void ReportEnvironmentId(Destination& destination, Headers const& headers) {
if (auto environment_id = ReadEnvironmentId(headers)) {
destination.SetEnvironmentId(*std::move(environment_id));
}
}

} // namespace launchdarkly::server_side::data_systems
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#include "fdv1_adapter_synchronizer.hpp"

#include <utility>
Expand Down Expand Up @@ -44,6 +44,7 @@
if (closed_future_.IsFinished()) {
return;
}
result.environment_id = environment_id_;
if (pending_promise_) {
promise = std::move(pending_promise_);
pending_promise_.reset();
Expand All @@ -57,6 +58,12 @@
promise->Resolve(std::move(result));
}

void FDv1AdapterSynchronizer::State::SetEnvironmentId(
std::string environment_id) {
std::lock_guard lock(mutex_);
environment_id_ = std::move(environment_id);
}

// ----- ConvertingDestination -----

FDv1AdapterSynchronizer::ConvertingDestination::ConvertingDestination(
Expand Down Expand Up @@ -113,6 +120,13 @@
data_model::Selector{}}}});
}

void FDv1AdapterSynchronizer::ConvertingDestination::SetEnvironmentId(
std::string environment_id) {
if (auto state = state_.lock()) {
state->SetEnvironmentId(std::move(environment_id));
}
}

std::string const& FDv1AdapterSynchronizer::ConvertingDestination::Identity()
const {
static std::string const identity = "FDv1 adapter destination";
Expand Down
Loading
Loading