Skip to content

Commit 1a16e39

Browse files
authored
feat: Add support for daemon mode (#110)
1 parent 1b472ff commit 1a16e39

File tree

7 files changed

+182
-17
lines changed

7 files changed

+182
-17
lines changed

contract-tests/src/command_params.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ pub struct EvaluateAllFlagsResponse {
6363
pub state: FlagDetail,
6464
}
6565

66+
#[allow(dead_code)]
6667
#[derive(Deserialize, Debug)]
6768
#[serde(rename_all = "camelCase")]
6869
pub struct CustomEventParams {

launchdarkly-server-sdk/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ mockito = "1.2.0"
5050
assert-json-diff = "2.0.2"
5151
async-std = "1.12.0"
5252
reqwest = { version = "0.12.4", features = ["json"] }
53+
testing_logger = "0.1.1"
5354

5455
[features]
5556
default = ["rustls"]

launchdarkly-server-sdk/src/client.rs

Lines changed: 153 additions & 15 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
@@ -325,9 +331,9 @@ impl Client {
325331
pub fn close(&self) {
326332
self.event_processor.close();
327333

328-
// If the system is in offline mode, no receiver will be listening to this broadcast
329-
// channel, so sending on it would always result in an error.
330-
if !self.offline {
334+
// If the system is in offline mode or daemon mode, no receiver will be listening to this
335+
// broadcast channel, so sending on it would always result in an error.
336+
if !self.offline && !self.daemon_mode {
331337
if let Err(e) = self.shutdown_broadcast.send(()) {
332338
error!("Failed to shutdown client appropriately: {}", e);
333339
}
@@ -844,7 +850,8 @@ mod tests {
844850
use eval::{ContextBuilder, MultiContextBuilder};
845851
use futures::FutureExt;
846852
use hyper::client::HttpConnector;
847-
use launchdarkly_server_sdk_evaluation::Reason;
853+
use launchdarkly_server_sdk_evaluation::{Flag, Reason, Segment};
854+
use maplit::hashmap;
848855
use std::collections::HashMap;
849856
use tokio::time::Instant;
850857

@@ -853,12 +860,17 @@ mod tests {
853860
use crate::events::create_event_sender;
854861
use crate::events::event::{OutputEvent, VariationKey};
855862
use crate::events::processor_builders::EventProcessorBuilder;
863+
use crate::stores::persistent_store::tests::InMemoryPersistentDataStore;
856864
use crate::stores::store_types::{PatchTarget, StorageItem};
857865
use crate::test_common::{
858866
self, basic_flag, basic_flag_with_prereq, basic_flag_with_prereqs_and_visibility,
859867
basic_flag_with_visibility, basic_int_flag, basic_migration_flag, basic_off_flag,
860868
};
861-
use crate::{ConfigBuilder, MigratorBuilder, Operation, Origin};
869+
use crate::{
870+
AllData, ConfigBuilder, MigratorBuilder, NullEventProcessorBuilder, Operation, Origin,
871+
PersistentDataStore, PersistentDataStoreBuilder, PersistentDataStoreFactory,
872+
SerializedItem,
873+
};
862874
use test_case::test_case;
863875

864876
use super::*;
@@ -872,7 +884,7 @@ mod tests {
872884

873885
#[tokio::test]
874886
async fn client_asynchronously_initializes() {
875-
let (client, _event_rx) = make_mocked_client_with_delay(1000, false);
887+
let (client, _event_rx) = make_mocked_client_with_delay(1000, false, false);
876888
client.start_with_default_executor();
877889

878890
let now = Instant::now();
@@ -885,7 +897,7 @@ mod tests {
885897

886898
#[tokio::test]
887899
async fn client_asynchronously_initializes_within_timeout() {
888-
let (client, _event_rx) = make_mocked_client_with_delay(1000, false);
900+
let (client, _event_rx) = make_mocked_client_with_delay(1000, false, false);
889901
client.start_with_default_executor();
890902

891903
let now = Instant::now();
@@ -900,7 +912,7 @@ mod tests {
900912

901913
#[tokio::test]
902914
async fn client_asynchronously_initializes_slower_than_timeout() {
903-
let (client, _event_rx) = make_mocked_client_with_delay(2000, false);
915+
let (client, _event_rx) = make_mocked_client_with_delay(2000, false, false);
904916
client.start_with_default_executor();
905917

906918
let now = Instant::now();
@@ -915,7 +927,23 @@ mod tests {
915927

916928
#[tokio::test]
917929
async fn client_initializes_immediately_in_offline_mode() {
918-
let (client, _event_rx) = make_mocked_client_with_delay(1000, true);
930+
let (client, _event_rx) = make_mocked_client_with_delay(1000, true, false);
931+
client.start_with_default_executor();
932+
933+
assert!(client.initialized());
934+
935+
let now = Instant::now();
936+
let initialized = client
937+
.wait_for_initialization(Duration::from_millis(2000))
938+
.await;
939+
let elapsed_time = now.elapsed();
940+
assert_eq!(initialized, Some(true));
941+
assert!(elapsed_time.as_millis() < 500)
942+
}
943+
944+
#[tokio::test]
945+
async fn client_initializes_immediately_in_daemon_mode() {
946+
let (client, _event_rx) = make_mocked_client_with_delay(1000, false, true);
919947
client.start_with_default_executor();
920948

921949
assert!(client.initialized());
@@ -1393,6 +1421,111 @@ mod tests {
13931421
assert_eq!(event_rx.iter().count(), 0);
13941422
}
13951423

1424+
struct InMemoryPersistentDataStoreFactory {
1425+
data: AllData<Flag, Segment>,
1426+
initialized: bool,
1427+
}
1428+
1429+
impl PersistentDataStoreFactory for InMemoryPersistentDataStoreFactory {
1430+
fn create_persistent_data_store(
1431+
&self,
1432+
) -> Result<Box<(dyn PersistentDataStore + 'static)>, std::io::Error> {
1433+
let serialized_data =
1434+
AllData::<SerializedItem, SerializedItem>::try_from(self.data.clone())?;
1435+
Ok(Box::new(InMemoryPersistentDataStore {
1436+
data: serialized_data,
1437+
initialized: self.initialized,
1438+
}))
1439+
}
1440+
}
1441+
1442+
#[test]
1443+
fn variation_detail_handles_daemon_mode() {
1444+
testing_logger::setup();
1445+
let factory = InMemoryPersistentDataStoreFactory {
1446+
data: AllData {
1447+
flags: hashmap!["flag".into() => basic_flag("flag")],
1448+
segments: HashMap::new(),
1449+
},
1450+
initialized: true,
1451+
};
1452+
let builder = PersistentDataStoreBuilder::new(Arc::new(factory));
1453+
1454+
let config = ConfigBuilder::new("sdk-key")
1455+
.daemon_mode(true)
1456+
.data_store(&builder)
1457+
.event_processor(&NullEventProcessorBuilder::new())
1458+
.build()
1459+
.expect("config should build");
1460+
1461+
let client = Client::build(config).expect("Should be built.");
1462+
1463+
client.start_with_default_executor();
1464+
1465+
let context = ContextBuilder::new("bob")
1466+
.build()
1467+
.expect("Failed to create context");
1468+
1469+
let detail = client.variation_detail(&context, "flag", FlagValue::Bool(false));
1470+
1471+
assert!(detail.value.unwrap().as_bool().unwrap());
1472+
assert!(matches!(
1473+
detail.reason,
1474+
Reason::Fallthrough {
1475+
in_experiment: false
1476+
}
1477+
));
1478+
client.flush();
1479+
client.close();
1480+
1481+
testing_logger::validate(|captured_logs| {
1482+
assert_eq!(captured_logs.len(), 1);
1483+
assert_eq!(
1484+
captured_logs[0].body,
1485+
"Started LaunchDarkly Client in daemon mode"
1486+
);
1487+
});
1488+
}
1489+
1490+
#[test]
1491+
fn daemon_mode_is_quiet_if_store_is_not_initialized() {
1492+
testing_logger::setup();
1493+
1494+
let factory = InMemoryPersistentDataStoreFactory {
1495+
data: AllData {
1496+
flags: HashMap::new(),
1497+
segments: HashMap::new(),
1498+
},
1499+
initialized: false,
1500+
};
1501+
let builder = PersistentDataStoreBuilder::new(Arc::new(factory));
1502+
1503+
let config = ConfigBuilder::new("sdk-key")
1504+
.daemon_mode(true)
1505+
.data_store(&builder)
1506+
.event_processor(&NullEventProcessorBuilder::new())
1507+
.build()
1508+
.expect("config should build");
1509+
1510+
let client = Client::build(config).expect("Should be built.");
1511+
1512+
client.start_with_default_executor();
1513+
1514+
let context = ContextBuilder::new("bob")
1515+
.build()
1516+
.expect("Failed to create context");
1517+
1518+
client.variation_detail(&context, "flag", FlagValue::Bool(false));
1519+
1520+
testing_logger::validate(|captured_logs| {
1521+
assert_eq!(captured_logs.len(), 1);
1522+
assert_eq!(
1523+
captured_logs[0].body,
1524+
"Started LaunchDarkly Client in daemon mode"
1525+
);
1526+
});
1527+
}
1528+
13961529
#[test]
13971530
fn variation_handles_off_flag_without_variation() {
13981531
let (client, event_rx) = make_mocked_client();
@@ -1612,7 +1745,7 @@ mod tests {
16121745

16131746
#[tokio::test]
16141747
async fn variation_detail_handles_client_not_ready() {
1615-
let (client, event_rx) = make_mocked_client_with_delay(u64::MAX, false);
1748+
let (client, event_rx) = make_mocked_client_with_delay(u64::MAX, false, false);
16161749
client.start_with_default_executor();
16171750
let context = ContextBuilder::new("bob")
16181751
.build()
@@ -2475,12 +2608,17 @@ mod tests {
24752608
}
24762609
}
24772610

2478-
fn make_mocked_client_with_delay(delay: u64, offline: bool) -> (Client, Receiver<OutputEvent>) {
2611+
fn make_mocked_client_with_delay(
2612+
delay: u64,
2613+
offline: bool,
2614+
daemon_mode: bool,
2615+
) -> (Client, Receiver<OutputEvent>) {
24792616
let updates = Arc::new(MockDataSource::new_with_init_delay(delay));
24802617
let (event_sender, event_rx) = create_event_sender();
24812618

24822619
let config = ConfigBuilder::new("sdk-key")
24832620
.offline(offline)
2621+
.daemon_mode(daemon_mode)
24842622
.data_source(MockDataSourceBuilder::new().data_source(updates))
24852623
.event_processor(
24862624
EventProcessorBuilder::<HttpConnector>::new().event_sender(Arc::new(event_sender)),
@@ -2494,10 +2632,10 @@ mod tests {
24942632
}
24952633

24962634
fn make_mocked_offline_client() -> (Client, Receiver<OutputEvent>) {
2497-
make_mocked_client_with_delay(0, true)
2635+
make_mocked_client_with_delay(0, true, false)
24982636
}
24992637

25002638
fn make_mocked_client() -> (Client, Receiver<OutputEvent>) {
2501-
make_mocked_client_with_delay(0, false)
2639+
make_mocked_client_with_delay(0, false, false)
25022640
}
25032641
}

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
}

launchdarkly-server-sdk/src/events/event.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ impl OutputEvent {
422422
}
423423
}
424424

425+
#[allow(clippy::large_enum_variant)]
425426
#[derive(Clone, Debug, Serialize)]
426427
pub enum InputEvent {
427428
FeatureRequest(FeatureRequestEvent),

launchdarkly-server-sdk/src/migrations/migrator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ where
452452
payload: &'a P,
453453
}
454454

455-
impl<'a, P, T, F> Executor<'a, P, T, F>
455+
impl<P, T, F> Executor<'_, P, T, F>
456456
where
457457
P: Send + Sync,
458458
T: Send + Sync,

0 commit comments

Comments
 (0)