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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

#include <launchdarkly/data_model/selector.hpp>

#include <optional>
#include <string>

namespace launchdarkly::data_model {

enum class ChangeSetType {
Expand All @@ -11,10 +14,13 @@
};

template <typename T>
struct ChangeSet {

Check warning on line 17 in libs/internal/include/launchdarkly/data_model/change_set.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

libs/internal/include/launchdarkly/data_model/change_set.hpp:17:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: type
ChangeSetType type;
T data;
Selector selector;

// Environment ID reported by LaunchDarkly alongside this data, if known.
std::optional<std::string> environment_id;
};

} // namespace launchdarkly::data_model
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <launchdarkly/data_model/item_descriptor.hpp>
#include <launchdarkly/data_model/segment.hpp>

#include <optional>
#include <string>
#include <unordered_map>

Expand All @@ -19,6 +20,9 @@ struct SDKDataSet {

Flags flags;
Segments segments;

// Environment ID reported by LaunchDarkly alongside this data, if known.
std::optional<std::string> environment_id;
};

} // namespace launchdarkly::data_model
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 @@ -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 @@ -48,6 +48,7 @@ std::string const& MemoryStore::Identity() const {
void MemoryStore::Init(data_model::SDKDataSet dataSet) {
std::lock_guard lock{data_mutex_};
initialized_ = true;
RetainEnvironmentId(dataSet.environment_id);
flags_.clear();
segments_.clear();
for (auto flag : dataSet.flags) {
Expand All @@ -74,6 +75,21 @@ void MemoryStore::Upsert(std::string const& key,
std::make_shared<data_model::SegmentDescriptor>(std::move(segment));
}

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

void MemoryStore::RetainEnvironmentId(
std::optional<std::string> const& environment_id) {
if (environment_id && !environment_id->empty()) {
environment_id_ = environment_id;
}
}

bool MemoryStore::RemoveFlag(std::string const& key) {
std::lock_guard lock{data_mutex_};
return flags_.erase(key) == 1;
Expand All @@ -88,6 +104,8 @@ void MemoryStore::Apply(
data_model::ChangeSet<data_interfaces::ChangeSetData> changeSet) {
std::lock_guard lock{data_mutex_};

RetainEnvironmentId(changeSet.environment_id);

switch (changeSet.type) {
case data_model::ChangeSetType::kNone:
return;
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,12 @@ class MemoryStore final : public data_interfaces::IStore,
void Upsert(std::string const& key,
data_model::SegmentDescriptor segment) 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 @@ -59,13 +66,18 @@ class MemoryStore final : public data_interfaces::IStore,
MemoryStore& operator=(MemoryStore&&) = delete;

private:
// Requires data_mutex_ to be held. Ignores empty values so that data
// without an environment ID does not discard a previously reported one.
void RetainEnvironmentId(std::optional<std::string> const& environment_id);

static inline std::string const description_ = "memory";
std::unordered_map<std::string, std::shared_ptr<data_model::FlagDescriptor>>
flags_;
std::unordered_map<std::string,
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 @@ -14,7 +14,9 @@ namespace launchdarkly::server_side::data_interfaces {
class IDestination {
public:
/**
* \brief Initialize the destination with a base set of data.
* \brief Initialize the destination with a base set of data. The data set
* may carry the environment ID that LaunchDarkly reported with it;
* destinations which do not track it ignore the value.
* \param data_set The initial data received by the SDK.
*/
virtual void Init(data_model::SDKDataSet data_set) = 0;
Expand Down
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
@@ -1,3 +1,3 @@
#include "polling_data_source.hpp"

#include <launchdarkly/encoding/base_64.hpp>
Expand All @@ -10,6 +10,7 @@

#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 @@ -150,6 +151,7 @@
tl::expected<data_model::SDKDataSet, JsonError>>(parsed);

if (poll_result.has_value()) {
poll_result->environment_id = ReadEnvironmentId(res.Headers());
sink_->Init(std::move(*poll_result));
status_manager_.SetState(
DataSourceStatus::DataSourceState::kValid);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ DataSourceEventHandler::DataSourceEventHandler(

DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage(
std::string const& type,
std::string const& data) {
std::string const& data,
std::optional<std::string> const& environment_id) {
if (type == "put") {
boost::system::error_code error_code;
auto parsed = boost::json::parse(data, error_code);
Expand All @@ -162,7 +163,9 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage(

// Check the inner optional.
if (res->has_value()) {
handler_.Init(std::move((*res)->data));
auto& data_set = (*res)->data;
data_set.environment_id = environment_id;
handler_.Init(std::move(data_set));
status_manager_.SetState(DataSourceStatus::DataSourceState::kValid);
return MessageStatus::kMessageHandled;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,14 @@ class DataSourceEventHandler {
* Handles an event from the LaunchDarkly service.
* @param type The type of the event. "put"/"patch"/"delete".
* @param data The content of the event.
* @param environment_id Environment ID reported by the connection, if any.
* It is carried with the data of a "put".
* @return A status indicating if the message could be handled.
*/
MessageStatus HandleMessage(std::string const& type,
std::string const& data);
MessageStatus HandleMessage(
std::string const& type,
std::string const& data,
std::optional<std::string> const& environment_id = std::nullopt);

private:
data_interfaces::IDestination& handler_;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#include "streaming_data_source.hpp"

#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 @@ -125,10 +126,21 @@

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;
}
if (auto environment_id = ReadEnvironmentId(headers)) {
self->environment_id_ = std::move(environment_id);
}
});

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());
auto status = self->event_handler_->HandleMessage(
event.type(), event.data(), self->environment_id_);
if (status == DataSourceEventHandler::MessageStatus::kInvalidMessage) {
// Invalid data received - restart the connection with backoff
// to get a fresh stream. The backoff mechanism prevents rapid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class StreamingDataSource final

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

// Environment ID from the most recent successful stream response. Read
// and written only from the SSE client's callbacks.
std::optional<std::string> environment_id_;

std::shared_ptr<sse::Client> client_;
};
} // namespace launchdarkly::server_side::data_systems
33 changes: 33 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,33 @@
#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());
}

} // 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,14 @@
if (closed_future_.IsFinished()) {
return;
}
if (auto* cs =
std::get_if<FDv2SourceResult::ChangeSet>(&result.value)) {
if (cs->change_set.environment_id) {
environment_id_ = cs->change_set.environment_id;
} else {
cs->change_set.environment_id = environment_id_;
}
}
if (pending_promise_) {
promise = std::move(pending_promise_);
pending_promise_.reset();
Expand Down Expand Up @@ -80,7 +88,7 @@
state->Notify(FDv2SourceResult{FDv2SourceResult::ChangeSet{
data_model::ChangeSet<data_interfaces::ChangeSetData>{
data_model::ChangeSetType::kFull, std::move(changes),
data_model::Selector{}}}});
data_model::Selector{}, std::move(data_set.environment_id)}}});
}

void FDv1AdapterSynchronizer::ConvertingDestination::Upsert(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ class FDv1AdapterSynchronizer final
std::optional<async::Promise<data_interfaces::FDv2SourceResult>>
pending_promise_;
std::deque<data_interfaces::FDv2SourceResult> result_queue_;
// Environment ID from the most recent FDv1 payload which carried one,
// stamped onto change sets which don't carry their own.
std::optional<std::string> environment_id_;
};

/**
Expand Down
Loading
Loading