use std::fmt;
use event_listener::Event;
use parking_lot::Mutex;
use wresp::RespCommand;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ObserverStatus {
#[default]
WaitingForResult,
ResultSet,
SessionDisposed,
}
#[derive(Debug, Default)]
struct ObserverState {
status: ObserverStatus,
result: CollectionItemResult,
}
pub struct CollectionItemObserver {
pub session_id: usize,
pub command: RespCommand,
pub command_args: Vec<Vec<u8>>,
state: Mutex<ObserverState>,
event: Event,
}
impl fmt::Debug for CollectionItemObserver {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CollectionItemObserver")
.field("session_id", &self.session_id)
.field("command", &self.command)
.field("command_args", &self.command_args)
.field("state", &self.state)
.finish()
}
}
impl CollectionItemObserver {
pub fn new(session_id: usize, command: RespCommand, command_args: Vec<Vec<u8>>) -> Self {
Self {
session_id,
command,
command_args,
state: Mutex::new(ObserverState::default()),
event: Event::new(),
}
}
#[inline]
fn notify_done(&self) {
self.event.notify(usize::MAX);
}
#[inline]
pub fn status(&self) -> ObserverStatus {
self.state.lock().status
}
#[inline]
pub fn result(&self) -> CollectionItemResult {
self.state.lock().result.clone()
}
pub async fn wait_result(&self) {
while self.status() == ObserverStatus::WaitingForResult {
let listener = self.event.listen();
if self.status() != ObserverStatus::WaitingForResult {
break;
}
listener.await;
}
}
pub fn handle_set_result(&self, result: CollectionItemResult) {
{
let mut state = self.state.lock();
if state.status != ObserverStatus::WaitingForResult {
return;
}
state.result = result;
state.status = ObserverStatus::ResultSet;
}
self.notify_done();
}
pub fn try_force_unblock(&self, throw_error: bool) -> bool {
{
let mut state = self.state.lock();
if state.status != ObserverStatus::WaitingForResult {
return false;
}
state.result = if throw_error {
CollectionItemResult::force_unblocked()
} else {
CollectionItemResult::empty()
};
state.status = ObserverStatus::ResultSet;
}
self.notify_done();
true
}
pub fn handle_session_disposed(&self) {
{
let mut state = self.state.lock();
state.status = ObserverStatus::SessionDisposed;
}
self.notify_done();
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CollectionItemResult {
pub key: Option<Vec<u8>>,
pub item: Option<Vec<u8>>,
pub score: Option<f64>,
pub items: Option<Vec<Vec<u8>>>,
pub scores: Option<Vec<f64>>,
pub is_force_unblocked: bool,
pub is_type_mismatch: bool,
}
impl CollectionItemResult {
pub fn empty() -> Self {
Self::default()
}
pub fn single(key: Vec<u8>, item: Vec<u8>) -> Self {
Self {
key: Some(key),
item: Some(item),
..Self::default()
}
}
pub fn single_with_score(key: Vec<u8>, score: f64, item: Vec<u8>) -> Self {
Self {
key: Some(key),
item: Some(item),
score: Some(score),
..Self::default()
}
}
pub fn multiple(key: Vec<u8>, items: Vec<Vec<u8>>) -> Self {
Self {
key: Some(key),
items: Some(items),
..Self::default()
}
}
pub fn multiple_with_scores(key: Vec<u8>, scores: Vec<f64>, items: Vec<Vec<u8>>) -> Self {
Self {
key: Some(key),
scores: Some(scores),
items: Some(items),
..Self::default()
}
}
#[inline]
pub fn found(&self) -> bool {
self.key.is_some()
}
pub fn force_unblocked() -> Self {
Self {
is_force_unblocked: true,
..Self::default()
}
}
pub fn type_mismatch() -> Self {
Self {
is_type_mismatch: true,
..Self::default()
}
}
}
#[cfg(test)]
mod tests {
use wbase::future::yield_now;
use super::*;
#[test]
fn set_result_once_only() {
let obs = CollectionItemObserver::new(7, RespCommand::Blpop, vec![]);
assert_eq!(obs.status(), ObserverStatus::WaitingForResult);
obs.handle_set_result(CollectionItemResult::single(b"k".to_vec(), b"v".to_vec()));
assert_eq!(obs.status(), ObserverStatus::ResultSet);
assert!(obs.result().found());
obs.handle_set_result(CollectionItemResult::empty());
assert!(obs.result().found());
}
#[test]
fn force_unblock_and_dispose() {
let obs = CollectionItemObserver::new(1, RespCommand::Bzpopmin, vec![]);
assert!(obs.try_force_unblock(false)); assert!(!obs.try_force_unblock(false)); assert!(!obs.result().found());
let obs2 = CollectionItemObserver::new(2, RespCommand::Blpop, vec![]);
assert!(obs2.try_force_unblock(true));
assert!(obs2.result().is_force_unblocked);
let obs3 = CollectionItemObserver::new(3, RespCommand::Blpop, vec![]);
obs3.handle_session_disposed();
assert_eq!(obs3.status(), ObserverStatus::SessionDisposed);
obs3.handle_set_result(CollectionItemResult::single(b"k".to_vec(), b"v".to_vec()));
assert!(!obs3.result().found());
}
#[test]
fn result_shapes() {
let multi = CollectionItemResult::multiple_with_scores(
b"z".to_vec(),
vec![1.0, 2.0],
vec![b"x".to_vec(), b"y".to_vec()],
);
assert!(multi.found());
assert_eq!(multi.scores.as_ref().unwrap().len(), 2);
assert!(!CollectionItemResult::default().found());
assert!(CollectionItemResult::type_mismatch().is_type_mismatch);
}
#[test]
fn wait_result_flow() {
use std::{sync::Arc, time::Duration};
use compio::{
runtime::{Runtime, spawn},
time::timeout,
};
let obs = Arc::new(CollectionItemObserver::new(4, RespCommand::Blpop, vec![]));
let obs_clone = obs.clone();
Runtime::new().unwrap().block_on(async {
timeout(Duration::from_secs(5), async {
let handle = spawn(async move {
yield_now().await;
obs_clone.handle_set_result(CollectionItemResult::single(
b"key".to_vec(),
b"val".to_vec(),
));
});
obs.wait_result().await;
obs.wait_result().await;
handle.await.unwrap();
})
.await
.expect("wait_result_flow should not timeout");
});
assert_eq!(obs.status(), ObserverStatus::ResultSet);
assert!(obs.result().found());
}
}