use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::Mutex;
use crate::{
InspectOwnedCapacityV1, OwnedCapacityInspectionErrorV1, OwnedCapacityInspectionV1,
OwnedCapacitySharedV1,
};
type AttachedOwner = crate::exact_provider_budget_v1::SharedOwnershipCharge;
struct State {
live: bool,
owner: Option<AttachedOwner>,
}
struct Control {
state: Mutex<State>,
}
impl Control {
fn attached_owner(&self) -> Result<Option<AttachedOwner>, OwnedCapacityInspectionErrorV1> {
let state = self
.state
.lock()
.map_err(|_| OwnedCapacityInspectionErrorV1)?;
if !state.live {
return Err(OwnedCapacityInspectionErrorV1);
}
Ok(state.owner.clone())
}
fn attach(
&self,
candidate: AttachedOwner,
) -> Result<AttachedOwner, OwnedCapacityInspectionErrorV1> {
let mut state = self
.state
.lock()
.map_err(|_| OwnedCapacityInspectionErrorV1)?;
if !state.live {
return Err(OwnedCapacityInspectionErrorV1);
}
match state.owner.as_ref() {
Some(existing) => Ok(existing.clone()),
None => {
state.owner = Some(candidate.clone());
Ok(candidate)
}
}
}
}
#[doc(hidden)]
pub struct OwnedCapacityLifecycleV1 {
control: OwnedCapacitySharedV1<Control>,
}
impl Default for OwnedCapacityLifecycleV1 {
fn default() -> Self {
Self {
control: OwnedCapacitySharedV1::new(Control {
state: Mutex::new(State {
live: true,
owner: None,
}),
}),
}
}
}
impl Clone for OwnedCapacityLifecycleV1 {
fn clone(&self) -> Self {
Self::default()
}
}
impl Drop for OwnedCapacityLifecycleV1 {
fn drop(&mut self) {
let mut state = self
.control
.state
.lock()
.unwrap_or_else(|error| error.into_inner());
state.live = false;
}
}
impl OwnedCapacityLifecycleV1 {
pub fn control_allocation_upper_bound() -> Result<u64, OwnedCapacityInspectionErrorV1> {
crate::owned_capacity::allocation_upper_bound(
std::mem::size_of::<Control>() + 2 * std::mem::size_of::<usize>(),
std::mem::align_of::<Control>(),
)
}
pub fn reference(&self) -> OwnedCapacityLifecycleReferenceV1<'_> {
OwnedCapacityLifecycleReferenceV1(&self.control)
}
pub fn observer(
&self,
) -> Result<OwnedCapacityLifecycleObserverV1, OwnedCapacityInspectionErrorV1> {
let state = self
.control
.state
.lock()
.map_err(|_| OwnedCapacityInspectionErrorV1)?;
if !state.live || state.owner.is_none() {
return Err(OwnedCapacityInspectionErrorV1);
}
Ok(OwnedCapacityLifecycleObserverV1(self.control.clone()))
}
}
impl fmt::Debug for OwnedCapacityLifecycleV1 {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("OwnedCapacityLifecycleV1([REDACTED])")
}
}
impl PartialEq for OwnedCapacityLifecycleV1 {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl Eq for OwnedCapacityLifecycleV1 {}
impl PartialOrd for OwnedCapacityLifecycleV1 {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OwnedCapacityLifecycleV1 {
fn cmp(&self, _: &Self) -> Ordering {
Ordering::Equal
}
}
impl Hash for OwnedCapacityLifecycleV1 {
fn hash<H: Hasher>(&self, _: &mut H) {}
}
impl InspectOwnedCapacityV1 for OwnedCapacityLifecycleV1 {
fn inspect_owned_capacity(
&self,
inspection: &mut OwnedCapacityInspectionV1,
) -> Result<(), OwnedCapacityInspectionErrorV1> {
inspection.allocation(
std::mem::size_of::<Control>() + 2 * std::mem::size_of::<usize>(),
std::mem::align_of::<Control>(),
)
}
}
#[doc(hidden)]
#[derive(Clone, Copy)]
pub struct OwnedCapacityLifecycleReferenceV1<'owner>(&'owner Control);
impl fmt::Debug for OwnedCapacityLifecycleReferenceV1<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("OwnedCapacityLifecycleReferenceV1([REDACTED])")
}
}
impl OwnedCapacityLifecycleReferenceV1<'_> {
pub fn attached_owner(&self) -> Result<Option<AttachedOwner>, OwnedCapacityInspectionErrorV1> {
self.0.attached_owner()
}
pub fn attach(
&self,
candidate: AttachedOwner,
) -> Result<AttachedOwner, OwnedCapacityInspectionErrorV1> {
self.0.attach(candidate)
}
}
#[doc(hidden)]
#[derive(Clone)]
pub struct OwnedCapacityLifecycleObserverV1(OwnedCapacitySharedV1<Control>);
impl fmt::Debug for OwnedCapacityLifecycleObserverV1 {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("OwnedCapacityLifecycleObserverV1([REDACTED])")
}
}
impl OwnedCapacityLifecycleObserverV1 {
pub fn observes(&self, lifecycle: &OwnedCapacityLifecycleV1) -> bool {
self.0.same_allocation(&lifecycle.control)
}
pub fn attached_owner(&self) -> Result<Option<AttachedOwner>, OwnedCapacityInspectionErrorV1> {
self.0.attached_owner()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::exact_provider_budget_v1::ProviderBudget;
use std::sync::{
Arc, Barrier,
atomic::{AtomicBool, Ordering as AtomicOrdering},
};
struct Data {
destroyed: Arc<AtomicBool>,
budget: ProviderBudget,
}
impl Drop for Data {
fn drop(&mut self) {
assert_eq!(self.budget.observation().unwrap().owned_bytes, 128);
self.destroyed.store(true, AtomicOrdering::SeqCst);
}
}
struct Allocation {
_data: Data,
lifecycle: OwnedCapacityLifecycleV1,
}
#[test]
fn shared_allocation_charge_survives_captures_and_releases_after_data_destruction() {
let budget = ProviderBudget::new();
let destroyed = Arc::new(AtomicBool::new(false));
let allocation = OwnedCapacitySharedV1::new(Allocation {
_data: Data {
destroyed: destroyed.clone(),
budget: budget.clone(),
},
lifecycle: OwnedCapacityLifecycleV1::default(),
});
let captured = allocation.clone();
let charge = budget.reserve(128).unwrap().commit().unwrap();
drop(
allocation
.lifecycle
.reference()
.attach(charge.clone())
.unwrap(),
);
drop(charge);
drop(allocation);
assert!(!destroyed.load(AtomicOrdering::SeqCst));
assert_eq!(budget.observation().unwrap().owned_bytes, 128);
drop(captured);
assert!(destroyed.load(AtomicOrdering::SeqCst));
assert_eq!(budget.observation().unwrap().owned_bytes, 0);
assert_eq!(budget.observation().unwrap().released_allocations, 1);
}
#[test]
fn lifecycle_reference_cannot_keep_data_alive_or_revive_destroyed_ownership() {
let lifecycle = OwnedCapacityLifecycleV1::default();
let budget = ProviderBudget::new();
assert!(lifecycle.observer().is_err());
lifecycle
.reference()
.attach(budget.reserve(128).unwrap().commit().unwrap())
.unwrap();
let observer = lifecycle.observer().unwrap();
drop(lifecycle);
assert!(observer.attached_owner().is_err());
assert_eq!(budget.observation().unwrap().owned_bytes, 128);
assert_eq!(
format!("{observer:?}"),
"OwnedCapacityLifecycleObserverV1([REDACTED])"
);
drop(observer);
assert_eq!(budget.observation().unwrap().owned_bytes, 0);
}
#[test]
fn first_attachment_is_immutable_and_deep_clone_has_its_own_lifecycle() {
let lifecycle = OwnedCapacityLifecycleV1::default();
let budget = ProviderBudget::new();
let first = budget.reserve(128).unwrap().commit().unwrap();
let attached = lifecycle.reference().attach(first.clone()).unwrap();
assert!(attached.same_allocation(&first));
let retained = lifecycle
.reference()
.attach(budget.reserve(256).unwrap().commit().unwrap())
.unwrap();
assert!(retained.same_allocation(&first));
let cloned = lifecycle.clone();
let independent = cloned
.reference()
.attach(budget.reserve(256).unwrap().commit().unwrap())
.unwrap();
assert!(!independent.same_allocation(&first));
assert_eq!(independent.bytes(), 256);
assert_eq!(lifecycle, cloned);
}
#[test]
fn concurrent_attachments_have_one_immutable_owner_and_cancel_only_the_loser() {
let lifecycle = OwnedCapacityLifecycleV1::default();
let budget = ProviderBudget::new();
let start = Arc::new(Barrier::new(3));
let owners = std::thread::scope(|scope| {
let workers = (0..2)
.map(|_| {
let reference = lifecycle.reference();
let start = start.clone();
let budget = budget.clone();
scope.spawn(move || {
let candidate = budget.reserve(128).unwrap().commit().unwrap();
start.wait();
reference.attach(candidate).unwrap()
})
})
.collect::<Vec<_>>();
start.wait();
workers
.into_iter()
.map(|worker| worker.join().unwrap())
.collect::<Vec<_>>()
});
assert!(owners[0].same_allocation(&owners[1]));
assert_eq!(budget.observation().unwrap().owned_bytes, 128);
assert_eq!(budget.observation().unwrap().released_allocations, 1);
drop(owners);
assert_eq!(budget.observation().unwrap().owned_bytes, 128);
drop(lifecycle);
assert_eq!(budget.observation().unwrap().owned_bytes, 0);
assert_eq!(budget.observation().unwrap().released_allocations, 2);
}
}