Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c416964
feat: implement JsonDestination
cwaldren-ld Nov 22, 2023
5711b7e
update SerializedItemDescriptor
cwaldren-ld Nov 22, 2023
c3fb9be
implementing JSON destination
cwaldren-ld Nov 22, 2023
623e174
update comments on ISerializedDestination
cwaldren-ld Nov 22, 2023
533d3a6
update comments
cwaldren-ld Nov 22, 2023
094905c
Merge branch 'feat/data-system' into cw/json-destination-impl
cwaldren-ld Nov 27, 2023
4ca20a9
add WriteMinimal for std::vector, use in flag/segment serialization
cwaldren-ld Nov 27, 2023
d49e37b
add bool perator for ISerializedItemKind and descriptor
cwaldren-ld Nov 27, 2023
d54ae62
add kinds.cpp to CMakeLists.txt
cwaldren-ld Nov 27, 2023
e532133
add tests for JsonDestination
cwaldren-ld Nov 27, 2023
e2e4d40
more tests
cwaldren-ld Nov 27, 2023
fb6971c
more tests
cwaldren-ld Nov 27, 2023
d904a77
add test fixture
cwaldren-ld Nov 27, 2023
055f40a
typo
cwaldren-ld Nov 27, 2023
4b88673
update flag serialization test in internal
cwaldren-ld Nov 27, 2023
f00bf07
update unbounded segment serialization test
cwaldren-ld Nov 27, 2023
b347f33
Merge branch 'feat/data-system' into cw/json-destination-impl
cwaldren-ld Nov 27, 2023
0ca8b5d
try to fix gmock segfault
cwaldren-ld Nov 28, 2023
d49690d
another attempt
cwaldren-ld Nov 28, 2023
ff5fd5a
try to not have a sigsegv
cwaldren-ld Nov 28, 2023
2d48311
one more attempt
cwaldren-ld Nov 28, 2023
753eb8e
think I was using gmock wrong, use testing::ReturnRef
cwaldren-ld Nov 28, 2023
af56baa
remove identity test
cwaldren-ld Nov 28, 2023
54e6953
try only instantiating the test fixture
cwaldren-ld Nov 28, 2023
8f9135b
29th try
cwaldren-ld Nov 28, 2023
58122a2
fix some includes
cwaldren-ld Nov 29, 2023
c4bd414
add back missing tests
cwaldren-ld Nov 29, 2023
ea054f9
fix broken formatting on json_destination_test.cpp
cwaldren-ld Nov 29, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,14 @@ class ISerializedItemKind {
protected:
ISerializedItemKind() = default;
};

/**
* @brief Used in test assertions; two ISerializedItemKinds are regarded as
* the same if they have the same namespace.
*/
inline bool operator==(ISerializedItemKind const& lhs,
ISerializedItemKind const& rhs) {
return lhs.Namespace() == rhs.Namespace();
}

} // namespace launchdarkly::server_side::integrations
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <cstdint>
#include <optional>
#include <ostream>
#include <string>

