-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathlazy_load_system.cpp
More file actions
195 lines (171 loc) · 8.3 KB
/
Copy pathlazy_load_system.cpp
File metadata and controls
195 lines (171 loc) · 8.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// The Lazy Load system is responsible for loading flags and segments from
// an underlying source when requested by the SDK's evaluation algorithm.
//
// This is fundamentally different than Background Sync, where all items
// are loaded into memory at initialization, and then updated asynchronously
// when changes arrive from LaunchDarkly.
// In this system, items are updated through a cache refresh process which
// depends on a TTL.
//
// This TTL is configured by the user, and represents a tradeoff between
// freshness (and consistency), and load/traffic to the underyling source.
//
// In the normal course of SDK operation, individual flags and segments loaded
// over time depending on the patterns of SDK usage present in an application.
// This generally spreads out the load on the source over time.
//
// A different usage pattern is caused by the SDK's "AllFlags" API, which
// evaluates all flags for a given context. Because this usage is potentially
// extremely costly (if there are thousands of flags or segments to be loaded),
// the Lazy Load system optimizes by performing a special "GetAll" operation
// on the underlying source. This operation fetches the entire set of a data
// kind (either flags or segments) in one trip, and then stores them in the
// cache.
//
// It's important that the "GetAll" operation also be bound by a TTL, so that
// repeated calls to "AllFlags" don't cause repeated calls to the source. The
// TTL for this operation is identical to the indivudal-item TTL configurable by
// the user.
//
// An implication of the current implementation is that if Lazy Load is being
// used to handle a "sparse" environment - that is, the environment is too big
// to load into memory, and so loading on demand is desirable - calling
// "AllFlags" will destroy that property because items are not actively evicted
// from the cache. On the other hand, that property could be used to prime the
// SDK's memory cache, preventing the need to individually load flags or
// segments.
//
// The current design does not perform active eviction when an item is stale
// because it is generally better to serve stale data than none at all. If the
// source is unavailable, the SDK will be able to indefinitely serve the last
// known values. Active eviction can be added in the future as a configurable
// option.
#include "lazy_load_system.hpp"
#include "../../data_components/serialization_adapters/json_deserializer.hpp"
namespace launchdarkly::server_side::data_systems {
integrations::FlagKind const LazyLoad::Kinds::Flag = integrations::FlagKind();
integrations::SegmentKind const LazyLoad::Kinds::Segment =
integrations::SegmentKind();
LazyLoad::LazyLoad(Logger const& logger,
config::built::LazyLoadConfig cfg,
data_components::DataSourceStatusManager& status_manager)
: LazyLoad(logger, std::move(cfg), status_manager, []() {
return std::chrono::steady_clock::now();
}) {}
LazyLoad::LazyLoad(Logger const& logger,
config::built::LazyLoadConfig cfg,
data_components::DataSourceStatusManager& status_manager,
TimeFn time)
: logger_(logger),
reader_(std::make_unique<data_components::JsonDeserializer>(
logger,
std::move(cfg.source))),
status_manager_(status_manager),
time_(std::move(time)),
fresh_duration_(cfg.refresh_ttl) {}
std::string const& LazyLoad::Identity() const {
static std::string id = "lazy load via " + reader_->Identity();
return id;
}
void LazyLoad::Initialize() {
status_manager_.SetState(DataSourceState::kInitializing);
// In lazy load (daemon) mode, the data system is always considered
// initialized immediately — it can fetch data on demand from the
// persistent store. This is consistent with Go, Java, and .NET SDKs
// which use a NullDataSource that immediately reports initialized.
//
// The store's $inited key state is a separate concern: if a Relay
// Proxy or other SDK hasn't set $inited, we log a warning but
// proceed. This matches the Node SDK pattern where the data source
// initializes immediately but the store state drives the warning.
if (!reader_->Initialized()) {
LD_LOG(logger_, LogLevel::kWarn)
<< "LazyLoad: the $inited key was not found in the store. "
"Evaluations will proceed using available data. Typically "
"a Relay Proxy or other SDK should set this key; verify "
"your configuration if this is unexpected.";
}
status_manager_.SetState(DataSourceState::kValid);
}
std::shared_ptr<data_model::FlagDescriptor> LazyLoad::GetFlag(
std::string const& key) const {
auto const state =
tracker_.State(data_components::DataKind::kFlag, key, time_());
return Get<std::shared_ptr<data_model::FlagDescriptor>>(
key, state, [this, &key]() { RefreshFlag(key); },
[this, &key]() { return cache_.GetFlag(key); });
}
std::shared_ptr<data_model::SegmentDescriptor> LazyLoad::GetSegment(
std::string const& key) const {
auto const state =
tracker_.State(data_components::DataKind::kSegment, key, time_());
return Get<std::shared_ptr<data_model::SegmentDescriptor>>(
key, state, [this, &key]() { RefreshSegment(key); },
[this, &key]() { return cache_.GetSegment(key); });
}
std::unordered_map<std::string, std::shared_ptr<data_model::FlagDescriptor>>
LazyLoad::AllFlags() const {
auto const state = tracker_.State(Keys::kAllFlags, time_());
return Get<std::unordered_map<std::string,
std::shared_ptr<data_model::FlagDescriptor>>>(
Keys::kAllFlags, state, [this]() { RefreshAllFlags(); },
[this]() { return cache_.AllFlags(); });
}
std::unordered_map<std::string, std::shared_ptr<data_model::SegmentDescriptor>>
LazyLoad::AllSegments() const {
auto const state = tracker_.State(Keys::kAllSegments, time_());
return Get<std::unordered_map<
std::string, std::shared_ptr<data_model::SegmentDescriptor>>>(
Keys::kAllSegments, state, [this]() { RefreshAllSegments(); },
[this]() { return cache_.AllSegments(); });
}
bool LazyLoad::Initialized() const {
/* In lazy load (daemon) mode, the data system is always considered
* initialized. It can serve evaluations on demand from the persistent
* store regardless of whether the $inited key has been set.
*
* This is consistent with Go/Java/.NET SDKs where the NullDataSource
* used in daemon mode always returns IsInitialized() = true. */
return true;
}
void LazyLoad::RefreshAllFlags() const {
RefreshAll<data_model::Flag>(Keys::kAllFlags,
data_components::DataKind::kFlag,
[this]() { return reader_->AllFlags(); });
}
void LazyLoad::RefreshAllSegments() const {
RefreshAll<data_model::Segment>(
Keys::kAllSegments, data_components::DataKind::kSegment,
[this]() { return reader_->AllSegments(); });
}
void LazyLoad::RefreshSegment(std::string const& segment_key) const {
RefreshItem<data_model::Segment>(
data_components::DataKind::kSegment, segment_key,
[this](std::string const& key) { return reader_->GetSegment(key); },
[this](std::string const& key) { return cache_.RemoveSegment(key); });
}
void LazyLoad::RefreshFlag(std::string const& flag_key) const {
RefreshItem<data_model::Flag>(
data_components::DataKind::kFlag, flag_key,
[this](std::string const& key) { return reader_->GetFlag(key); },
[this](std::string const& key) { return cache_.RemoveFlag(key); });
}
std::chrono::time_point<std::chrono::steady_clock> LazyLoad::ExpiryTime()
const {
return time_() +
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
fresh_duration_);
}
std::string LazyLoad::CacheTraceMsg(
data_components::ExpirationTracker::TrackState const state) {
switch (state) {
case data_components::ExpirationTracker::TrackState::kStale:
return "cache hit (stale)";
case data_components::ExpirationTracker::TrackState::kNotTracked:
return "cache miss";
case data_components::ExpirationTracker::TrackState::kFresh:
return "cache hit";
}
detail::unreachable();
}
} // namespace launchdarkly::server_side::data_systems