use tokio_util::sync::CancellationToken;
use crate::client::CyclesClient;
use crate::error::Error;
use crate::heartbeat::start_heartbeat;
use crate::models::request::{CommitRequest, ReleaseRequest};
use crate::models::response::{CommitResponse, ReleaseResponse};
use crate::models::{Caps, Decision, ReservationId};
#[must_use = "reservation must be committed or released; will auto-release on drop"]
pub struct ReservationGuard {
client: CyclesClient,
id: ReservationId,
decision: Decision,
caps: Option<Caps>,
expires_at_ms: Option<u64>,
affected_scopes: Vec<String>,
finalized: bool,
cancel: CancellationToken,
_heartbeat: Option<tokio::task::JoinHandle<()>>,
}
impl ReservationGuard {
pub(crate) fn new(
client: CyclesClient,
id: ReservationId,
decision: Decision,
caps: Option<Caps>,
expires_at_ms: Option<u64>,
affected_scopes: Vec<String>,
ttl_ms: u64,
) -> Self {
let cancel = CancellationToken::new();
let heartbeat = start_heartbeat(client.clone(), id.clone(), ttl_ms, cancel.clone());
Self {
client,
id,
decision,
caps,
expires_at_ms,
affected_scopes,
finalized: false,
cancel,
_heartbeat: Some(heartbeat),
}
}
pub fn reservation_id(&self) -> &ReservationId {
&self.id
}
pub fn decision(&self) -> Decision {
self.decision
}
pub fn caps(&self) -> Option<&Caps> {
self.caps.as_ref()
}
pub fn is_capped(&self) -> bool {
self.decision == Decision::AllowWithCaps
}
pub fn expires_at_ms(&self) -> Option<u64> {
self.expires_at_ms
}
pub fn affected_scopes(&self) -> &[String] {
&self.affected_scopes
}
pub async fn commit(mut self, req: CommitRequest) -> Result<CommitResponse, Error> {
self.finalized = true;
self.cancel.cancel();
self.client.commit_reservation(&self.id, &req).await
}
pub async fn release(mut self, reason: impl Into<String>) -> Result<ReleaseResponse, Error> {
self.finalized = true;
self.cancel.cancel();
let req = ReleaseRequest::new(Some(reason.into()));
self.client.release_reservation(&self.id, &req).await
}
pub async fn extend(&self, extend_by_ms: u64) -> Result<(), Error> {
let req = crate::models::ExtendRequest::new(extend_by_ms);
self.client.extend_reservation(&self.id, &req).await?;
Ok(())
}
}
impl Drop for ReservationGuard {
fn drop(&mut self) {
self.cancel.cancel();
if !self.finalized {
tracing::warn!(
reservation_id = %self.id,
"ReservationGuard dropped without commit/release; attempting best-effort release"
);
if let Ok(handle) = tokio::runtime::Handle::try_current() {
let client = self.client.clone();
let id = self.id.clone();
handle.spawn(async move {
let req = ReleaseRequest::new(Some("guard_dropped".to_string()));
let _ = client.release_reservation(&id, &req).await;
});
}
}
}
}
impl std::fmt::Debug for ReservationGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReservationGuard")
.field("id", &self.id)
.field("decision", &self.decision)
.field("finalized", &self.finalized)
.finish()
}
}
const _: fn() = || {
fn assert_send<T: Send>() {}
assert_send::<ReservationGuard>();
};