use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use tonic::{Request, Response, Status};
use uuid::Uuid;
use micro_traffic_sim::pb;
#[derive(Default)]
pub struct RecordingHandle {
pub progress_tick: AtomicU64,
pub rows: AtomicU64,
pub cancel: AtomicBool,
}
pub type Recordings = Arc<Mutex<HashMap<Uuid, Arc<RecordingHandle>>>>;
pub fn new_registry() -> Recordings {
Arc::new(Mutex::new(HashMap::new()))
}
pub struct RecordingGuard {
recordings: Recordings,
id: Uuid,
}
impl RecordingGuard {
pub fn new(recordings: Recordings, id: Uuid) -> Self {
Self { recordings, id }
}
}
impl Drop for RecordingGuard {
fn drop(&mut self) {
if let Ok(mut reg) = self.recordings.lock() {
reg.remove(&self.id);
}
}
}
fn parse_session_uuid(id: &Option<pb::UuiDv4>) -> Result<Uuid, Status> {
let value = id
.as_ref()
.map(|u| u.value.as_str())
.ok_or_else(|| Status::invalid_argument("No session ID has been provided"))?;
Uuid::parse_str(value).map_err(|_| {
Status::invalid_argument(format!("Session ID should be a UUID v4: '{}'", value))
})
}
pub async fn recording_status(
recordings: Recordings,
request: Request<pb::RecordingStatusRequest>,
) -> Result<Response<pb::RecordingStatusResponse>, Status> {
let id = parse_session_uuid(&request.into_inner().session_id)?;
let resp = {
let reg = recordings
.lock()
.map_err(|_| Status::internal("recordings registry lock poisoned"))?;
match reg.get(&id) {
Some(h) => pb::RecordingStatusResponse {
state: pb::RecordingState::Running as i32,
current_tick: h.progress_tick.load(Ordering::Relaxed),
rows: h.rows.load(Ordering::Relaxed),
cancel_requested: h.cancel.load(Ordering::Relaxed),
},
None => pb::RecordingStatusResponse {
state: pb::RecordingState::NotRunning as i32,
current_tick: 0,
rows: 0,
cancel_requested: false,
},
}
};
Ok(Response::new(resp))
}
pub async fn stop_recording(
recordings: Recordings,
request: Request<pb::StopRecordingRequest>,
) -> Result<Response<pb::StopRecordingResponse>, Status> {
let id = parse_session_uuid(&request.into_inner().session_id)?;
let accepted = {
let reg = recordings
.lock()
.map_err(|_| Status::internal("recordings registry lock poisoned"))?;
match reg.get(&id) {
Some(h) => {
h.cancel.store(true, Ordering::Relaxed);
true
}
None => false,
}
};
Ok(Response::new(pb::StopRecordingResponse { accepted }))
}
#[cfg(test)]
mod tests {
use super::*;
fn status_req(id: Uuid) -> Request<pb::RecordingStatusRequest> {
Request::new(pb::RecordingStatusRequest {
session_id: Some(pb::UuiDv4 {
value: id.to_string(),
}),
})
}
fn stop_req(id: Uuid) -> Request<pb::StopRecordingRequest> {
Request::new(pb::StopRecordingRequest {
session_id: Some(pb::UuiDv4 {
value: id.to_string(),
}),
})
}
#[tokio::test]
async fn status_and_stop_via_registry() {
let reg = new_registry();
let id = Uuid::new_v4();
let r = recording_status(reg.clone(), status_req(id))
.await
.unwrap()
.into_inner();
assert_eq!(r.state, pb::RecordingState::NotRunning as i32);
let s = stop_recording(reg.clone(), stop_req(id))
.await
.unwrap()
.into_inner();
assert!(!s.accepted, "stop on a non-running recording is a no-op");
let handle = Arc::new(RecordingHandle::default());
handle.progress_tick.store(42, Ordering::Relaxed);
handle.rows.store(100, Ordering::Relaxed);
reg.lock().unwrap().insert(id, handle.clone());
let r = recording_status(reg.clone(), status_req(id))
.await
.unwrap()
.into_inner();
assert_eq!(r.state, pb::RecordingState::Running as i32);
assert_eq!(r.current_tick, 42);
assert_eq!(r.rows, 100);
assert!(!r.cancel_requested);
let s = stop_recording(reg.clone(), stop_req(id))
.await
.unwrap()
.into_inner();
assert!(s.accepted);
assert!(handle.cancel.load(Ordering::Relaxed));
let r = recording_status(reg.clone(), status_req(id))
.await
.unwrap()
.into_inner();
assert!(r.cancel_requested);
}
}