Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions libs/internal/include/launchdarkly/data_model/change_set.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

#include <launchdarkly/data_model/selector.hpp>

namespace launchdarkly::data_model {

enum class ChangeSetType {
kFull = 0,
kPartial = 1,
kNone = 2,
};

template <typename T>
struct ChangeSet {
ChangeSetType type;
T data;
Selector selector;
};

} // namespace launchdarkly::data_model
23 changes: 11 additions & 12 deletions libs/internal/include/launchdarkly/data_model/fdv2_change.hpp
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
#pragma once

#include <launchdarkly/data_model/flag.hpp>
#include <launchdarkly/data_model/item_descriptor.hpp>
#include <launchdarkly/data_model/segment.hpp>
#include <launchdarkly/data_model/change_set.hpp>
#include <launchdarkly/data_model/selector.hpp>

#include <boost/json/value.hpp>

#include <cstdint>
#include <string>
#include <variant>
#include <vector>

namespace launchdarkly::data_model {

struct FDv2Change {
enum class ChangeType { kPut, kDelete };

ChangeType change_type;
std::string kind;
std::string key;
std::variant<ItemDescriptor<Flag>, ItemDescriptor<Segment>> object;
uint64_t version;
boost::json::value object; // set for kPut; unused for kDelete
};

struct FDv2ChangeSet {
enum class Type {
kFull = 0,
kPartial = 1,
kNone = 2,
};

Type type;
ChangeSetType type;
std::vector<FDv2Change> changes;
Selector selector;
};
Expand Down
95 changes: 13 additions & 82 deletions libs/internal/src/fdv2_protocol_handler.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
#include <launchdarkly/fdv2_protocol_handler.hpp>

#include <launchdarkly/data_model/flag.hpp>
#include <launchdarkly/data_model/item_descriptor.hpp>
#include <launchdarkly/data_model/segment.hpp>
#include <launchdarkly/serialization/json_flag.hpp>
#include <launchdarkly/serialization/json_segment.hpp>

#include <boost/json.hpp>
#include <tl/expected.hpp>

Expand All @@ -20,66 +14,6 @@ static char const* const kGoodbye = "goodbye";

using Error = FDv2ProtocolHandler::Error;

// Returns the parsed FDv2Change on success, nullopt for unknown kinds (which
// should be silently skipped for forward-compatibility), or an Error if
// a known kind fails to deserialize.
static tl::expected<std::optional<data_model::FDv2Change>, Error> ParsePut(
PutObject const& put) {
if (put.kind == "flag") {
auto result = boost::json::value_to<
tl::expected<std::optional<data_model::Flag>, JsonError>>(
put.object);
// One bad flag aborts the entire transfer so the store is never
// left in a partially-updated state.
if (!result) {
return tl::make_unexpected(Error::JsonParseError(
result.error(),
"could not deserialize flag '" + put.key + "'"));
}
if (!result->has_value()) {
return tl::make_unexpected(Error::JsonParseError(
"flag '" + put.key + "' object was null"));
}
return data_model::FDv2Change{
put.key,
data_model::ItemDescriptor<data_model::Flag>{std::move(**result)}};
}
if (put.kind == "segment") {
auto result = boost::json::value_to<
tl::expected<std::optional<data_model::Segment>, JsonError>>(
put.object);
// One bad segment aborts the entire transfer so the store is never
// left in a partially-updated state.
if (!result) {
return tl::make_unexpected(Error::JsonParseError(
result.error(),
"could not deserialize segment '" + put.key + "'"));
}
if (!result->has_value()) {
return tl::make_unexpected(Error::JsonParseError(
"segment '" + put.key + "' object was null"));
}
return data_model::FDv2Change{
put.key, data_model::ItemDescriptor<data_model::Segment>{
std::move(**result)}};
}
// Silently skip unknown kinds for forward-compatibility.
return std::nullopt;
}

static data_model::FDv2Change MakeDeleteChange(DeleteObject const& del) {
if (del.kind == "flag") {
return data_model::FDv2Change{
del.key,
data_model::ItemDescriptor<data_model::Flag>{
data_model::Tombstone{static_cast<uint64_t>(del.version)}}};
}
return data_model::FDv2Change{
del.key,
data_model::ItemDescriptor<data_model::Segment>{
data_model::Tombstone{static_cast<uint64_t>(del.version)}}};
}

FDv2ProtocolHandler::Result FDv2ProtocolHandler::HandleEvent(
std::string_view event_type,
boost::json::value const& data) {
Expand Down Expand Up @@ -137,7 +71,7 @@ FDv2ProtocolHandler::Result FDv2ProtocolHandler::HandleServerIntent(
// kNone or kUnknown: emit an empty changeset immediately.
state_ = State::kInactive;
return data_model::FDv2ChangeSet{
data_model::FDv2ChangeSet::Type::kNone, {}, data_model::Selector{}};
data_model::ChangeSetType::kNone, {}, data_model::Selector{}};
}
return std::monostate{};
}
Expand All @@ -158,14 +92,10 @@ FDv2ProtocolHandler::Result FDv2ProtocolHandler::HandlePutObject(
Reset();
return Error::JsonParseError("put-object data was null");
}
auto change = ParsePut(**result);
if (!change) {
Reset();
return std::move(change.error());
}
if (*change) {
changes_.push_back(std::move(**change));
}
auto const& put = **result;
changes_.push_back(data_model::FDv2Change{
data_model::FDv2Change::ChangeType::kPut, put.kind, put.key,
static_cast<uint64_t>(put.version), put.object});
return std::monostate{};
}

