use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use taquba::EnqueueRequest;
use crate::error::{Error, Result};
use crate::runtime::RESERVED_KV_PREFIX;
#[derive(Debug, Clone)]
pub struct EffectsHandle {
inner: Arc<Mutex<EffectsState>>,
}
#[derive(Debug, Default)]
struct EffectsState {
staged: StagedEffects,
sealed: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub(crate) struct StagedEffects {
pub(crate) writes: HashMap<Vec<u8>, Vec<u8>>,
pub(crate) deletes: HashSet<Vec<u8>>,
}
impl EffectsHandle {
pub fn detached() -> Self {
Self::for_delivery()
}
pub(crate) fn for_delivery() -> Self {
Self {
inner: Arc::new(Mutex::new(EffectsState::default())),
}
}
pub fn put(&self, key: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) -> Result<()> {
let key = key.into();
let value = value.into();
let mut state = self.inner.lock().unwrap();
check_key(&state, &key)?;
if value.len() > taquba::MAX_KV_VALUE_SIZE {
return Err(Error::Queue(taquba::Error::KvValueTooLarge {
size: value.len(),
max: taquba::MAX_KV_VALUE_SIZE,
}));
}
if state.staged.deletes.contains(&key) {
return Err(Error::ConflictingKvEffect(display_key(&key)));
}
state.staged.writes.insert(key, value);
Ok(())
}
pub fn delete(&self, key: impl Into<Vec<u8>>) -> Result<()> {
let key = key.into();
let mut state = self.inner.lock().unwrap();
check_key(&state, &key)?;
if state.staged.writes.contains_key(&key) {
return Err(Error::ConflictingKvEffect(display_key(&key)));
}
state.staged.deletes.insert(key);
Ok(())
}
pub(crate) fn seal_and_take(&self) -> StagedEffects {
let mut state = self.inner.lock().unwrap();
state.sealed = true;
std::mem::take(&mut state.staged)
}
}
fn check_key(state: &EffectsState, key: &[u8]) -> Result<()> {
if state.sealed {
return Err(Error::EffectsSealed);
}
if key.starts_with(RESERVED_KV_PREFIX.as_bytes()) {
return Err(Error::ReservedKvKey(display_key(key)));
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct TerminalEffects {
inner: Arc<Mutex<TerminalState>>,
}
#[derive(Debug, Default)]
struct TerminalState {
kv: EffectsState,
enqueues: Vec<EnqueueRequest>,
}
impl TerminalEffects {
pub fn detached() -> Self {
Self::for_delivery()
}
pub(crate) fn for_delivery() -> Self {
Self {
inner: Arc::new(Mutex::new(TerminalState::default())),
}
}
pub fn enqueue(&self, request: EnqueueRequest) -> Result<()> {
let mut state = self.inner.lock().unwrap();
if state.kv.sealed {
return Err(Error::EffectsSealed);
}
state.enqueues.push(request);
Ok(())
}
pub fn put(&self, key: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) -> Result<()> {
let key = key.into();
let value = value.into();
let mut state = self.inner.lock().unwrap();
check_key(&state.kv, &key)?;
if value.len() > taquba::MAX_KV_VALUE_SIZE {
return Err(Error::Queue(taquba::Error::KvValueTooLarge {
size: value.len(),
max: taquba::MAX_KV_VALUE_SIZE,
}));
}
if state.kv.staged.deletes.contains(&key) {
return Err(Error::ConflictingKvEffect(display_key(&key)));
}
state.kv.staged.writes.insert(key, value);
Ok(())
}
pub fn delete(&self, key: impl Into<Vec<u8>>) -> Result<()> {
let key = key.into();
let mut state = self.inner.lock().unwrap();
check_key(&state.kv, &key)?;
if state.kv.staged.writes.contains_key(&key) {
return Err(Error::ConflictingKvEffect(display_key(&key)));
}
state.kv.staged.deletes.insert(key);
Ok(())
}
pub(crate) fn seal_and_take(&self) -> (StagedEffects, Vec<EnqueueRequest>) {
let mut state = self.inner.lock().unwrap();
state.kv.sealed = true;
(
std::mem::take(&mut state.kv.staged),
std::mem::take(&mut state.enqueues),
)
}
}
fn display_key(key: &[u8]) -> String {
String::from_utf8_lossy(key).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_reserved_prefix_key_is_rejected() {
let handle = EffectsHandle::detached();
assert!(matches!(
handle.put("workflow/x", "v"),
Err(Error::ReservedKvKey(_))
));
assert!(matches!(
handle.delete("workflow/x"),
Err(Error::ReservedKvKey(_))
));
}
#[test]
fn an_oversized_value_is_rejected() {
let handle = EffectsHandle::detached();
let oversized = vec![0u8; taquba::MAX_KV_VALUE_SIZE + 1];
assert!(matches!(
handle.put("k", oversized),
Err(Error::Queue(taquba::Error::KvValueTooLarge { .. }))
));
}
#[test]
fn a_key_staged_one_way_rejects_the_other() {
let handle = EffectsHandle::detached();
handle.put("a", "v").unwrap();
assert!(matches!(
handle.delete("a"),
Err(Error::ConflictingKvEffect(_))
));
handle.delete("b").unwrap();
assert!(matches!(
handle.put("b", "v"),
Err(Error::ConflictingKvEffect(_))
));
}
#[test]
fn a_sealed_handle_rejects_staging() {
let handle = EffectsHandle::for_delivery();
let clone = handle.clone();
handle.put("a", "v").unwrap();
let staged = handle.seal_and_take();
assert_eq!(staged.writes.len(), 1);
assert!(matches!(clone.put("b", "v"), Err(Error::EffectsSealed)));
assert!(matches!(clone.delete("b"), Err(Error::EffectsSealed)));
}
#[test]
fn a_sealed_terminal_handle_rejects_staging() {
let handle = TerminalEffects::for_delivery();
handle.put("a", "v").unwrap();
handle.delete("b").unwrap();
handle
.enqueue(taquba::EnqueueRequest {
queue: "side".to_string(),
payload: Vec::new(),
options: Default::default(),
})
.unwrap();
let (staged, enqueues) = handle.seal_and_take();
assert_eq!(staged.writes.len(), 1);
assert_eq!(staged.deletes.len(), 1);
assert_eq!(enqueues.len(), 1);
assert!(matches!(handle.put("c", "v"), Err(Error::EffectsSealed)));
assert!(matches!(handle.delete("c"), Err(Error::EffectsSealed)));
assert!(matches!(
handle.enqueue(taquba::EnqueueRequest {
queue: "side".to_string(),
payload: Vec::new(),
options: Default::default(),
}),
Err(Error::EffectsSealed)
));
}
#[test]
fn the_terminal_handle_applies_the_kv_staging_rules() {
let handle = TerminalEffects::detached();
assert!(matches!(
handle.put("workflow/x", "v"),
Err(Error::ReservedKvKey(_))
));
assert!(matches!(
handle.delete("workflow/x"),
Err(Error::ReservedKvKey(_))
));
handle.put("a", "v").unwrap();
assert!(matches!(
handle.delete("a"),
Err(Error::ConflictingKvEffect(_))
));
}
#[test]
fn clones_share_one_accumulator() {
let handle = EffectsHandle::for_delivery();
let clone = handle.clone();
clone.put("a", "v").unwrap();
clone.delete("b").unwrap();
let staged = handle.seal_and_take();
assert_eq!(staged.writes.get(b"a".as_slice()), Some(&b"v".to_vec()));
assert!(staged.deletes.contains(b"b".as_slice()));
}
}