Skip to content

Commit a7ff3e8

Browse files
committed
feat: Add support for daemon mode
1 parent 1b472ff commit a7ff3e8

File tree

2 files changed

+85
-10
lines changed

2 files changed

+85
-10
lines changed

launchdarkly-server-sdk/src/client.rs

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ pub struct Client {
155155
init_state: Arc<AtomicUsize>,
156156
started: AtomicBool,
157157
offline: bool,
158+
daemon_mode: bool,
158159
sdk_key: String,
159160
shutdown_broadcast: broadcast::Sender<()>,
160161
runtime: RwLock<Option<Runtime>>,
@@ -165,6 +166,8 @@ impl Client {
165166
pub fn build(config: Config) -> Result<Self, BuildError> {
166167
if config.offline() {
167168
info!("Started LaunchDarkly Client in offline mode");
169+
} else if config.daemon_mode() {
170+
info!("Started LaunchDarkly Client in daemon mode");
168171
}
169172

170173
let tags = config.application_tag();
@@ -210,6 +213,7 @@ impl Client {
210213
init_state: Arc::new(AtomicUsize::new(ClientInitState::Initializing as usize)),
211214
started: AtomicBool::new(false),
212215
offline: config.offline(),
216+
daemon_mode: config.daemon_mode(),
213217
sdk_key: config.sdk_key().into(),
214218
shutdown_broadcast: shutdown_tx,
215219
runtime: RwLock::new(None),
@@ -297,7 +301,7 @@ impl Client {
297301
}
298302

299303
async fn initialized_async_internal(&self) -> bool {
300-
if self.offline {
304+
if self.offline || self.daemon_mode {
301305
return true;
302306
}
303307

@@ -316,7 +320,9 @@ impl Client {
316320
/// In the case of unrecoverable errors in establishing a connection it is possible for the
317321
/// SDK to never become initialized.
318322
pub fn initialized(&self) -> bool {
319-
self.offline || ClientInitState::Initialized == self.init_state.load(Ordering::SeqCst)
323+
self.offline
324+
|| self.daemon_mode
325+
|| ClientInitState::Initialized == self.init_state.load(Ordering::SeqCst)
320326
}
321327

322328
/// Close shuts down the LaunchDarkly client. After calling this, the LaunchDarkly client
@@ -872,7 +878,7 @@ mod tests {
872878

873879
#[tokio::test]
874880
async fn client_asynchronously_initializes() {
875-
let (client, _event_rx) = make_mocked_client_with_delay(1000, false);
881+
let (client, _event_rx) = make_mocked_client_with_delay(1000, false, false);
876882
client.start_with_default_executor();
877883

878884
let now = Instant::now();
@@ -885,7 +891,7 @@ mod tests {
885891

886892
#[tokio::test]
887893
async fn client_asynchronously_initializes_within_timeout() {
888-
let (client, _event_rx) = make_mocked_client_with_delay(1000, false);
894+
let (client, _event_rx) = make_mocked_client_with_delay(1000, false, false);
889895
client.start_with_default_executor();
890896

891897
let now = Instant::now();
@@ -900,7 +906,7 @@ mod tests {
900906

901907
#[tokio::test]
902908
async fn client_asynchronously_initializes_slower_than_timeout() {
903-
let (client, _event_rx) = make_mocked_client_with_delay(2000, false);
909+
let (client, _event_rx) = make_mocked_client_with_delay(2000, false, false);
904910
client.start_with_default_executor();
905911

906912
let now = Instant::now();
@@ -915,7 +921,23 @@ mod tests {
915921

916922
#[tokio::test]
917923
async fn client_initializes_immediately_in_offline_mode() {
918-
let (client, _event_rx) = make_mocked_client_with_delay(1000, true);
924+
let (client, _event_rx) = make_mocked_client_with_delay(1000, true, false);
925+
client.start_with_default_executor();
926+
927+
assert!(client.initialized());
928+
929+
let now = Instant::now();
930+
let initialized = client
931+
.wait_for_initialization(Duration::from_millis(2000))
932+
.await;
933+
let elapsed_time = now.elapsed();
934+
assert_eq!(initialized, Some(true));
935+
assert!(elapsed_time.as_millis() < 500)
936+
}
937+
938+
#[tokio::test]
939+
async fn client_initializes_immediately_in_daemon_mode() {
940+
let (client, _event_rx) = make_mocked_client_with_delay(1000, false, true);
919941
client.start_with_default_executor();
920942

921943
assert!(client.initialized());
@@ -1393,6 +1415,30 @@ mod tests {
13931415
assert_eq!(event_rx.iter().count(), 0);
13941416
}
13951417

1418+
#[test]
1419+
fn variation_detail_handles_daemon_mode() {
1420+
let (client, event_rx) = make_mocked_client_with_delay(1000, false, true);
1421+
client.start_with_default_executor();
1422+
1423+
let context = ContextBuilder::new("bob")
1424+
.build()
1425+
.expect("Failed to create context");
1426+
1427+
let detail = client.variation_detail(&context, "myFlag", FlagValue::Bool(false));
1428+
1429+
assert!(!detail.value.unwrap().as_bool().unwrap());
1430+
assert!(matches!(
1431+
detail.reason,
1432+
Reason::Error {
1433+
error: eval::Error::FlagNotFound
1434+
}
1435+
));
1436+
client.flush();
1437+
client.close();
1438+
1439+
assert_eq!(event_rx.iter().count(), 2);
1440+
}
1441+
13961442
#[test]
13971443
fn variation_handles_off_flag_without_variation() {
13981444
let (client, event_rx) = make_mocked_client();
@@ -1612,7 +1658,7 @@ mod tests {
16121658

16131659
#[tokio::test]
16141660
async fn variation_detail_handles_client_not_ready() {
1615-
let (client, event_rx) = make_mocked_client_with_delay(u64::MAX, false);
1661+
let (client, event_rx) = make_mocked_client_with_delay(u64::MAX, false, false);
16161662
client.start_with_default_executor();
16171663
let context = ContextBuilder::new("bob")
16181664
.build()
@@ -2475,12 +2521,17 @@ mod tests {
24752521
}
24762522
}
24772523

2478-
fn make_mocked_client_with_delay(delay: u64, offline: bool) -> (Client, Receiver<OutputEvent>) {
2524+
fn make_mocked_client_with_delay(
2525+
delay: u64,
2526+
offline: bool,
2527+
daemon_mode: bool,
2528+
) -> (Client, Receiver<OutputEvent>) {
24792529
let updates = Arc::new(MockDataSource::new_with_init_delay(delay));
24802530
let (event_sender, event_rx) = create_event_sender();
24812531

24822532
let config = ConfigBuilder::new("sdk-key")
24832533
.offline(offline)
2534+
.daemon_mode(daemon_mode)
24842535
.data_source(MockDataSourceBuilder::new().data_source(updates))
24852536
.event_processor(
24862537
EventProcessorBuilder::<HttpConnector>::new().event_sender(Arc::new(event_sender)),
@@ -2494,10 +2545,10 @@ mod tests {
24942545
}
24952546

24962547
fn make_mocked_offline_client() -> (Client, Receiver<OutputEvent>) {
2497-
make_mocked_client_with_delay(0, true)
2548+
make_mocked_client_with_delay(0, true, false)
24982549
}
24992550

25002551
fn make_mocked_client() -> (Client, Receiver<OutputEvent>) {
2501-
make_mocked_client_with_delay(0, false)
2552+
make_mocked_client_with_delay(0, false, false)
25022553
}
25032554
}

launchdarkly-server-sdk/src/config.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ pub struct Config {
127127
event_processor_builder: Box<dyn EventProcessorFactory>,
128128
application_tag: Option<String>,
129129
offline: bool,
130+
daemon_mode: bool,
130131
}
131132

132133
impl Config {
@@ -160,6 +161,11 @@ impl Config {
160161
self.offline
161162
}
162163

164+
/// Returns the daemon mode status
165+
pub fn daemon_mode(&self) -> bool {
166+
self.daemon_mode
167+
}
168+
163169
/// Returns the tag builder if provided
164170
pub fn application_tag(&self) -> &Option<String> {
165171
&self.application_tag
@@ -189,6 +195,7 @@ pub struct ConfigBuilder {
189195
event_processor_builder: Option<Box<dyn EventProcessorFactory>>,
190196
application_info: Option<ApplicationInfo>,
191197
offline: bool,
198+
daemon_mode: bool,
192199
sdk_key: String,
193200
}
194201

@@ -201,6 +208,7 @@ impl ConfigBuilder {
201208
data_source_builder: None,
202209
event_processor_builder: None,
203210
offline: false,
211+
daemon_mode: false,
204212
application_info: None,
205213
sdk_key: sdk_key.to_string(),
206214
}
@@ -248,6 +256,16 @@ impl ConfigBuilder {
248256
self
249257
}
250258

259+
/// Whether the client should operate in daemon mode.
260+
///
261+
/// In daemon mode, the client will not receive updates directly from LaunchDarkly. Instead,
262+
/// the client will rely on the data store to provide the latest feature flag values. By
263+
/// default, this is false.
264+
pub fn daemon_mode(mut self, enable: bool) -> Self {
265+
self.daemon_mode = enable;
266+
self
267+
}
268+
251269
/// Provides configuration of application metadata.
252270
///
253271
/// These properties are optional and informational. They may be used in LaunchDarkly analytics
@@ -276,6 +294,11 @@ impl ConfigBuilder {
276294
warn!("Custom data source builders will be ignored when in offline mode");
277295
Ok(Box::new(NullDataSourceBuilder::new()))
278296
}
297+
None if self.daemon_mode => Ok(Box::new(NullDataSourceBuilder::new())),
298+
Some(_) if self.daemon_mode => {
299+
warn!("Custom data source builders will be ignored when in daemon mode");
300+
Ok(Box::new(NullDataSourceBuilder::new()))
301+
}
279302
Some(builder) => Ok(builder),
280303
#[cfg(feature = "rustls")]
281304
None => Ok(Box::new(StreamingDataSourceBuilder::<
@@ -320,6 +343,7 @@ impl ConfigBuilder {
320343
event_processor_builder,
321344
application_tag,
322345
offline: self.offline,
346+
daemon_mode: self.daemon_mode,
323347
})
324348
}
325349
}

0 commit comments

Comments
 (0)