use std::time::Duration;
use exocortex_storage::RegionKey;
use smol_str::SmolStr;
pub const DREAMS_QUEUE_KEY: &str = "exocortex:dreams:queue";
pub const QUIET_HOURS_BACKLOG_MIN: u64 = 32;
pub const RESET_COUNTERS_LUA: &str = r#"
local key = KEYS[1]
local mem = redis.call('HGET', key, 'memories') or '0'
local edges = redis.call('HGET', key, 'edges') or '0'
redis.call('DEL', key)
return {mem, edges}
"#;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct FireMessage {
pub region: RegionPayload,
pub fired_by: SmolStr,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct RegionPayload {
pub org: String,
pub project: String,
pub memory_type: u8,
}
impl From<&RegionKey> for RegionPayload {
fn from(r: &RegionKey) -> Self {
Self {
org: r.org.to_string(),
project: r.project.to_string(),
memory_type: r.memory_type,
}
}
}
impl From<RegionPayload> for RegionKey {
fn from(p: RegionPayload) -> Self {
Self {
org: p.org.into(),
project: p.project.into(),
memory_type: p.memory_type,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct QuietHours {
pub start_hour: u8,
pub end_hour: u8,
pub enabled: bool,
}
impl QuietHours {
pub fn none() -> Self {
Self::default()
}
pub fn nightly() -> Self {
Self {
start_hour: 23,
end_hour: 7,
enabled: true,
}
}
pub fn contains(&self, local_hour: u8) -> bool {
if !self.enabled {
return false;
}
if self.start_hour <= self.end_hour {
local_hour >= self.start_hour && local_hour < self.end_hour
} else {
local_hour >= self.start_hour || local_hour < self.end_hour
}
}
}
pub struct RedisFireQueue {
conn: redis::aio::MultiplexedConnection,
pub quiet_hours: QuietHours,
}
impl RedisFireQueue {
pub fn new(conn: redis::aio::MultiplexedConnection, quiet_hours: QuietHours) -> Self {
Self { conn, quiet_hours }
}
pub async fn fire(&mut self, region: &RegionKey, fired_by: &str) -> anyhow::Result<()> {
let msg = FireMessage {
region: region.into(),
fired_by: fired_by.into(),
};
let payload = serde_json::to_string(&msg)?;
let _: () = redis::cmd("RPUSH")
.arg(DREAMS_QUEUE_KEY)
.arg(payload)
.query_async(&mut self.conn)
.await?;
metrics::counter!("exocortex_dreams_fired_total", "transport" => "redis").increment(1);
Ok(())
}
pub async fn drain(&mut self, timeout: Duration) -> anyhow::Result<Option<RegionKey>> {
let (key, payload): (String, String) = match redis::cmd("BLPOP")
.arg(DREAMS_QUEUE_KEY)
.arg(timeout.as_secs_f64())
.query_async(&mut self.conn)
.await
{
Ok(pair) => pair,
Err(e) => {
if e.to_string().contains("timeout") {
return Ok(None);
}
return Err(e.into());
}
};
let _ = key;
let msg: FireMessage = serde_json::from_str(&payload)?;
let local_hour = local_hour_now();
if self.quiet_hours.contains(local_hour)
&& self.backlog_len().await? < QUIET_HOURS_BACKLOG_MIN
{
let _: () = redis::cmd("RPUSH")
.arg(DREAMS_QUEUE_KEY)
.arg(payload)
.query_async(&mut self.conn)
.await?;
metrics::counter!("exocortex_dreams_deferred_quiet_total").increment(1);
return Ok(None);
}
Ok(Some(msg.region.into()))
}
async fn backlog_len(&mut self) -> anyhow::Result<u64> {
let n: u64 = redis::cmd("LLEN")
.arg(DREAMS_QUEUE_KEY)
.query_async(&mut self.conn)
.await?;
Ok(n)
}
pub async fn reset_counters(&mut self, region: &RegionKey) -> anyhow::Result<(u32, u32)> {
let key = format!(
"exocortex:dreams:counters:{}:{}:{}",
region.org, region.project, region.memory_type
);
let script = redis::Script::new(RESET_COUNTERS_LUA);
let invoke = script.key(key);
let (mem, edges): (String, String) = invoke.invoke_async(&mut self.conn).await?;
Ok((mem.parse().unwrap_or(0), edges.parse().unwrap_or(0)))
}
}
fn local_hour_now() -> u8 {
chrono::Local::now()
.format("%H")
.to_string()
.parse()
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quiet_hours_windows() {
let none = QuietHours::none();
assert!(!none.contains(3));
let nightly = QuietHours::nightly();
assert!(nightly.contains(23));
assert!(nightly.contains(2));
assert!(nightly.contains(6));
assert!(!nightly.contains(7), "end hour is exclusive");
assert!(!nightly.contains(22));
let day = QuietHours {
start_hour: 9,
end_hour: 17,
enabled: true,
};
assert!(day.contains(9));
assert!(!day.contains(17), "end hour is exclusive");
}
#[test]
fn fire_message_round_trips_region() {
let region = RegionKey {
org: "o".into(),
project: "p".into(),
memory_type: 3,
};
let payload = RegionPayload::from(®ion);
let back: RegionKey = payload.into();
assert_eq!(back.org, region.org);
assert_eq!(back.project, region.project);
assert_eq!(back.memory_type, region.memory_type);
}
}