Expand All @@ -186,11 +116,12 @@ FDv2ProtocolHandler::Result FDv2ProtocolHandler::HandleDeleteObject(
return Error::JsonParseError("delete-object data was null");
}
auto const& del = **result;
// Silently skip unknown kinds for forward-compatibility.
if (del.kind != "flag" && del.kind != "segment") {
return std::monostate{};
}
changes_.push_back(MakeDeleteChange(del));
changes_.push_back(
data_model::FDv2Change{data_model::FDv2Change::ChangeType::kDelete,
del.kind,
del.key,
static_cast<uint64_t>(del.version),
{}});
return std::monostate{};
}

Expand All @@ -215,8 +146,8 @@ FDv2ProtocolHandler::Result FDv2ProtocolHandler::HandlePayloadTransferred(
}
auto const& transferred = **result;
auto type = (state_ == State::kPartial)
? data_model::FDv2ChangeSet::Type::kPartial
: data_model::FDv2ChangeSet::Type::kFull;
? data_model::ChangeSetType::kPartial
: data_model::ChangeSetType::kFull;
data_model::FDv2ChangeSet changeset{
type, std::move(changes_),
data_model::Selector{data_model::Selector::State{transferred.version,
Expand Down
42 changes: 12 additions & 30 deletions libs/internal/tests/fdv2_protocol_handler_test.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <gtest/gtest.h>

#include <launchdarkly/data_model/change_set.hpp>
#include <launchdarkly/fdv2_protocol_handler.hpp>

#include <boost/json.hpp>
Expand Down Expand Up @@ -56,7 +57,7 @@ TEST(FDv2ProtocolHandlerTest, NoneIntentEmitsEmptyChangeSetImmediately) {

auto* cs = std::get_if<data_model::FDv2ChangeSet>(&result);
ASSERT_NE(cs, nullptr);
EXPECT_EQ(cs->type, data_model::FDv2ChangeSet::Type::kNone);
EXPECT_EQ(cs->type, data_model::ChangeSetType::kNone);
EXPECT_TRUE(cs->changes.empty());
EXPECT_FALSE(cs->selector.value.has_value());
}
Expand All @@ -81,7 +82,7 @@ TEST(FDv2ProtocolHandlerTest, FullIntentEmitsChangeSetOnPayloadTransferred) {

auto* cs = std::get_if<data_model::FDv2ChangeSet>(&r3);
ASSERT_NE(cs, nullptr);
EXPECT_EQ(cs->type, data_model::FDv2ChangeSet::Type::kFull);
EXPECT_EQ(cs->type, data_model::ChangeSetType::kFull);
EXPECT_EQ(cs->changes.size(), 1u);
EXPECT_EQ(cs->changes[0].key, "my-flag");
ASSERT_TRUE(cs->selector.value.has_value());
Expand All @@ -105,7 +106,7 @@ TEST(FDv2ProtocolHandlerTest, FullIntentAccumulatesMultipleObjects) {

auto* cs = std::get_if<data_model::FDv2ChangeSet>(&result);
ASSERT_NE(cs, nullptr);
EXPECT_EQ(cs->type, data_model::FDv2ChangeSet::Type::kFull);
EXPECT_EQ(cs->type, data_model::ChangeSetType::kFull);
EXPECT_EQ(cs->changes.size(), 3u);
}

Expand All @@ -132,7 +133,7 @@ TEST(FDv2ProtocolHandlerTest,

auto* cs = std::get_if<data_model::FDv2ChangeSet>(&result);
ASSERT_NE(cs, nullptr);
EXPECT_EQ(cs->type, data_model::FDv2ChangeSet::Type::kPartial);
EXPECT_EQ(cs->type, data_model::ChangeSetType::kPartial);
ASSERT_EQ(cs->changes.size(), 1u);
EXPECT_EQ(cs->changes[0].key, "flag-2");
}
Expand All @@ -153,7 +154,7 @@ TEST(FDv2ProtocolHandlerTest, PartialIntentEmitsPartialChangeSet) {

auto* cs = std::get_if<data_model::FDv2ChangeSet>(&result);
ASSERT_NE(cs, nullptr);
EXPECT_EQ(cs->type, data_model::FDv2ChangeSet::Type::kPartial);
EXPECT_EQ(cs->type, data_model::ChangeSetType::kPartial);
EXPECT_EQ(cs->changes.size(), 1u);
EXPECT_EQ(cs->changes[0].key, "my-seg");
ASSERT_TRUE(cs->selector.value.has_value());
Expand All @@ -164,7 +165,7 @@ TEST(FDv2ProtocolHandlerTest, PartialIntentEmitsPartialChangeSet) {
// Unknown kind in put-object → silently skipped
// ============================================================================

TEST(FDv2ProtocolHandlerTest, UnknownKindInPutObjectIsSilentlySkipped) {
TEST(FDv2ProtocolHandlerTest, UnknownKindInPutObjectIsPassedThrough) {
FDv2ProtocolHandler handler;

handler.HandleEvent("server-intent", MakeServerIntent("xfer-full"));
Expand All @@ -179,12 +180,11 @@ TEST(FDv2ProtocolHandlerTest, UnknownKindInPutObjectIsSilentlySkipped) {

auto* cs = std::get_if<data_model::FDv2ChangeSet>(&result);
ASSERT_NE(cs, nullptr);
// Only the known kind (flag) should appear.
EXPECT_EQ(cs->changes.size(), 1u);
EXPECT_EQ(cs->changes[0].key, "my-flag");
// All kinds pass through; filtering happens in the translator.
EXPECT_EQ(cs->changes.size(), 2u);
}

TEST(FDv2ProtocolHandlerTest, UnknownKindInDeleteObjectIsSilentlySkipped) {
TEST(FDv2ProtocolHandlerTest, UnknownKindInDeleteObjectIsPassedThrough) {
FDv2ProtocolHandler handler;

handler.HandleEvent("server-intent", MakeServerIntent("xfer-full"));
Expand All @@ -198,9 +198,8 @@ TEST(FDv2ProtocolHandlerTest, UnknownKindInDeleteObjectIsSilentlySkipped) {

auto* cs = std::get_if<data_model::FDv2ChangeSet>(&result);
ASSERT_NE(cs, nullptr);
// Only the known kind (flag) should appear.
EXPECT_EQ(cs->changes.size(), 1u);
EXPECT_EQ(cs->changes[0].key, "my-flag");
// All kinds pass through; filtering happens in the translator.
EXPECT_EQ(cs->changes.size(), 2u);
}

// ============================================================================
Expand Down Expand Up @@ -252,23 +251,6 @@ TEST(FDv2ProtocolHandlerTest, ErrorEventWithIdSetsServerId) {
EXPECT_EQ(err->server_error->reason, "overloaded");
}

TEST(FDv2ProtocolHandlerTest, MalformedPutObjectReturnsJsonError) {
FDv2ProtocolHandler handler;

handler.HandleEvent("server-intent", MakeServerIntent("xfer-full"));

// 'object' field is missing required flag fields — deserialisation fails.
auto result = handler.HandleEvent(
"put-object",
boost::json::parse(
R"({"version":1,"kind":"flag","key":"f","object":{}})"));

auto* err = std::get_if<FDv2ProtocolHandler::Error>(&result);
ASSERT_NE(err, nullptr);
EXPECT_EQ(err->kind, FDv2ProtocolHandler::Error::Kind::kJsonError);
EXPECT_TRUE(err->json_error.has_value());
}

// ============================================================================
// goodbye event → return Goodbye
// ============================================================================
Expand Down
2 changes: 2 additions & 0 deletions libs/server-sdk/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ target_sources(${LIBNAME}
data_systems/background_sync/detail/payload_filter_validation/payload_filter_validation.cpp
data_systems/background_sync/sources/polling/polling_data_source.hpp
data_systems/background_sync/sources/polling/polling_data_source.cpp
data_systems/fdv2/fdv2_changeset_translation.hpp
data_systems/fdv2/fdv2_changeset_translation.cpp
data_systems/fdv2/fdv2_polling_impl.hpp
data_systems/fdv2/fdv2_polling_impl.cpp
data_systems/fdv2/polling_initializer.hpp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,16 @@ bool MemoryStore::RemoveSegment(std::string const& key) {
return segments_.erase(key) == 1;
}

void MemoryStore::Apply(data_model::FDv2ChangeSet changeSet) {
void MemoryStore::Apply(
data_model::ChangeSet<data_interfaces::ChangeSetData> changeSet) {
std::lock_guard lock{data_mutex_};

switch (changeSet.type) {
case data_model::FDv2ChangeSet::Type::kNone:
case data_model::ChangeSetType::kNone:
return;
case data_model::FDv2ChangeSet::Type::kPartial:
case data_model::ChangeSetType::kPartial:
break;
case data_model::FDv2ChangeSet::Type::kFull:
case data_model::ChangeSetType::kFull:
initialized_ = true;
flags_.clear();
segments_.clear();
Expand All @@ -101,7 +102,7 @@ void MemoryStore::Apply(data_model::FDv2ChangeSet changeSet) {
detail::unreachable();
}

for (auto& change : changeSet.changes) {
for (auto& change : changeSet.data) {
if (std::holds_alternative<data_model::FlagDescriptor>(change.object)) {
flags_[change.key] = std::make_shared<data_model::FlagDescriptor>(
std::move(std::get<data_model::FlagDescriptor>(change.object)));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#pragma once

#include "../../data_interfaces/destination/idestination.hpp"
#include "../../data_interfaces/item_change.hpp"
#include "../../data_interfaces/store/istore.hpp"

#include <launchdarkly/data_model/fdv2_change.hpp>
#include <launchdarkly/data_model/change_set.hpp>

#include <memory>
#include <mutex>
Expand Down Expand Up @@ -46,7 +47,7 @@ class MemoryStore final : public data_interfaces::IStore,

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

void Apply(data_model::FDv2ChangeSet changeSet);
void Apply(data_model::ChangeSet<data_interfaces::ChangeSetData> changeSet);

MemoryStore() = default;
~MemoryStore() override = default;
Expand Down
22 changes: 22 additions & 0 deletions libs/server-sdk/src/data_interfaces/item_change.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include <launchdarkly/data_model/flag.hpp>
#include <launchdarkly/data_model/item_descriptor.hpp>
#include <launchdarkly/data_model/segment.hpp>

#include <string>
#include <variant>
#include <vector>

namespace launchdarkly::server_side::data_interfaces {

struct ItemChange {
std::string key;
std::variant<data_model::ItemDescriptor<data_model::Flag>,
data_model::ItemDescriptor<data_model::Segment>>
object;
};

using ChangeSetData = std::vector<ItemChange>;

} // namespace launchdarkly::server_side::data_interfaces
Loading
Loading