use feattle_core::{BoxError, Feattles};
use std::sync::{Arc, Weak};
use std::time::Duration;
use tokio::task::JoinHandle;
use tokio::time::sleep;
#[derive(Debug)]
pub struct BackgroundSync<F> {
ok_interval: Duration,
err_interval: Duration,
feattles: Weak<F>,
}
impl<F> BackgroundSync<F> {
pub fn new(feattles: &Arc<F>) -> Self {
BackgroundSync {
ok_interval: Duration::from_secs(30),
err_interval: Duration::from_secs(60),
feattles: Arc::downgrade(feattles),
}
}
pub fn interval(&mut self, value: Duration) -> &mut Self {
self.ok_interval = value;
self.err_interval = value;
self
}
pub fn ok_interval(&mut self, value: Duration) -> &mut Self {
self.ok_interval = value;
self
}
pub fn err_interval(&mut self, value: Duration) -> &mut Self {
self.err_interval = value;
self
}
}
impl<F: Feattles + Sync + Send + 'static> BackgroundSync<F> {
#[deprecated = "use `start_sync()` that will try a first update right away"]
pub fn spawn(self) -> JoinHandle<()> {
tokio::spawn(async move {
while let Some(feattles) = self.feattles.upgrade() {
match feattles.reload().await {
Ok(()) => {
log::debug!("Feattles updated");
sleep(self.ok_interval).await;
}
Err(err) => {
log::warn!("Failed to sync Feattles: {:?}", err);
sleep(self.err_interval).await;
}
}
}
log::info!("Stop background sync since Feattles got dropped")
})
}
pub async fn start(self) -> Option<BoxError> {
let feattles = self.feattles.upgrade()?;
let first_error = feattles.reload().await.err();
let first_sleep = match &first_error {
Some(err) => {
log::warn!("Failed to sync Feattles: {:?}", err);
self.err_interval
}
None => {
log::debug!("Feattles updated");
self.ok_interval
}
};
tokio::spawn(async move {
sleep(first_sleep).await;
while let Some(feattles) = self.feattles.upgrade() {
match feattles.reload().await {
Ok(()) => {
log::debug!("Feattles updated");
sleep(self.ok_interval).await;
}
Err(err) => {
log::warn!("Failed to sync Feattles: {:?}", err);
sleep(self.err_interval).await;
}
}
}
log::info!("Stop background sync since Feattles got dropped")
});
first_error
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use feattle_core::persist::{CurrentValues, Persist, ValueHistory};
use feattle_core::{feattles, BoxError, Feattles};
use parking_lot::Mutex;
use tokio::time;
use tokio::time::Instant;
#[derive(Debug, thiserror::Error)]
#[error("Some error")]
struct SomeError;
#[derive(Clone)]
struct MockPersistence {
call_instants: Arc<Mutex<Vec<Instant>>>,
}
impl MockPersistence {
fn new() -> Self {
MockPersistence {
call_instants: Arc::new(Mutex::new(vec![Instant::now()])),
}
}
fn call_intervals(&self) -> Vec<Duration> {
self.call_instants
.lock()
.windows(2)
.map(|instants| instants[1] - instants[0])
.collect()
}
}
#[async_trait]
impl Persist for MockPersistence {
async fn save_current(&self, _value: &CurrentValues) -> Result<(), BoxError> {
unimplemented!()
}
async fn load_current(&self) -> Result<Option<CurrentValues>, BoxError> {
let mut call_instants = self.call_instants.lock();
call_instants.push(Instant::now());
if call_instants.len() == 3 {
Err(Box::new(SomeError))
} else {
Ok(None)
}
}
async fn save_history(&self, _key: &str, _value: &ValueHistory) -> Result<(), BoxError> {
unimplemented!()
}
async fn load_history(&self, _key: &str) -> Result<Option<ValueHistory>, BoxError> {
unimplemented!()
}
}
#[tokio::test]
async fn test() {
feattles! {
struct MyToggles { }
}
time::pause();
let persistence = Arc::new(MockPersistence::new());
let toggles = Arc::new(MyToggles::new(persistence.clone()));
BackgroundSync::new(&toggles).start().await;
loop {
let call_intervals = persistence.call_intervals();
if call_intervals.len() == 4 {
assert_eq!(call_intervals[0].as_secs_f32().round() as i32, 0);
assert_eq!(call_intervals[1].as_secs_f32().round() as i32, 30);
assert_eq!(call_intervals[2].as_secs_f32().round() as i32, 60);
assert_eq!(call_intervals[3].as_secs_f32().round() as i32, 30);
break;
}
tokio::task::yield_now().await;
time::sleep(Duration::from_millis(100)).await;
}
drop(toggles);
for _ in 0..5 {
tokio::task::yield_now().await;
}
assert_eq!(persistence.call_intervals().len(), 4);
}
}