use crate::tracing_compat::{error, info, trace};
use crate::types::{ObligationId, RegionId, TaskId, Time};
use core::fmt;
use std::backtrace::Backtrace;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceLocation {
pub file: &'static str,
pub line: u32,
pub column: u32,
}
impl SourceLocation {
#[must_use]
pub const fn unknown() -> Self {
Self {
file: "<unknown>",
line: 0,
column: 0,
}
}
#[must_use]
pub fn from_panic_location(location: &'static std::panic::Location<'static>) -> Self {
Self {
file: location.file(),
line: location.line(),
column: location.column(),
}
}
}
impl fmt::Display for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ObligationKind {
SendPermit,
Ack,
Lease,
IoOp,
SemaphorePermit,
}
impl ObligationKind {
#[inline]
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::SendPermit => "send_permit",
Self::Ack => "ack",
Self::Lease => "lease",
Self::IoOp => "io_op",
Self::SemaphorePermit => "semaphore_permit",
}
}
}
impl fmt::Display for ObligationKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObligationAbortReason {
Cancel,
Error,
Explicit,
}
impl ObligationAbortReason {
#[inline]
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Cancel => "cancel",
Self::Error => "error",
Self::Explicit => "explicit",
}
}
}
impl fmt::Display for ObligationAbortReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObligationState {
Reserved,
Committed,
Aborted,
Leaked,
}
impl ObligationState {
#[inline]
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Committed | Self::Aborted | Self::Leaked)
}
#[inline]
#[must_use]
pub const fn is_resolved(self) -> bool {
self.is_terminal()
}
#[inline]
#[must_use]
pub const fn is_success(self) -> bool {
matches!(self, Self::Committed | Self::Aborted)
}
#[inline]
#[must_use]
pub const fn is_leaked(self) -> bool {
matches!(self, Self::Leaked)
}
}
#[derive(Debug)]
pub struct ObligationRecord {
pub id: ObligationId,
pub kind: ObligationKind,
pub holder: TaskId,
pub region: RegionId,
pub state: ObligationState,
pub description: Option<String>,
pub acquired_at: SourceLocation,
pub acquire_backtrace: Option<Arc<Backtrace>>,
pub reserved_at: Time,
pub resolved_at: Option<Time>,
pub abort_reason: Option<ObligationAbortReason>,
}
impl ObligationRecord {
#[must_use]
pub fn new(
id: ObligationId,
kind: ObligationKind,
holder: TaskId,
region: RegionId,
reserved_at: Time,
) -> Self {
Self::new_with_context(
id,
kind,
holder,
region,
reserved_at,
SourceLocation::unknown(),
None,
)
}
#[must_use]
pub fn new_with_context(
id: ObligationId,
kind: ObligationKind,
holder: TaskId,
region: RegionId,
reserved_at: Time,
acquired_at: SourceLocation,
acquire_backtrace: Option<Arc<Backtrace>>,
) -> Self {
trace!(
obligation_id = ?id,
kind = %kind,
holder_task = ?holder,
owning_region = ?region,
reserved_at = ?reserved_at,
acquired_at = %acquired_at,
"obligation reserved"
);
Self {
id,
kind,
holder,
region,
state: ObligationState::Reserved,
description: None,
acquired_at,
acquire_backtrace,
reserved_at,
resolved_at: None,
abort_reason: None,
}
}
#[must_use]
pub fn with_description(
id: ObligationId,
kind: ObligationKind,
holder: TaskId,
region: RegionId,
reserved_at: Time,
description: impl Into<String>,
) -> Self {
Self::with_description_and_context(
id,
kind,
holder,
region,
reserved_at,
description,
SourceLocation::unknown(),
None,
)
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn with_description_and_context(
id: ObligationId,
kind: ObligationKind,
holder: TaskId,
region: RegionId,
reserved_at: Time,
description: impl Into<String>,
acquired_at: SourceLocation,
acquire_backtrace: Option<Arc<Backtrace>>,
) -> Self {
let desc = description.into();
trace!(
obligation_id = ?id,
kind = %kind,
holder_task = ?holder,
owning_region = ?region,
reserved_at = ?reserved_at,
description = %desc,
acquired_at = %acquired_at,
"obligation reserved"
);
Self {
id,
kind,
holder,
region,
state: ObligationState::Reserved,
description: Some(desc),
acquired_at,
acquire_backtrace,
reserved_at,
resolved_at: None,
abort_reason: None,
}
}
#[inline]
#[must_use]
pub const fn is_pending(&self) -> bool {
matches!(self.state, ObligationState::Reserved)
}
pub fn commit(&mut self, now: Time) -> u64 {
assert!(self.is_pending(), "obligation already resolved");
self.state = ObligationState::Committed;
self.resolved_at = Some(now);
self.abort_reason = None;
let duration_held = now.duration_since(self.reserved_at);
info!(
obligation_id = ?self.id,
kind = %self.kind,
duration_held_ns = duration_held,
"obligation committed"
);
duration_held
}
pub fn abort(&mut self, now: Time, reason: ObligationAbortReason) -> u64 {
assert!(self.is_pending(), "obligation already resolved");
self.state = ObligationState::Aborted;
self.resolved_at = Some(now);
self.abort_reason = Some(reason);
let duration_held = now.duration_since(self.reserved_at);
info!(
obligation_id = ?self.id,
kind = %self.kind,
abort_reason = %reason,
duration_held_ns = duration_held,
"obligation aborted"
);
duration_held
}
pub fn mark_leaked(&mut self, now: Time) -> u64 {
assert!(self.is_pending(), "obligation already resolved");
self.state = ObligationState::Leaked;
self.resolved_at = Some(now);
self.abort_reason = None;
let duration_held = now.duration_since(self.reserved_at);
error!(
obligation_id = ?self.id,
kind = %self.kind,
holder_task = ?self.holder,
owning_region = ?self.region,
duration_held_ns = duration_held,
description = ?self.description,
"OBLIGATION LEAKED: holder completed without resolving obligation"
);
duration_held
}
#[must_use]
pub const fn is_leaked(&self) -> bool {
self.state.is_leaked()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::ArenaIndex;
fn init_test(name: &str) {
crate::test_utils::init_test_logging();
crate::test_phase!(name);
}
fn test_ids() -> (ObligationId, TaskId, RegionId) {
(
ObligationId::from_arena(ArenaIndex::new(0, 0)),
TaskId::from_arena(ArenaIndex::new(0, 0)),
RegionId::from_arena(ArenaIndex::new(0, 0)),
)
}
#[test]
fn obligation_state_predicates() {
init_test("obligation_state_predicates");
let reserved_terminal = ObligationState::Reserved.is_terminal();
crate::assert_with_log!(
!reserved_terminal,
"reserved terminal",
false,
reserved_terminal
);
let committed_terminal = ObligationState::Committed.is_terminal();
crate::assert_with_log!(
committed_terminal,
"committed terminal",
true,
committed_terminal
);
let aborted_terminal = ObligationState::Aborted.is_terminal();
crate::assert_with_log!(aborted_terminal, "aborted terminal", true, aborted_terminal);
let leaked_terminal = ObligationState::Leaked.is_terminal();
crate::assert_with_log!(leaked_terminal, "leaked terminal", true, leaked_terminal);
let reserved_resolved = ObligationState::Reserved.is_resolved();
crate::assert_with_log!(
!reserved_resolved,
"reserved resolved",
false,
reserved_resolved
);
let committed_resolved = ObligationState::Committed.is_resolved();
crate::assert_with_log!(
committed_resolved,
"committed resolved",
true,
committed_resolved
);
let aborted_resolved = ObligationState::Aborted.is_resolved();
crate::assert_with_log!(aborted_resolved, "aborted resolved", true, aborted_resolved);
let leaked_resolved = ObligationState::Leaked.is_resolved();
crate::assert_with_log!(leaked_resolved, "leaked resolved", true, leaked_resolved);
let reserved_success = ObligationState::Reserved.is_success();
crate::assert_with_log!(
!reserved_success,
"reserved success",
false,
reserved_success
);
let committed_success = ObligationState::Committed.is_success();
crate::assert_with_log!(
committed_success,
"committed success",
true,
committed_success
);
let aborted_success = ObligationState::Aborted.is_success();
crate::assert_with_log!(aborted_success, "aborted success", true, aborted_success);
let leaked_success = ObligationState::Leaked.is_success();
crate::assert_with_log!(!leaked_success, "leaked success", false, leaked_success);
let reserved_leaked = ObligationState::Reserved.is_leaked();
crate::assert_with_log!(!reserved_leaked, "reserved leaked", false, reserved_leaked);
let committed_leaked = ObligationState::Committed.is_leaked();
crate::assert_with_log!(
!committed_leaked,
"committed leaked",
false,
committed_leaked
);
let aborted_leaked = ObligationState::Aborted.is_leaked();
crate::assert_with_log!(!aborted_leaked, "aborted leaked", false, aborted_leaked);
let leaked_leaked = ObligationState::Leaked.is_leaked();
crate::assert_with_log!(leaked_leaked, "leaked leaked", true, leaked_leaked);
crate::test_complete!("obligation_state_predicates");
}
#[test]
fn obligation_lifecycle_commit() {
init_test("obligation_lifecycle_commit");
let (oid, tid, rid) = test_ids();
let reserved_at = Time::from_nanos(10);
let mut ob = ObligationRecord::new(oid, ObligationKind::SendPermit, tid, rid, reserved_at);
let pending = ob.is_pending();
crate::assert_with_log!(pending, "pending", true, pending);
let leaked = ob.is_leaked();
crate::assert_with_log!(!leaked, "leaked", false, leaked);
crate::assert_with_log!(
ob.state == ObligationState::Reserved,
"state",
ObligationState::Reserved,
ob.state
);
let duration = ob.commit(Time::from_nanos(25));
let pending = ob.is_pending();
crate::assert_with_log!(!pending, "pending", false, pending);
let leaked = ob.is_leaked();
crate::assert_with_log!(!leaked, "leaked", false, leaked);
crate::assert_with_log!(
ob.state == ObligationState::Committed,
"state",
ObligationState::Committed,
ob.state
);
crate::assert_with_log!(duration == 15, "duration", 15, duration);
let resolved = ob.resolved_at;
crate::assert_with_log!(
resolved == Some(Time::from_nanos(25)),
"resolved_at",
Some(Time::from_nanos(25)),
resolved
);
crate::test_complete!("obligation_lifecycle_commit");
}
#[test]
fn obligation_lifecycle_abort() {
init_test("obligation_lifecycle_abort");
let (oid, tid, rid) = test_ids();
let reserved_at = Time::from_nanos(100);
let mut ob = ObligationRecord::new(oid, ObligationKind::Ack, tid, rid, reserved_at);
let duration = ob.abort(Time::from_nanos(140), ObligationAbortReason::Explicit);
let pending = ob.is_pending();
crate::assert_with_log!(!pending, "pending", false, pending);
let leaked = ob.is_leaked();
crate::assert_with_log!(!leaked, "leaked", false, leaked);
crate::assert_with_log!(
ob.state == ObligationState::Aborted,
"state",
ObligationState::Aborted,
ob.state
);
crate::assert_with_log!(duration == 40, "duration", 40, duration);
let reason = ob.abort_reason;
crate::assert_with_log!(
reason == Some(ObligationAbortReason::Explicit),
"abort_reason",
Some(ObligationAbortReason::Explicit),
reason
);
crate::test_complete!("obligation_lifecycle_abort");
}
#[test]
fn obligation_lifecycle_leaked() {
init_test("obligation_lifecycle_leaked");
let (oid, tid, rid) = test_ids();
let reserved_at = Time::from_nanos(5);
let mut ob = ObligationRecord::new(oid, ObligationKind::Lease, tid, rid, reserved_at);
let duration = ob.mark_leaked(Time::from_nanos(8));
let pending = ob.is_pending();
crate::assert_with_log!(!pending, "pending", false, pending);
let leaked = ob.is_leaked();
crate::assert_with_log!(leaked, "leaked", true, leaked);
crate::assert_with_log!(
ob.state == ObligationState::Leaked,
"state",
ObligationState::Leaked,
ob.state
);
crate::assert_with_log!(duration == 3, "duration", 3, duration);
crate::test_complete!("obligation_lifecycle_leaked");
}
#[test]
#[should_panic(expected = "obligation already resolved")]
fn double_commit_panics() {
init_test("double_commit_panics");
let (oid, tid, rid) = test_ids();
let mut ob = ObligationRecord::new(oid, ObligationKind::IoOp, tid, rid, Time::ZERO);
ob.commit(Time::ZERO);
ob.commit(Time::ZERO); }
#[test]
#[should_panic(expected = "obligation already resolved")]
fn double_abort_panics() {
init_test("double_abort_panics");
let (oid, tid, rid) = test_ids();
let mut ob = ObligationRecord::new(oid, ObligationKind::IoOp, tid, rid, Time::ZERO);
ob.abort(Time::ZERO, ObligationAbortReason::Explicit);
ob.abort(Time::ZERO, ObligationAbortReason::Explicit); }
#[test]
#[should_panic(expected = "obligation already resolved")]
fn commit_after_abort_panics() {
init_test("commit_after_abort_panics");
let (oid, tid, rid) = test_ids();
let mut ob = ObligationRecord::new(oid, ObligationKind::SendPermit, tid, rid, Time::ZERO);
ob.abort(Time::ZERO, ObligationAbortReason::Cancel);
ob.commit(Time::ZERO); }
#[test]
#[should_panic(expected = "obligation already resolved")]
fn mark_leaked_after_commit_panics() {
init_test("mark_leaked_after_commit_panics");
let (oid, tid, rid) = test_ids();
let mut ob = ObligationRecord::new(oid, ObligationKind::SendPermit, tid, rid, Time::ZERO);
ob.commit(Time::ZERO);
ob.mark_leaked(Time::ZERO); }
#[test]
fn obligation_kinds_are_distinguishable() {
init_test("obligation_kinds_are_distinguishable");
crate::assert_with_log!(
ObligationKind::SendPermit != ObligationKind::Ack,
"send != ack",
"not equal",
(ObligationKind::SendPermit, ObligationKind::Ack)
);
crate::assert_with_log!(
ObligationKind::Ack != ObligationKind::Lease,
"ack != lease",
"not equal",
(ObligationKind::Ack, ObligationKind::Lease)
);
crate::assert_with_log!(
ObligationKind::Lease != ObligationKind::IoOp,
"lease != ioop",
"not equal",
(ObligationKind::Lease, ObligationKind::IoOp)
);
crate::test_complete!("obligation_kinds_are_distinguishable");
}
#[test]
fn with_description_sets_description() {
init_test("with_description_sets_description");
let (oid, tid, rid) = test_ids();
let ob = ObligationRecord::with_description(
oid,
ObligationKind::SendPermit,
tid,
rid,
Time::ZERO,
"test description",
);
crate::assert_with_log!(
ob.description == Some("test description".to_string()),
"description",
Some("test description".to_string()),
ob.description
);
crate::test_complete!("with_description_sets_description");
}
#[test]
fn source_location_display() {
let loc = SourceLocation {
file: "src/main.rs",
line: 42,
column: 5,
};
assert_eq!(loc.to_string(), "src/main.rs:42:5");
}
#[test]
fn source_location_unknown() {
let loc = SourceLocation::unknown();
assert_eq!(loc.file, "<unknown>");
assert_eq!(loc.line, 0);
assert_eq!(loc.column, 0);
assert_eq!(loc.to_string(), "<unknown>:0:0");
}
#[test]
fn source_location_debug_copy_eq() {
let loc = SourceLocation {
file: "f.rs",
line: 1,
column: 1,
};
let dbg = format!("{loc:?}");
assert!(dbg.contains("f.rs"));
let loc2 = loc;
assert_eq!(loc, loc2);
let loc3 = SourceLocation {
file: "g.rs",
line: 1,
column: 1,
};
assert_ne!(loc, loc3);
}
#[test]
fn obligation_kind_display_all() {
assert_eq!(ObligationKind::SendPermit.to_string(), "send_permit");
assert_eq!(ObligationKind::Ack.to_string(), "ack");
assert_eq!(ObligationKind::Lease.to_string(), "lease");
assert_eq!(ObligationKind::IoOp.to_string(), "io_op");
}
#[test]
fn obligation_kind_as_str_all() {
assert_eq!(ObligationKind::SendPermit.as_str(), "send_permit");
assert_eq!(ObligationKind::Ack.as_str(), "ack");
assert_eq!(ObligationKind::Lease.as_str(), "lease");
assert_eq!(ObligationKind::IoOp.as_str(), "io_op");
}
#[test]
fn obligation_kind_debug_copy_hash_ord() {
use std::collections::HashSet;
let k = ObligationKind::Lease;
let dbg = format!("{k:?}");
assert!(dbg.contains("Lease"));
let k2 = k;
assert_eq!(k, k2);
let mut set = HashSet::new();
set.insert(ObligationKind::SendPermit);
set.insert(ObligationKind::Ack);
set.insert(ObligationKind::Lease);
set.insert(ObligationKind::IoOp);
assert_eq!(set.len(), 4);
let mut kinds = [
ObligationKind::IoOp,
ObligationKind::SendPermit,
ObligationKind::Lease,
ObligationKind::Ack,
];
kinds.sort();
assert_eq!(kinds[0], ObligationKind::SendPermit);
}
#[test]
fn obligation_abort_reason_display_all() {
assert_eq!(ObligationAbortReason::Cancel.to_string(), "cancel");
assert_eq!(ObligationAbortReason::Error.to_string(), "error");
assert_eq!(ObligationAbortReason::Explicit.to_string(), "explicit");
}
#[test]
fn obligation_abort_reason_debug_copy_eq() {
let r = ObligationAbortReason::Cancel;
let dbg = format!("{r:?}");
assert!(dbg.contains("Cancel"));
let r2 = r;
assert_eq!(r, r2);
assert_ne!(ObligationAbortReason::Cancel, ObligationAbortReason::Error);
}
#[test]
fn obligation_state_debug_copy_eq() {
let states = [
ObligationState::Reserved,
ObligationState::Committed,
ObligationState::Aborted,
ObligationState::Leaked,
];
for s in &states {
let dbg = format!("{s:?}");
assert!(!dbg.is_empty());
let s2 = *s;
assert_eq!(*s, s2);
}
assert_ne!(ObligationState::Reserved, ObligationState::Committed);
assert_ne!(ObligationState::Aborted, ObligationState::Leaked);
}
#[test]
fn obligation_record_new_defaults() {
let (oid, tid, rid) = test_ids();
let ob = ObligationRecord::new(oid, ObligationKind::IoOp, tid, rid, Time::ZERO);
assert_eq!(ob.state, ObligationState::Reserved);
assert!(ob.description.is_none());
assert!(ob.resolved_at.is_none());
assert!(ob.abort_reason.is_none());
assert!(ob.acquire_backtrace.is_none());
assert_eq!(ob.acquired_at, SourceLocation::unknown());
}
#[test]
fn obligation_record_debug() {
let (oid, tid, rid) = test_ids();
let ob = ObligationRecord::new(oid, ObligationKind::SendPermit, tid, rid, Time::ZERO);
let dbg = format!("{ob:?}");
assert!(dbg.contains("ObligationRecord"));
assert!(dbg.contains("SendPermit"));
}
#[test]
fn source_location_debug_clone_copy_eq() {
let loc = SourceLocation::unknown();
let dbg = format!("{loc:?}");
assert!(dbg.contains("SourceLocation"), "{dbg}");
let copied: SourceLocation = loc;
let cloned = loc;
assert_eq!(copied, cloned);
}
#[test]
fn obligation_kind_debug_clone_copy_hash_eq() {
use std::collections::HashSet;
let k = ObligationKind::Lease;
let dbg = format!("{k:?}");
assert!(dbg.contains("Lease"), "{dbg}");
let copied: ObligationKind = k;
let cloned = k;
assert_eq!(copied, cloned);
assert!(k < ObligationKind::IoOp);
let mut set = HashSet::new();
set.insert(ObligationKind::SendPermit);
set.insert(ObligationKind::Ack);
set.insert(ObligationKind::Lease);
set.insert(ObligationKind::IoOp);
assert_eq!(set.len(), 4);
}
#[test]
fn obligation_abort_reason_debug_clone_copy_eq() {
let r = ObligationAbortReason::Cancel;
let dbg = format!("{r:?}");
assert!(dbg.contains("Cancel"), "{dbg}");
let copied: ObligationAbortReason = r;
let cloned = r;
assert_eq!(copied, cloned);
assert_ne!(r, ObligationAbortReason::Explicit);
}
}