Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions contract-tests/server-contract-tests/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ int main(int argc, char* argv[]) {
srv.add_capability("evaluation-hooks");
srv.add_capability("track-hooks");
srv.add_capability("wrapper");
srv.add_capability("instance-id");

net::signal_set signals{ioc, SIGINT, SIGTERM};

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 @@ -20,6 +20,8 @@ target_sources(${LIBNAME}
client.cpp
client_impl.cpp
data_source_status.cpp
instance_id.hpp
instance_id.cpp
config/config.cpp
config/config_builder.cpp
config/builders/data_system/background_sync_builder.cpp
Expand Down
8 changes: 8 additions & 0 deletions libs/server-sdk/src/client_impl.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#include "client_impl.hpp"

#include "all_flags_state/all_flags_state_builder.hpp"
Expand All @@ -5,6 +5,7 @@
#include "data_systems/lazy_load/lazy_load_system.hpp"
#include "data_systems/offline.hpp"
#include "evaluation/evaluation_stack.hpp"
#include "instance_id.hpp"
#include "prereq_event_recorder/prereq_event_recorder.hpp"

#include "data_interfaces/system/idata_system.hpp"
Expand Down Expand Up @@ -119,6 +120,13 @@
.Header("user-agent", "CPPServer/" + version)
.Header("authorization", config.SdkKey())
.Header("x-launchdarkly-tags", config.ApplicationTag())
// Per SCMP-server-connection-minutes-polling, every polling
// request must carry a per-instance GUID v4. We attach it to the
// shared HTTP properties so it's also present on streaming and
// event requests, and we generate it here (once during
// ClientImpl construction) so it remains stable for the lifetime
// of the SDK instance.
.Header(kInstanceIdHeader, MakeInstanceId())
.Build()),
logger_(MakeLogger(config.Logging())),
ioc_(kAsioConcurrencyHint),
Expand Down
18 changes: 18 additions & 0 deletions libs/server-sdk/src/instance_id.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include "instance_id.hpp"

#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>

namespace launchdarkly::server_side {

std::string MakeInstanceId() {
// boost::uuids::random_generator emits a version 4 (random) UUID, which is
// what the SCMP spec requires. The generator carries state (an internal
// RNG), so constructing it on every call is fine for our use case where we
// only call MakeInstanceId once per SDK instance.
static thread_local boost::uuids::random_generator generator;
Comment on lines +10 to +14

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// boost::uuids::random_generator emits a version 4 (random) UUID, which is
// what the SCMP spec requires. The generator carries state (an internal
// RNG), so constructing it on every call is fine for our use case where we
// only call MakeInstanceId once per SDK instance.
static thread_local boost::uuids::random_generator generator;
// boost::uuids::random_generator emits a version 4 (random) UUID, which is
// what the SCMP spec requires. The generator carries state (an internal RNG
// seeded from system entropy on construction), so we cache it per thread to
// avoid redundant entropy draws on repeated calls.
static thread_local boost::uuids::random_generator generator;

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.

Done -- updated the comment to your wording in 9ecdc7e.

return boost::uuids::to_string(generator());
}

} // namespace launchdarkly::server_side
28 changes: 28 additions & 0 deletions libs/server-sdk/src/instance_id.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#pragma once

#include <string>

namespace launchdarkly::server_side {

/**
* Name of the HTTP header used to identify this SDK instance for the purpose of
* estimating server-connection-minutes when polling. The value is a v4 UUID
* that is generated once per SDK instance and remains constant for the
* lifetime of the client.
*
* See: sdk-specs / SCMP-server-connection-minutes-polling.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* See: sdk-specs / SCMP-server-connection-minutes-polling.

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.

Done -- removed the spec reference line in 9ecdc7e.

*/
inline constexpr char const* kInstanceIdHeader = "X-LaunchDarkly-Instance-Id";

/**
* Generate a fresh v4 UUID suitable for use as the value of the
* X-LaunchDarkly-Instance-Id header. Each call returns a new identifier;
* callers are expected to generate the value exactly once per SDK instance
* and reuse it for the lifetime of that instance.
*
* @return A string formatted as a lowercase v4 UUID, e.g.
* "550e8400-e29b-41d4-a716-446655440000".
*/
[[nodiscard]] std::string MakeInstanceId();

} // namespace launchdarkly::server_side
75 changes: 75 additions & 0 deletions libs/server-sdk/tests/instance_id_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#include <gtest/gtest.h>

#include "instance_id.hpp"

#include <launchdarkly/server_side/client.hpp>
#include <launchdarkly/server_side/config/config_builder.hpp>

#include <cstddef>
#include <regex>
#include <set>
#include <string>

using namespace launchdarkly;
using namespace launchdarkly::server_side;

namespace {

// Matches a canonical UUID v4 in lowercase hex:
// xxxxxxxx-xxxx-4xxx-Yxxx-xxxxxxxxxxxx
// where 'Y' is one of 8, 9, a, or b (RFC 4122 variant).
bool IsUuidV4(std::string const& s) {
static std::regex const re(
"^[0-9a-f]{8}-"
"[0-9a-f]{4}-"
"4[0-9a-f]{3}-"
"[89ab][0-9a-f]{3}-"
"[0-9a-f]{12}$");
return std::regex_match(s, re);
}

} // namespace

// Spec: SCMP-server-connection-minutes-polling section 1.1 requires the
// X-LaunchDarkly-Instance-Id value to be a v4 UUID.
TEST(InstanceIdTest, GeneratedValueIsUuidV4) {
auto id = MakeInstanceId();
ASSERT_FALSE(id.empty()) << "MakeInstanceId returned an empty string";
EXPECT_TRUE(IsUuidV4(id))
<< "MakeInstanceId returned " << id << " which is not a v4 UUID";
}

// Each invocation must yield a different value; spec requires "the GUID MUST
// be used uniquely for this purpose".
TEST(InstanceIdTest, GeneratedValuesAreUnique) {
constexpr int kSamples = 100;
std::set<std::string> seen;
for (int i = 0; i < kSamples; ++i) {
auto id = MakeInstanceId();
ASSERT_FALSE(id.empty());
EXPECT_TRUE(seen.insert(id).second)
<< "duplicate UUID emitted from MakeInstanceId: " << id;
}
EXPECT_EQ(seen.size(), static_cast<std::size_t>(kSamples));
}

// The header name constant must match the spec verbatim. This guards against
// accidental renaming (the header name is part of the wire contract).
TEST(InstanceIdTest, HeaderNameMatchesSpec) {
EXPECT_STREQ(kInstanceIdHeader, "X-LaunchDarkly-Instance-Id");
}

// Sanity-check that a Client can be constructed; the integration that the
// instance-id header actually ends up on outbound requests is covered by the
// cross-SDK contract test harness (capability: "instance-id").
TEST(InstanceIdTest, ClientConstructsWithInstanceIdHeader) {
// Building a Client exercises the code path that stamps the instance-id
// header into the shared HttpProperties. We can't observe the header
// directly from the public API, so this test simply asserts that the
// construction succeeds; the spec-level guarantees about the header value
// are exercised by GeneratedValueIsUuidV4 and GeneratedValuesAreUnique,
// and the on-the-wire guarantee is covered by the cross-SDK contract test
// harness (capability: "instance-id").
Client client(ConfigBuilder("sdk-123").Build().value());
EXPECT_NE(client.Version(), nullptr);
}
Loading