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/src/command_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub struct EvaluateAllFlagsResponse {
pub state: FlagDetail,
}

#[allow(dead_code)]
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CustomEventParams {
Expand Down
1 change: 1 addition & 0 deletions launchdarkly-server-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ mockito = "1.2.0"
assert-json-diff = "2.0.2"
async-std = "1.12.0"
reqwest = { version = "0.12.4", features = ["json"] }
testing_logger = "0.1.1"

[features]
default = ["rustls"]
Expand Down
168 changes: 153 additions & 15 deletions launchdarkly-server-sdk/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ pub struct Client {
init_state: Arc<AtomicUsize>,
started: AtomicBool,
offline: bool,
daemon_mode: bool,
sdk_key: String,
shutdown_broadcast: broadcast::Sender<()>,
runtime: RwLock<Option<Runtime>>,
Expand All @@ -165,6 +166,8 @@ impl Client {
pub fn build(config: Config) -> Result<Self, BuildError> {
if config.offline() {
info!("Started LaunchDarkly Client in offline mode");
} else if config.daemon_mode() {
info!("Started LaunchDarkly Client in daemon mode");
}

let tags = config.application_tag();
Expand Down Expand Up @@ -210,6 +213,7 @@ impl Client {
init_state: Arc::new(AtomicUsize::new(ClientInitState::Initializing as usize)),
started: AtomicBool::new(false),
offline: config.offline(),
daemon_mode: config.daemon_mode(),
sdk_key: config.sdk_key().into(),
shutdown_broadcast: shutdown_tx,
runtime: RwLock::new(None),
Expand Down Expand Up @@ -297,7 +301,7 @@ impl Client {
}

async fn initialized_async_internal(&self) -> bool {
if self.offline {
if self.offline || self.daemon_mode {
return true;
}

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

/// Close shuts down the LaunchDarkly client. After calling this, the LaunchDarkly client
Expand All @@ -325,9 +331,9 @@ impl Client {
pub fn close(&self) {
self.event_processor.close();

// If the system is in offline mode, no receiver will be listening to this broadcast
// channel, so sending on it would always result in an error.
if !self.offline {
// If the system is in offline mode or daemon mode, no receiver will be listening to this
// broadcast channel, so sending on it would always result in an error.
if !self.offline && !self.daemon_mode {
if let Err(e) = self.shutdown_broadcast.send(()) {
error!("Failed to shutdown client appropriately: {}", e);
}
Expand Down Expand Up @@ -844,7 +850,8 @@ mod tests {
use eval::{ContextBuilder, MultiContextBuilder};
use futures::FutureExt;
use hyper::client::HttpConnector;
use launchdarkly_server_sdk_evaluation::Reason;
use launchdarkly_server_sdk_evaluation::{Flag, Reason, Segment};
use maplit::hashmap;
use std::collections::HashMap;
use tokio::time::Instant;

Expand All @@ -853,12 +860,17 @@ mod tests {
use crate::events::create_event_sender;
use crate::events::event::{OutputEvent, VariationKey};
use crate::events::processor_builders::EventProcessorBuilder;
use crate::stores::persistent_store::tests::InMemoryPersistentDataStore;
use crate::stores::store_types::{PatchTarget, StorageItem};
use crate::test_common::{
self, basic_flag, basic_flag_with_prereq, basic_flag_with_prereqs_and_visibility,
basic_flag_with_visibility, basic_int_flag, basic_migration_flag, basic_off_flag,
};
use crate::{ConfigBuilder, MigratorBuilder, Operation, Origin};
use crate::{
AllData, ConfigBuilder, MigratorBuilder, NullEventProcessorBuilder, Operation, Origin,
PersistentDataStore, PersistentDataStoreBuilder, PersistentDataStoreFactory,
SerializedItem,
};
use test_case::test_case;

use super::*;
Expand All @@ -872,7 +884,7 @@ mod tests {

#[tokio::test]
async fn client_asynchronously_initializes() {
let (client, _event_rx) = make_mocked_client_with_delay(1000, false);
let (client, _event_rx) = make_mocked_client_with_delay(1000, false, false);
client.start_with_default_executor();

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

#[tokio::test]
async fn client_asynchronously_initializes_within_timeout() {
let (client, _event_rx) = make_mocked_client_with_delay(1000, false);
let (client, _event_rx) = make_mocked_client_with_delay(1000, false, false);
client.start_with_default_executor();

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

#[tokio::test]
async fn client_asynchronously_initializes_slower_than_timeout() {
let (client, _event_rx) = make_mocked_client_with_delay(2000, false);
let (client, _event_rx) = make_mocked_client_with_delay(2000, false, false);
client.start_with_default_executor();

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

#[tokio::test]
async fn client_initializes_immediately_in_offline_mode() {
let (client, _event_rx) = make_mocked_client_with_delay(1000, true);
let (client, _event_rx) = make_mocked_client_with_delay(1000, true, false);
client.start_with_default_executor();

assert!(client.initialized());

let now = Instant::now();
let initialized = client
.wait_for_initialization(Duration::from_millis(2000))
.await;
let elapsed_time = now.elapsed();
assert_eq!(initialized, Some(true));
assert!(elapsed_time.as_millis() < 500)
}

#[tokio::test]
async fn client_initializes_immediately_in_daemon_mode() {
let (client, _event_rx) = make_mocked_client_with_delay(1000, false, true);
client.start_with_default_executor();

assert!(client.initialized());
Expand Down Expand Up @@ -1393,6 +1421,111 @@ mod tests {
assert_eq!(event_rx.iter().count(), 0);
}

struct InMemoryPersistentDataStoreFactory {
data: AllData<Flag, Segment>,
initialized: bool,
}

impl PersistentDataStoreFactory for InMemoryPersistentDataStoreFactory {
fn create_persistent_data_store(
&self,
) -> Result<Box<(dyn PersistentDataStore + 'static)>, std::io::Error> {
let serialized_data =
AllData::<SerializedItem, SerializedItem>::try_from(self.data.clone())?;
Ok(Box::new(InMemoryPersistentDataStore {
data: serialized_data,
initialized: self.initialized,
}))
}
}

#[test]
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be nice to have a daemon mode test that initialized from a store and evaluated a flag that did exist, and there were no undesirable logs.

fn variation_detail_handles_daemon_mode() {
testing_logger::setup();
let factory = InMemoryPersistentDataStoreFactory {
data: AllData {
flags: hashmap!["flag".into() => basic_flag("flag")],
segments: HashMap::new(),
},
initialized: true,
};
let builder = PersistentDataStoreBuilder::new(Arc::new(factory));

let config = ConfigBuilder::new("sdk-key")
.daemon_mode(true)
.data_store(&builder)
.event_processor(&NullEventProcessorBuilder::new())
.build()
.expect("config should build");

let client = Client::build(config).expect("Should be built.");

client.start_with_default_executor();

let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");

let detail = client.variation_detail(&context, "flag", FlagValue::Bool(false));

assert!(detail.value.unwrap().as_bool().unwrap());
assert!(matches!(
detail.reason,
Reason::Fallthrough {
in_experiment: false
}
));
client.flush();
client.close();

testing_logger::validate(|captured_logs| {
assert_eq!(captured_logs.len(), 1);
assert_eq!(
captured_logs[0].body,
"Started LaunchDarkly Client in daemon mode"
);
});
}

#[test]
fn daemon_mode_is_quiet_if_store_is_not_initialized() {
testing_logger::setup();

let factory = InMemoryPersistentDataStoreFactory {
data: AllData {
flags: HashMap::new(),
segments: HashMap::new(),
},
initialized: false,
};
let builder = PersistentDataStoreBuilder::new(Arc::new(factory));

let config = ConfigBuilder::new("sdk-key")
.daemon_mode(true)
.data_store(&builder)
.event_processor(&NullEventProcessorBuilder::new())
.build()
.expect("config should build");

let client = Client::build(config).expect("Should be built.");

client.start_with_default_executor();

let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");

client.variation_detail(&context, "flag", FlagValue::Bool(false));

testing_logger::validate(|captured_logs| {
assert_eq!(captured_logs.len(), 1);
assert_eq!(
captured_logs[0].body,
"Started LaunchDarkly Client in daemon mode"
);
});
}

#[test]
fn variation_handles_off_flag_without_variation() {
let (client, event_rx) = make_mocked_client();
Expand Down Expand Up @@ -1612,7 +1745,7 @@ mod tests {

#[tokio::test]
async fn variation_detail_handles_client_not_ready() {
let (client, event_rx) = make_mocked_client_with_delay(u64::MAX, false);
let (client, event_rx) = make_mocked_client_with_delay(u64::MAX, false, false);
client.start_with_default_executor();
let context = ContextBuilder::new("bob")
.build()
Expand Down Expand Up @@ -2475,12 +2608,17 @@ mod tests {
}
}

fn make_mocked_client_with_delay(delay: u64, offline: bool) -> (Client, Receiver<OutputEvent>) {
fn make_mocked_client_with_delay(
delay: u64,
offline: bool,
daemon_mode: bool,
) -> (Client, Receiver<OutputEvent>) {
let updates = Arc::new(MockDataSource::new_with_init_delay(delay));
let (event_sender, event_rx) = create_event_sender();

let config = ConfigBuilder::new("sdk-key")
.offline(offline)
.daemon_mode(daemon_mode)
.data_source(MockDataSourceBuilder::new().data_source(updates))
.event_processor(
EventProcessorBuilder::<HttpConnector>::new().event_sender(Arc::new(event_sender)),
Expand All @@ -2494,10 +2632,10 @@ mod tests {
}

fn make_mocked_offline_client() -> (Client, Receiver<OutputEvent>) {
make_mocked_client_with_delay(0, true)
make_mocked_client_with_delay(0, true, false)
}

fn make_mocked_client() -> (Client, Receiver<OutputEvent>) {
make_mocked_client_with_delay(0, false)
make_mocked_client_with_delay(0, false, false)
}
}
24 changes: 24 additions & 0 deletions launchdarkly-server-sdk/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ pub struct Config {
event_processor_builder: Box<dyn EventProcessorFactory>,
application_tag: Option<String>,
offline: bool,
daemon_mode: bool,
}

impl Config {
Expand Down Expand Up @@ -160,6 +161,11 @@ impl Config {
self.offline
}

/// Returns the daemon mode status
pub fn daemon_mode(&self) -> bool {
self.daemon_mode
}

/// Returns the tag builder if provided
pub fn application_tag(&self) -> &Option<String> {
&self.application_tag
Expand Down Expand Up @@ -189,6 +195,7 @@ pub struct ConfigBuilder {
event_processor_builder: Option<Box<dyn EventProcessorFactory>>,
application_info: Option<ApplicationInfo>,
offline: bool,
daemon_mode: bool,
sdk_key: String,
}

Expand All @@ -201,6 +208,7 @@ impl ConfigBuilder {
data_source_builder: None,
event_processor_builder: None,
offline: false,
daemon_mode: false,
application_info: None,
sdk_key: sdk_key.to_string(),
}
Expand Down Expand Up @@ -248,6 +256,16 @@ impl ConfigBuilder {
self
}

/// Whether the client should operate in daemon mode.
///
/// In daemon mode, the client will not receive updates directly from LaunchDarkly. Instead,
/// the client will rely on the data store to provide the latest feature flag values. By
/// default, this is false.
pub fn daemon_mode(mut self, enable: bool) -> Self {
self.daemon_mode = enable;
self
}

/// Provides configuration of application metadata.
///
/// These properties are optional and informational. They may be used in LaunchDarkly analytics
Expand Down Expand Up @@ -276,6 +294,11 @@ impl ConfigBuilder {
warn!("Custom data source builders will be ignored when in offline mode");
Ok(Box::new(NullDataSourceBuilder::new()))
}
None if self.daemon_mode => Ok(Box::new(NullDataSourceBuilder::new())),
Some(_) if self.daemon_mode => {
warn!("Custom data source builders will be ignored when in daemon mode");
Ok(Box::new(NullDataSourceBuilder::new()))
}
Some(builder) => Ok(builder),
#[cfg(feature = "rustls")]
None => Ok(Box::new(StreamingDataSourceBuilder::<
Expand Down Expand Up @@ -320,6 +343,7 @@ impl ConfigBuilder {
event_processor_builder,
application_tag,
offline: self.offline,
daemon_mode: self.daemon_mode,
})
}
}
Expand Down
1 change: 1 addition & 0 deletions launchdarkly-server-sdk/src/events/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ impl OutputEvent {
}
}

#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Serialize)]
pub enum InputEvent {
FeatureRequest(FeatureRequestEvent),
Expand Down
2 changes: 1 addition & 1 deletion launchdarkly-server-sdk/src/migrations/migrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ where
payload: &'a P,
}

impl<'a, P, T, F> Executor<'a, P, T, F>
impl<P, T, F> Executor<'_, P, T, F>
where
P: Send + Sync,
T: Send + Sync,
Expand Down
Loading