namespace launchdarkly::server_side::integrations {
Expand All @@ -24,6 +25,47 @@ struct SerializedItemDescriptor {
* std::nullopt for deleted items.
*/
std::optional<std::string> serializedItem;

/**
* @brief Constructs a SerializedItemDescriptor from a version and a
* serialized item.
* @param version Version of item.
* @param data Serialized item.
* @return SerializedItemDescriptor.
*/
static SerializedItemDescriptor Present(std::uint64_t version,
std::string data) {
return SerializedItemDescriptor{version, false, std::move(data)};
}

/**
* @brief Constructs a SerializedItemDescriptor from a version and a
* tombstone.
*
* This is used when an item is deleted: the tombstone can be stored in
* place of the item, and the version checked in the future. Without the
* tombstone, out-of-order data updates could "resurrect" a deleted item.
*
* @param version Version of the item.
* @param tombstone_rep Serialized tombstone representation of the item.
* @return SerializedItemDescriptor.
*/
static SerializedItemDescriptor Absent(std::uint64_t const version,
std::string tombstone_rep) {
return SerializedItemDescriptor{version, true,
std::move(tombstone_rep)};
}
};

inline bool operator==(SerializedItemDescriptor const& lhs,
SerializedItemDescriptor const& rhs) {
return lhs.version == rhs.version && lhs.deleted == rhs.deleted &&
lhs.serializedItem == rhs.serializedItem;
}

inline void PrintTo(SerializedItemDescriptor const& item, std::ostream* os) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Used for pretty-er failures in google test.

*os << "{version=" << item.version << ", deleted=" << item.deleted
<< ", item=" << item.serializedItem.value_or("nullopt") << "}";
}

} // namespace launchdarkly::server_side::integrations
4 changes: 4 additions & 0 deletions libs/server-sdk/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ target_sources(${LIBNAME}
data_components/memory_store/memory_store.cpp
data_components/serialization_adapters/json_deserializer.hpp
data_components/serialization_adapters/json_deserializer.cpp
data_components/serialization_adapters/json_destination.hpp
data_components/serialization_adapters/json_destination.cpp
data_components/kinds/kinds.hpp
data_components/kinds/kinds.cpp
data_systems/background_sync/sources/noop/null_data_source.hpp
data_systems/background_sync/sources/noop/null_data_source.cpp
data_systems/background_sync/sources/polling/polling_data_source.hpp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
#include "../dependency_tracker/data_kind.hpp"
#include "../dependency_tracker/tagged_data.hpp"

#include <launchdarkly/connection.hpp>

#include <array>
#include <chrono>
#include <functional>
#include <memory>
#include <optional>
#include <ostream>
#include <string>
#include <unordered_map>
#include <vector>

namespace launchdarkly::server_side::data_components {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,129 @@
#include "json_destination.hpp"

#include <launchdarkly/serialization/json_flag.hpp>
#include <launchdarkly/serialization/json_segment.hpp>

namespace launchdarkly::server_side::data_components {

JsonDestination::JsonDestination(
data_interfaces::ISerializedDestination& destination)
: dest_(destination) {}
using data_interfaces::ISerializedDestination;
using integrations::SerializedItemDescriptor;

FlagKind const JsonDestination::Kinds::Flag = FlagKind();
SegmentKind const JsonDestination::Kinds::Segment = SegmentKind();

/**
* @brief Creates a boost::json::value representing a tombstone for a given
* ItemDescriptor. The tombstone includes 'deleted', 'key', and 'version'
* fields.
*
* @param T Type of descriptor.
* @param key Key of item.
* @param desc The descriptor.
* @return Tombstone suitable for serialization.
*/
template <typename T>
boost::json::value Tombstone(std::string const& key,
data_model::ItemDescriptor<T> const& desc) {
boost::json::object tombstone;
tombstone.emplace("key", key);
tombstone.emplace("version", desc.version);
tombstone.emplace("deleted", true);
return tombstone;
}

/**
* @brief Creates a SerializedItemDescriptor for a given ItemDescriptor. The
* SerializedItemDescriptor either represents an item that is present, or one
* that is absent.
*
* @param T Type of descriptor.
* @param key Key of item.
* @param desc The descriptor.
* @return SerializedItemDescriptor suitable for forwarding to an
* ISerializedDestination.
*/
template <typename T>
SerializedItemDescriptor Serialize(std::string const& key,
data_model::ItemDescriptor<T> const& desc) {
return desc.item
? SerializedItemDescriptor::Present(
desc.version, boost::json::serialize(
boost::json::value_from(*desc.item)))
: SerializedItemDescriptor::Absent(
desc.version,
boost::json::serialize(Tombstone(key, desc)));
}

JsonDestination::JsonDestination(Logger const& logger,
ISerializedDestination& destination)
: logger_(logger),
dest_(destination),
ident_(dest_.Identity() + " (JSON)") {}

std::string const& JsonDestination::Identity() const {
return ident_;
}

void JsonDestination::Init(data_model::SDKDataSet data_set) {
// TODO: serialize and forward to dest_.Init
// TODO(sc-225327): Topographical sort of flag dependencies

std::vector<ISerializedDestination::ItemCollection> items;

ISerializedDestination::OrderedNamepace flags;
for (auto const& [key, descriptor] : data_set.flags) {
flags.emplace_back(key, Serialize(key, descriptor));
}
std::sort(flags.begin(), flags.end(), [](auto const& lhs, auto const& rhs) {
return lhs.first < rhs.first;
});
items.emplace_back(Kinds::Flag, std::move(flags));

ISerializedDestination::OrderedNamepace segments;
for (auto const& [key, descriptor] : data_set.segments) {
segments.emplace_back(key, Serialize(key, descriptor));
}
std::sort(
segments.begin(), segments.end(),
[](auto const& lhs, auto const& rhs) { return lhs.first < rhs.first; });
items.emplace_back(Kinds::Segment, std::move(segments));

if (auto const result = dest_.Init(std::move(items));
result != ISerializedDestination::InitResult::kSuccess) {
LD_LOG(logger_, LogLevel::kError)
<< dest_.Identity() << ": failed to store initial SDK data";
}
}

void JsonDestination::Upsert(std::string const& key,
data_model::FlagDescriptor flag) {
// TODO: serialize and forward to dest_.Upsert
data_model::FlagDescriptor const flag) {
LogUpsertResult(key, "flag",
dest_.Upsert(Kinds::Flag, key, Serialize(key, flag)));
}

void JsonDestination::Upsert(std::string const& key,
data_model::SegmentDescriptor segment) {
// TODO: serialize and forward to dest_.Upsert
data_model::SegmentDescriptor const segment) {
LogUpsertResult(key, "segment",
dest_.Upsert(Kinds::Segment, key, Serialize(key, segment)));
}

std::string const& JsonDestination::Identity() const {
return dest_.Identity();
void JsonDestination::LogUpsertResult(
std::string const& key,
std::string const& data_type,
ISerializedDestination::UpsertResult const& result) const {
switch (result) {
case ISerializedDestination::UpsertResult::kSuccess:
break;
case ISerializedDestination::UpsertResult::kError:
LD_LOG(logger_, LogLevel::kError)
<< dest_.Identity() << ": failed to update " << data_type << " "
<< key;
break;
case ISerializedDestination::UpsertResult::kNotUpdated:
LD_LOG(logger_, LogLevel::kDebug)
<< dest_.Identity() << ": " << data_type << " " << key
<< " not updated; data was stale";
break;
}
}

} // namespace launchdarkly::server_side::data_components
Original file line number Diff line number Diff line change
@@ -1,24 +1,98 @@
#pragma once

#include "../../data_components/kinds/kinds.hpp"
#include "../../data_interfaces/destination/idestination.hpp"
#include "../../data_interfaces/destination/iserialized_destination.hpp"

#include <launchdarkly/logging/logger.hpp>

#include <string>

namespace launchdarkly::server_side::data_components {

class JsonDestination : public data_interfaces::IDestination {
/**
* @brief JsonDestination is responsible for converting flag and segment
* models into serialized data suitable for storage in an
* ISerializedDestination.
*
* By encapsulating the serialization logic here, different adapters can be
* swapped in if our serialization format ever changes.
*
* JsonDestination does not currently initialize ISerializedDestination with a
* flag-dependency-order payload, which is required to minimize bugs in
* stores without atomic transactions (e.g. DynamoDB).
*
* Instead, it sorts items within a collection using 'operator<' on their keys,
* giving which is enough determinism for testing purposes.
*
* TODO(sc-225327): Implement topographic sort as prerequisite for DynamoDB.
*
*/
class JsonDestination final : public data_interfaces::IDestination {
public:
JsonDestination(data_interfaces::ISerializedDestination& destination);
/**
* @brief Construct the JsonDestination with the given
* ISerializedDestination. Calls to Upsert will trigger serialization and
* store to the destination.
* @param logger Used for logging storage errors.
* @param destination Where data should be forwarded.
*/
JsonDestination(Logger const& logger,
data_interfaces::ISerializedDestination& destination);

virtual void Init(data_model::SDKDataSet data_set) override;
/**
* @brief Initialize the destination with an SDK data set.
* @param data_set The initial data.
*/
void Init(data_model::SDKDataSet data_set) override;

/**
* @brief Upsert data for the flag named by key.
*
* If the descriptor represents a deleted item, a tombstone will
* be forwarded to the ISerializedDestination.
*
* @param key Key of flag.
* @param flag Descriptor of flag.
*/
void Upsert(std::string const& key,
data_model::FlagDescriptor flag) override;

/**
* @brief Upsert data for the segment named by key.
*
* If the descriptor represents a deleted item, a tombstone will
* be forwarded to the ISerializedDestination.
*
* @param key Key of segment.
* @param segment Descriptor of segment.
*/
void Upsert(std::string const& key,
data_model::SegmentDescriptor segment) override;

/**
* @return Identity of this destination. Used in logs.
*/
[[nodiscard]] std::string const& Identity() const override;

/**
* @brief These are public so they can be referenced in tests.
*/
struct Kinds {
static FlagKind const Flag;
static SegmentKind const Segment;
};

private:
void LogUpsertResult(
std::string const& key,
std::string const& data_type,
data_interfaces::ISerializedDestination::UpsertResult const& result)
const;

Logger const& logger_;
data_interfaces::ISerializedDestination& dest_;
std::string const ident_;
};

} // namespace launchdarkly::server_side::data_components
Loading