use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard, Weak};
use std::time::Duration;
use serde_json::Value;
use tokio::sync::{Notify, watch};
use tokio::task::JoinHandle;
use crate::transport::OfrepTransport;
use crate::{Error, ErrorCode, EvaluationContext, FlagEvaluation, FlagValue, ResolvedFlag};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SnapshotStatus {
NotReady,
Ready,
Stale,
Error,
}
#[derive(Clone, Debug)]
pub struct FlagSnapshot {
shared: Arc<Shared>,
}
#[derive(Debug)]
struct Shared {
transport: Arc<OfrepTransport>,
state: RwLock<State>,
revision: watch::Sender<u64>,
loaded_epoch: watch::Sender<u64>,
reset_timer: Arc<Notify>,
task: Mutex<Option<JoinHandle<()>>>,
}
#[derive(Debug)]
struct State {
flags: HashMap<String, ResolvedFlag>,
version: Option<String>,
context: EvaluationContext,
status: SnapshotStatus,
error: Option<Error>,
generation: u64,
epoch: u64,
revision: u64,
closed: bool,
}
impl FlagSnapshot {
pub(crate) fn start(
transport: Arc<OfrepTransport>,
context: EvaluationContext,
poll_interval: Duration,
) -> Self {
let reset_timer = Arc::new(Notify::new());
let shared = Arc::new(Shared {
transport,
state: RwLock::new(State {
flags: HashMap::new(),
version: None,
context,
status: SnapshotStatus::NotReady,
error: None,
generation: 0,
epoch: 1,
revision: 0,
closed: false,
}),
revision: watch::Sender::new(0),
loaded_epoch: watch::Sender::new(0),
reset_timer: Arc::clone(&reset_timer),
task: Mutex::new(None),
});
let task = tokio::spawn(poll(Arc::downgrade(&shared), reset_timer, poll_interval));
*shared
.task
.lock()
.unwrap_or_else(|poison| poison.into_inner()) = Some(task);
Self { shared }
}
pub fn status(&self) -> SnapshotStatus {
self.shared.read().status
}
pub fn error(&self) -> Option<Error> {
self.shared.read().error.clone()
}
pub fn context(&self) -> EvaluationContext {
self.shared.read().context.clone()
}
pub fn revision(&self) -> u64 {
self.shared.read().revision
}
pub fn is_closed(&self) -> bool {
self.shared.read().closed
}
pub async fn ready(&self) {
let target = self.shared.read().epoch;
let mut loaded = self.shared.loaded_epoch.subscribe();
let _ = loaded.wait_for(|epoch| *epoch >= target).await;
}
pub fn subscribe(&self) -> watch::Receiver<u64> {
self.shared.revision.subscribe()
}
pub fn evaluate<T: FlagValue>(&self, key: &str, default_value: T) -> FlagEvaluation<T> {
let state = self.shared.read();
match state.status {
SnapshotStatus::NotReady => FlagEvaluation::error(
key,
default_value,
ErrorCode::ProviderNotReady,
"Flags are still loading",
),
SnapshotStatus::Error => {
let (code, message) = state.error.as_ref().map_or_else(
|| (ErrorCode::General, "Flags could not be loaded".to_owned()),
|error| (error.code(), error.to_string()),
);
FlagEvaluation::error(key, default_value, code, message)
}
SnapshotStatus::Ready | SnapshotStatus::Stale => match state.flags.get(key) {
Some(flag) => flag.to_evaluation(default_value),
None => FlagEvaluation::error(
key,
default_value,
ErrorCode::FlagNotFound,
format!("Flag '{key}' does not exist for this client"),
),
},
}
}
pub fn get_bool(&self, key: &str, default_value: bool) -> bool {
self.evaluate(key, default_value).value
}
pub fn get_string(&self, key: &str, default_value: &str) -> String {
self.evaluate(key, default_value.to_owned()).value
}
pub fn get_i64(&self, key: &str, default_value: i64) -> i64 {
self.evaluate(key, default_value).value
}
pub fn get_f64(&self, key: &str, default_value: f64) -> f64 {
self.evaluate(key, default_value).value
}
pub fn get_value(&self, key: &str, default_value: Value) -> Value {
self.evaluate(key, default_value).value
}
pub fn is_enabled(&self, key: &str) -> bool {
self.get_bool(key, false)
}
pub fn keys(&self) -> Vec<String> {
self.shared.read().flags.keys().cloned().collect()
}
pub async fn set_context(&self, context: EvaluationContext) {
{
let mut state = self.shared.write();
if state.closed {
return;
}
state.context = context;
state.version = None;
state.epoch += 1;
}
self.refresh().await;
}
pub async fn refresh(&self) {
self.shared.refresh().await;
self.shared.reset_timer.notify_waiters();
}
pub fn close(&self) {
self.shared.write().closed = true;
self.shared.stop();
self.shared.loaded_epoch.send_replace(u64::MAX);
}
}
impl Shared {
fn read(&self) -> RwLockReadGuard<'_, State> {
self.state
.read()
.unwrap_or_else(|poison| poison.into_inner())
}
fn write(&self) -> RwLockWriteGuard<'_, State> {
self.state
.write()
.unwrap_or_else(|poison| poison.into_inner())
}
fn stop(&self) {
if let Some(task) = self
.task
.lock()
.unwrap_or_else(|poison| poison.into_inner())
.take()
{
task.abort();
}
}
async fn refresh(&self) {
let (generation, context, version) = {
let mut state = self.write();
if state.closed {
return;
}
state.generation += 1;
(
state.generation,
state.context.clone(),
state.version.clone(),
)
};
let result = self
.transport
.evaluate_all(&context, version.as_deref())
.await;
let mut state = self.write();
if state.generation != generation || state.closed {
return; }
let previous = (state.status, state.revision);
let mut changed = false;
match result {
Ok(bulk) => {
state.error = None;
state.status = SnapshotStatus::Ready;
if !bulk.not_modified {
changed = state.flags != bulk.flags;
state.flags = bulk.flags;
state.version = bulk.version;
}
}
Err(error) => {
state.error = Some(error);
state.status = if state.flags.is_empty() {
SnapshotStatus::Error
} else {
SnapshotStatus::Stale
};
}
}
if changed || state.status != previous.0 {
state.revision = previous.1 + 1;
}
let (epoch, revision) = (state.epoch, state.revision);
drop(state);
self.revision.send_if_modified(|current| {
let modified = *current != revision;
*current = revision;
modified
});
self.loaded_epoch.send_if_modified(|loaded| {
let modified = *loaded < epoch;
if modified {
*loaded = epoch;
}
modified
});
}
}
impl Drop for Shared {
fn drop(&mut self) {
self.stop();
}
}
async fn poll(shared: Weak<Shared>, reset_timer: Arc<Notify>, interval: Duration) {
loop {
match shared.upgrade() {
Some(shared) => shared.refresh().await,
None => return,
}
if interval.is_zero() {
return;
}
while tokio::time::timeout(interval, reset_timer.notified())
.await
.is_ok()
{}
}
}