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
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
// 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;
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
26 changes: 26 additions & 0 deletions libs/server-sdk/src/instance_id.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#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.
*/
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