use std::fmt::Debug;
use std::ptr::NonNull;
use std::sync::Arc;
use crate::binding::{MechanismOperation, ReleaseGate};
use crate::{
AllocationIdentity, AuthorityIdentity, BindingIdentity, DeviceAllocator, DeviceKey,
MemoryBinding, ProviderContextIdentity,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AllocationReleaseState {
Live,
Queued,
PartiallyUnmapped,
Released,
DeviceLost,
Quarantined,
}
impl AllocationReleaseState {
pub const fn retains_ownership(self) -> bool {
matches!(
self,
Self::Live
| Self::Queued
| Self::PartiallyUnmapped
| Self::DeviceLost
| Self::Quarantined
)
}
pub const fn permits_allocator_call(self) -> bool {
matches!(self, Self::Live | Self::Queued)
}
pub const fn is_terminal(self) -> bool {
matches!(
self,
Self::Released | Self::DeviceLost | Self::Quarantined | Self::PartiallyUnmapped
)
}
pub const fn name(self) -> &'static str {
match self {
Self::Live => "live",
Self::Queued => "queued",
Self::PartiallyUnmapped => "partially unmapped",
Self::Released => "released",
Self::DeviceLost => "device lost",
Self::Quarantined => "quarantined",
}
}
}
impl std::fmt::Display for AllocationReleaseState {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.name())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct ReleaseAccounting {
pub allocation_bytes: u64,
pub unmapped_bytes: u64,
}
impl ReleaseAccounting {
pub const fn new(allocation_bytes: u64, unmapped_bytes: u64) -> Self {
Self {
allocation_bytes,
unmapped_bytes,
}
}
pub const fn eager(allocation_bytes: u64) -> Self {
Self::new(allocation_bytes, 0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum QuarantineReason {
AbandonedRequest,
OwnerDropped,
EnqueueRejected(DeferredEnqueueRejection),
DeviceLost,
MechanismTerminated,
PartialRelease,
AllocatorRefused,
StatePoisoned,
}
impl QuarantineReason {
pub const fn name(self) -> &'static str {
match self {
Self::AbandonedRequest => "a prepared release request was abandoned",
Self::OwnerDropped => "an owning allocation was dropped without explicit release",
Self::EnqueueRejected(_) => "the deferred release queue refused the request",
Self::DeviceLost => "the device or provider context was lost",
Self::MechanismTerminated => "the mechanism was already terminated",
Self::PartialRelease => "the allocator released only part of the allocation",
Self::AllocatorRefused => "the allocator refused after the record was retired",
Self::StatePoisoned => "mechanism state was poisoned",
}
}
}
impl std::fmt::Display for QuarantineReason {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.name())?;
if let Self::EnqueueRejected(rejection) = self {
write!(formatter, " ({})", rejection.name())?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ResidualOwnership {
pub state: AllocationReleaseState,
pub reason: QuarantineReason,
pub retained_bytes: u64,
pub address: usize,
pub align: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReleaseFailure {
reason: Arc<str>,
}
impl ReleaseFailure {
pub fn new(reason: impl Into<Arc<str>>) -> Self {
Self {
reason: reason.into(),
}
}
pub fn reason(&self) -> &str {
&self.reason
}
}
impl std::fmt::Display for ReleaseFailure {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.reason)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AllocationReleaseOutcome {
Complete { accounting: ReleaseAccounting },
Quarantined {
accounting: ReleaseAccounting,
residual: ResidualOwnership,
},
Failed { failure: ReleaseFailure },
}
impl AllocationReleaseOutcome {
pub const fn complete(accounting: ReleaseAccounting) -> Self {
Self::Complete { accounting }
}
pub const fn quarantined(accounting: ReleaseAccounting, residual: ResidualOwnership) -> Self {
Self::Quarantined {
accounting,
residual,
}
}
pub fn failed(reason: impl Into<Arc<str>>) -> Self {
Self::Failed {
failure: ReleaseFailure::new(reason),
}
}
pub const fn state(&self) -> AllocationReleaseState {
match self {
Self::Complete { .. } => AllocationReleaseState::Released,
Self::Quarantined { residual, .. } => residual.state,
Self::Failed { .. } => AllocationReleaseState::Live,
}
}
pub const fn accounting(&self) -> Option<ReleaseAccounting> {
match self {
Self::Complete { accounting } | Self::Quarantined { accounting, .. } => {
Some(*accounting)
}
Self::Failed { .. } => None,
}
}
pub const fn residual(&self) -> Option<ResidualOwnership> {
match self {
Self::Quarantined { residual, .. } => Some(*residual),
_ => None,
}
}
pub const fn failure(&self) -> Option<&ReleaseFailure> {
match self {
Self::Failed { failure } => Some(failure),
_ => None,
}
}
pub const fn is_complete(&self) -> bool {
matches!(self, Self::Complete { .. })
}
pub const fn is_quarantined(&self) -> bool {
matches!(self, Self::Quarantined { .. })
}
pub const fn unmapped_bytes(&self) -> u64 {
match self {
Self::Complete { accounting } | Self::Quarantined { accounting, .. } => {
accounting.unmapped_bytes
}
Self::Failed { .. } => 0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DeferredEnqueueRejection {
Closed,
Full,
DeviceLost,
Refused,
}
impl DeferredEnqueueRejection {
pub const fn name(self) -> &'static str {
match self {
Self::Closed => "closed",
Self::Full => "full",
Self::DeviceLost => "device lost",
Self::Refused => "refused",
}
}
}
#[derive(Debug)]
pub struct DeferredEnqueueError {
rejection: DeferredEnqueueRejection,
request: Box<PreparedAllocationRelease>,
}
impl DeferredEnqueueError {
pub fn new(rejection: DeferredEnqueueRejection, request: PreparedAllocationRelease) -> Self {
Self {
rejection,
request: Box::new(request),
}
}
pub const fn rejection(&self) -> DeferredEnqueueRejection {
self.rejection
}
pub const fn request(&self) -> &PreparedAllocationRelease {
&self.request
}
pub fn into_request(self) -> PreparedAllocationRelease {
*self.request
}
pub fn quarantine(self) -> AllocationReleaseOutcome {
let rejection = self.rejection;
(*self.request).quarantine(QuarantineReason::EnqueueRejected(rejection))
}
}
impl std::fmt::Display for DeferredEnqueueError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"the deferred release queue refused allocation {:?}: {}",
self.request.identity(),
self.rejection.name()
)
}
}
impl std::error::Error for DeferredEnqueueError {}
pub trait DeferredReleaseQueue: Send + Sync + Debug {
fn enqueue(&self, request: PreparedAllocationRelease) -> Result<(), DeferredEnqueueError>;
fn pending(&self) -> usize {
0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DeferredReleaseDisposition {
Queued { identity: AllocationIdentity },
Quarantined {
identity: AllocationIdentity,
rejection: DeferredEnqueueRejection,
outcome: AllocationReleaseOutcome,
},
}
impl DeferredReleaseDisposition {
pub const fn identity(&self) -> AllocationIdentity {
match self {
Self::Queued { identity } | Self::Quarantined { identity, .. } => *identity,
}
}
pub const fn state(&self) -> AllocationReleaseState {
match self {
Self::Queued { .. } => AllocationReleaseState::Queued,
Self::Quarantined { .. } => AllocationReleaseState::Quarantined,
}
}
pub const fn is_queued(&self) -> bool {
matches!(self, Self::Queued { .. })
}
}
pub struct PreparedAllocationRelease {
binding: MemoryBinding,
identity: AllocationIdentity,
ptr: NonNull<u8>,
bytes: usize,
align: usize,
allocator: Arc<dyn DeviceAllocator>,
authority: AuthorityIdentity,
context: ProviderContextIdentity,
operation: Option<MechanismOperation>,
armed: bool,
}
unsafe impl Send for PreparedAllocationRelease {}
unsafe impl Sync for PreparedAllocationRelease {}
impl Debug for PreparedAllocationRelease {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PreparedAllocationRelease")
.field("identity", &self.identity)
.field("address", &(self.ptr.as_ptr() as usize))
.field("bytes", &self.bytes)
.field("align", &self.align)
.field("authority", &self.authority)
.field("provider_context", &self.context)
.field("armed", &self.armed)
.finish()
}
}
pub(crate) struct PreparedReleasePins {
pub(crate) allocator: Arc<dyn DeviceAllocator>,
pub(crate) authority: AuthorityIdentity,
pub(crate) context: ProviderContextIdentity,
pub(crate) operation: MechanismOperation,
}
impl PreparedAllocationRelease {
pub(crate) fn new(
binding: MemoryBinding,
identity: AllocationIdentity,
ptr: NonNull<u8>,
bytes: usize,
align: usize,
pins: PreparedReleasePins,
) -> Self {
Self {
binding,
identity,
ptr,
bytes,
align,
allocator: pins.allocator,
authority: pins.authority,
context: pins.context,
operation: Some(pins.operation),
armed: true,
}
}
pub const fn identity(&self) -> AllocationIdentity {
self.identity
}
pub const fn binding_identity(&self) -> BindingIdentity {
self.identity.binding()
}
pub const fn device(&self) -> DeviceKey {
self.identity.binding().device()
}
pub const fn authority(&self) -> AuthorityIdentity {
self.authority
}
pub const fn provider_context(&self) -> ProviderContextIdentity {
self.context
}
pub fn allocator(&self) -> &Arc<dyn DeviceAllocator> {
&self.allocator
}
pub const fn as_ptr(&self) -> NonNull<u8> {
self.ptr
}
pub const fn len(&self) -> usize {
self.bytes
}
pub const fn is_empty(&self) -> bool {
self.bytes == 0
}
pub const fn alignment(&self) -> usize {
self.align
}
pub const fn state(&self) -> AllocationReleaseState {
AllocationReleaseState::Queued
}
pub fn execute(mut self) -> AllocationReleaseOutcome {
self.armed = false;
match self.binding.mechanism().release_gate() {
ReleaseGate::Allowed => {}
ReleaseGate::DeviceLost => {
return self.settle_quarantine(
ReleaseAccounting::new(self.bytes as u64, 0),
AllocationReleaseState::DeviceLost,
QuarantineReason::DeviceLost,
self.bytes as u64,
);
}
ReleaseGate::Terminated => {
return self.settle_quarantine(
ReleaseAccounting::new(self.bytes as u64, 0),
AllocationReleaseState::Quarantined,
QuarantineReason::MechanismTerminated,
self.bytes as u64,
);
}
ReleaseGate::Poisoned => {
return self.settle_quarantine(
ReleaseAccounting::new(self.bytes as u64, 0),
AllocationReleaseState::Quarantined,
QuarantineReason::StatePoisoned,
self.bytes as u64,
);
}
}
let outcome = unsafe { self.allocator.release(self.ptr, self.bytes, self.align) };
match outcome {
AllocationReleaseOutcome::Complete { accounting } => {
self.settle_released();
AllocationReleaseOutcome::Complete { accounting }
}
AllocationReleaseOutcome::Quarantined {
accounting,
residual,
} => self.settle_quarantine(
accounting,
residual.state,
residual.reason,
residual.retained_bytes,
),
AllocationReleaseOutcome::Failed { .. } => {
let bytes = self.bytes as u64;
self.settle_quarantine(
ReleaseAccounting::new(bytes, 0),
AllocationReleaseState::Quarantined,
QuarantineReason::AllocatorRefused,
bytes,
)
}
}
}
pub fn quarantine_device_lost(mut self) -> AllocationReleaseOutcome {
self.armed = false;
let bytes = self.bytes as u64;
self.settle_quarantine(
ReleaseAccounting::new(bytes, 0),
AllocationReleaseState::DeviceLost,
QuarantineReason::DeviceLost,
bytes,
)
}
pub fn quarantine(mut self, reason: QuarantineReason) -> AllocationReleaseOutcome {
self.armed = false;
let bytes = self.bytes as u64;
self.settle_quarantine(
ReleaseAccounting::new(bytes, 0),
AllocationReleaseState::Quarantined,
reason,
bytes,
)
}
fn settle_released(&mut self) {
self.binding.mechanism().settle_release(self.identity);
self.operation = None;
}
fn settle_quarantine(
&mut self,
accounting: ReleaseAccounting,
state: AllocationReleaseState,
reason: QuarantineReason,
retained_bytes: u64,
) -> AllocationReleaseOutcome {
let residual = ResidualOwnership {
state,
reason,
retained_bytes,
address: self.ptr.as_ptr() as usize,
align: self.align,
};
self.binding
.mechanism()
.settle_quarantine(QuarantinedAllocation {
identity: self.identity,
address: residual.address,
bytes: self.bytes,
align: self.align,
state,
reason,
retained_bytes,
});
self.operation = None;
AllocationReleaseOutcome::Quarantined {
accounting,
residual,
}
}
}
impl Drop for PreparedAllocationRelease {
fn drop(&mut self) {
if !self.armed {
return;
}
self.armed = false;
let bytes = self.bytes as u64;
let _ = self.settle_quarantine(
ReleaseAccounting::new(bytes, 0),
AllocationReleaseState::Quarantined,
QuarantineReason::AbandonedRequest,
bytes,
);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct QuarantinedAllocation {
pub identity: AllocationIdentity,
pub address: usize,
pub bytes: usize,
pub align: usize,
pub state: AllocationReleaseState,
pub reason: QuarantineReason,
pub retained_bytes: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_unmapped_bytes_is_a_valid_complete_outcome() {
let outcome = AllocationReleaseOutcome::complete(ReleaseAccounting::eager(4096));
assert!(outcome.is_complete());
assert_eq!(outcome.unmapped_bytes(), 0);
assert_eq!(outcome.state(), AllocationReleaseState::Released);
assert!(outcome.residual().is_none());
}
#[test]
fn failure_is_the_only_unchanged_shape() {
let failed = AllocationReleaseOutcome::failed("driver busy");
assert_eq!(failed.state(), AllocationReleaseState::Live);
assert!(failed.accounting().is_none());
assert_eq!(
failed.failure().map(ReleaseFailure::reason),
Some("driver busy")
);
}
#[test]
fn states_answer_ownership_and_callback_questions() {
assert!(AllocationReleaseState::Live.retains_ownership());
assert!(!AllocationReleaseState::Released.retains_ownership());
assert!(!AllocationReleaseState::DeviceLost.permits_allocator_call());
assert!(!AllocationReleaseState::Quarantined.permits_allocator_call());
assert!(AllocationReleaseState::Queued.permits_allocator_call());
assert!(AllocationReleaseState::PartiallyUnmapped.is_terminal());
}
}