mod tracking;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, RwLock, Weak};
use ciborium::Value as CborValue;
use tokio::sync::{broadcast, watch};
use vantage_core::Result;
use vantage_types::Record;
use crate::dio::{Dio, DioEvent, DioInner, Generation, cbor_scalar_string};
use crate::ops::{ChangeFlash, FlashKind, FlashRejection};
#[derive(Debug, Clone)]
pub enum ServoStatus {
Tracking,
Pending,
Failed(FlashRejection),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IdStrategy {
#[default]
Uuid,
Auto,
FromRecord,
}
pub(crate) struct ServoState {
_tally: crate::stats::Tally,
id: RwLock<Option<String>>,
strategy: IdStrategy,
baseline: RwLock<Option<Record<CborValue>>>,
data: RwLock<Record<CborValue>>,
status: RwLock<ServoStatus>,
in_flight: AtomicU64,
generation: AtomicU64,
generation_tx: watch::Sender<Generation>,
}
impl ServoState {
fn bump_generation(&self) {
let next = self.generation.fetch_add(1, Ordering::SeqCst) + 1;
let _ = self.generation_tx.send_replace(Generation(next));
}
fn absorb(&self, incoming: Option<Record<CborValue>>) {
{
let mut baseline = self.baseline.write().unwrap();
let mut data = self.data.write().unwrap();
tracking::absorb(&mut baseline, &mut data, incoming);
}
self.bump_generation();
}
fn set_status(&self, status: ServoStatus) {
*self.status.write().unwrap() = status;
self.bump_generation();
}
}
pub struct Servo {
dio: Dio,
state: Arc<ServoState>,
_guard: ServoGuard,
}
struct ServoGuard {
task: tokio::task::JoinHandle<()>,
dio_weak: Weak<DioInner>,
}
impl Drop for ServoGuard {
fn drop(&mut self) {
self.task.abort();
if let Some(dio) = self.dio_weak.upgrade() {
dio.servo_census.fetch_sub(1, Ordering::Relaxed);
dio.emit_census("servo", "closed");
}
}
}
impl Servo {
pub fn id(&self) -> Option<String> {
self.state.id.read().unwrap().clone()
}
pub fn set(&self, field: impl Into<String>, value: impl Into<CborValue>) {
self.state
.data
.write()
.unwrap()
.insert(field.into(), value.into());
self.state.bump_generation();
}
pub fn get(&self, field: &str) -> Option<CborValue> {
self.state.data.read().unwrap().get(field).cloned()
}
pub fn record(&self) -> Record<CborValue> {
self.state.data.read().unwrap().clone()
}
pub fn baseline(&self) -> Option<Record<CborValue>> {
self.state.baseline.read().unwrap().clone()
}
pub fn error(&self) -> Record<CborValue> {
let baseline = self.state.baseline.read().unwrap();
let data = self.state.data.read().unwrap();
tracking::error_of(baseline.as_ref(), &data)
}
pub fn dirty(&self, field: &str) -> bool {
self.error().get(field).is_some()
}
pub fn is_dirty(&self) -> bool {
!self.error().is_empty()
}
pub fn revert(&self, field: &str) {
{
let baseline = self.state.baseline.read().unwrap();
let mut data = self.state.data.write().unwrap();
match baseline.as_ref().and_then(|b| b.get(field)).cloned() {
Some(measured) => {
data.insert(field.to_string(), measured);
}
None => {
data.shift_remove(field);
}
}
}
self.state.bump_generation();
}
pub fn revert_all(&self) {
{
let baseline = self.state.baseline.read().unwrap();
let mut data = self.state.data.write().unwrap();
*data = baseline.clone().unwrap_or_default();
}
self.state.bump_generation();
}
pub fn status(&self) -> ServoStatus {
self.state.status.read().unwrap().clone()
}
pub fn subscribe(&self) -> watch::Receiver<Generation> {
self.state.generation_tx.subscribe()
}
pub async fn flash(&self) -> Result<Option<ChangeFlash>> {
let frozen = {
let baseline = self.state.baseline.read().unwrap();
let data = self.state.data.read().unwrap();
match baseline.as_ref() {
Some(base) => {
let error = tracking::error_of(Some(base), &data);
if error.is_empty() {
return Ok(None);
}
let id = self
.state
.id
.read()
.unwrap()
.clone()
.expect("a servo with a baseline is bound to an id");
Some(
ChangeFlash::new(FlashKind::Patch, Some(id), error)
.with_before(base.clone()),
)
}
None => {
if data.is_empty() {
return Ok(None);
}
match self.state.id.read().unwrap().clone() {
Some(id) => {
let mut record = data.clone();
let id_column = self
.dio
.master()
.get_id_column()
.unwrap_or("id")
.to_string();
if record.get(&id_column).is_none() {
record.insert(id_column, CborValue::Text(id.clone()));
}
Some(ChangeFlash::insert(id, record))
}
None => match self.state.strategy {
IdStrategy::Auto => None,
_ => Some(ChangeFlash::insert(
self.id_from_record(&data)?,
data.clone(),
)),
},
}
}
}
};
let Some(flash) = frozen else {
return self.flash_auto_insert().await.map(Some);
};
{
*self.state.id.write().unwrap() = flash.id().map(str::to_string);
*self.state.status.write().unwrap() = ServoStatus::Pending;
}
self.state.in_flight.fetch_add(1, Ordering::SeqCst);
self.state.bump_generation();
let outcome = self.dio.flash(flash.clone()).await;
let last = self.state.in_flight.fetch_sub(1, Ordering::SeqCst) == 1;
if last {
self.absorb_now().await;
}
match outcome {
Ok(()) => {
if last {
self.state.set_status(ServoStatus::Tracking);
}
Ok(Some(flash))
}
Err(e) => {
self.state
.set_status(ServoStatus::Failed(FlashRejection::from_error_or_message(
&e,
)));
Err(e)
}
}
}
async fn flash_auto_insert(&self) -> Result<ChangeFlash> {
use vantage_dataset::traits::InsertableValueSet as _;
let record = self.state.data.read().unwrap().clone();
*self.state.status.write().unwrap() = ServoStatus::Pending;
self.state.in_flight.fetch_add(1, Ordering::SeqCst);
self.state.bump_generation();
let master = self.dio.master();
let inserted = master.insert_return_id_value(&record).await;
let id = match inserted {
Ok(id) => {
*self.state.id.write().unwrap() = Some(id.clone());
id
}
Err(e) => {
self.state.in_flight.fetch_sub(1, Ordering::SeqCst);
self.state
.set_status(ServoStatus::Failed(FlashRejection::from_error_or_message(
&e,
)));
return Err(e);
}
};
let id_column = master.get_id_column().unwrap_or("id").to_string();
let mut with_id = record.clone();
with_id.insert(id_column, CborValue::Text(id.clone()));
let seeded = self.dio.patched(id.clone(), with_id.clone()).await;
let last = self.state.in_flight.fetch_sub(1, Ordering::SeqCst) == 1;
if last {
self.absorb_now().await;
}
match seeded {
Ok(()) => {
if last {
self.state.set_status(ServoStatus::Tracking);
}
Ok(ChangeFlash::insert(id, with_id))
}
Err(e) => {
self.state
.set_status(ServoStatus::Failed(FlashRejection::from_error_or_message(
&e,
)));
Err(e)
}
}
}
pub async fn delete(&self) -> Result<ChangeFlash> {
let (id, before) = {
let id =
self.state.id.read().unwrap().clone().ok_or_else(|| {
vantage_core::error!("an unsaved servo has no record to delete")
})?;
(id, self.state.baseline.read().unwrap().clone())
};
let mut flash = ChangeFlash::delete(id);
if let Some(b) = before {
flash = flash.with_before(b);
}
self.dio.flash(flash.clone()).await?;
{
*self.state.baseline.write().unwrap() = None;
self.state.data.write().unwrap().clear();
}
self.state.bump_generation();
Ok(flash)
}
pub(crate) fn absorb(&self, incoming: Option<Record<CborValue>>) {
self.state.absorb(incoming);
}
async fn absorb_now(&self) {
let Some(id) = self.state.id.read().unwrap().clone() else {
return;
};
match self.dio.inner.cache.get_value(&id).await {
Ok(value) => self.state.absorb(value),
Err(e) => tracing::error!(error = %e, "servo measurement read failed"),
}
}
fn id_from_record(&self, data: &Record<CborValue>) -> Result<String> {
let id_column = self
.dio
.master()
.get_id_column()
.unwrap_or("id")
.to_string();
let id = data
.get(&id_column)
.map(cbor_scalar_string)
.filter(|s| !s.is_empty());
id.ok_or_else(|| {
vantage_core::error!(
"flashing a new record requires its id field",
id_column = id_column
)
})
}
}
async fn track_loop(
state: Arc<ServoState>,
dio_weak: Weak<DioInner>,
mut bus: broadcast::Receiver<DioEvent>,
) {
loop {
if dio_weak.upgrade().is_none() {
return;
}
let event = match bus.recv().await {
Ok(event) => event,
Err(broadcast::error::RecvError::Lagged(_)) => {
if state.in_flight.load(Ordering::SeqCst) == 0 {
absorb_from_cache(&state, &dio_weak).await;
}
continue;
}
Err(broadcast::error::RecvError::Closed) => return,
};
if state.in_flight.load(Ordering::SeqCst) > 0 {
continue;
}
let bound = |id: &str| state.id.read().unwrap().as_deref() == Some(id);
match event {
DioEvent::RecordChanged { id }
| DioEvent::RecordInserted { id }
| DioEvent::RecordRemoved { id }
if bound(&id) =>
{
absorb_from_cache(&state, &dio_weak).await;
}
DioEvent::DatasetChanged | DioEvent::Seeded => {
absorb_from_cache(&state, &dio_weak).await;
}
DioEvent::WritePending { id, kind } if bound(&id) => {
if matches!(
kind,
crate::FlashKind::Patch | crate::FlashKind::Replace | crate::FlashKind::Insert
) {
state.set_status(ServoStatus::Pending);
}
}
DioEvent::WriteReverted { id, error, kind } if bound(&id) => {
if matches!(
kind,
crate::FlashKind::Patch | crate::FlashKind::Replace | crate::FlashKind::Insert
) {
state.set_status(ServoStatus::Failed(FlashRejection::new(error)));
}
absorb_from_cache(&state, &dio_weak).await;
}
_ => {}
}
}
}
async fn absorb_from_cache(state: &Arc<ServoState>, dio_weak: &Weak<DioInner>) {
let Some(inner) = dio_weak.upgrade() else {
return;
};
let Some(id) = state.id.read().unwrap().clone() else {
return;
};
match inner.cache.get_value(&id).await {
Ok(value) => state.absorb(value),
Err(e) => tracing::error!(error = %e, "servo measurement read failed"),
}
}
pub(crate) fn spawn_servo(dio: &Dio, id: Option<String>, strategy: IdStrategy) -> Servo {
let mut id = id;
if id.is_none() && strategy == IdStrategy::Uuid {
id = Some(uuid::Uuid::now_v7().to_string());
}
let (generation_tx, _rx) = watch::channel(Generation::default());
let state = Arc::new(ServoState {
_tally: crate::stats::Tally::servo(),
id: RwLock::new(id),
strategy,
baseline: RwLock::new(None),
data: RwLock::new(Record::new()),
status: RwLock::new(ServoStatus::Tracking),
in_flight: AtomicU64::new(0),
generation: AtomicU64::new(0),
generation_tx,
});
let bus_rx = dio.inner.event_bus.subscribe();
let dio_weak = Arc::downgrade(&dio.inner);
let task_state = state.clone();
let task = dio
.inner
.lens
.runtime
.spawn(track_loop(task_state, dio_weak.clone(), bus_rx));
dio.inner.servo_census.fetch_add(1, Ordering::Relaxed);
dio.inner.emit_census("servo", "opened");
Servo {
dio: dio.clone(),
state,
_guard: ServoGuard { task, dio_weak },
}
}