Skip to main content

ferrum_interfaces/vnext/resource/
allocation.rs

1use super::{
2    invalid_resource, validate_runtime_descriptor_for_admission, Arc, AtomicBool, BufferDescriptor,
3    BufferRequest, DeviceCapacityClaim, DeviceId, DeviceRuntime, Ordering, PhantomData, RefCell,
4    ResourceAbandonSignal, ResourceDriverFailure, ResourceId, ResourcePoolIdentity,
5    ResourceReservation, ResourceReservationBatch, ResourceTransactionAction,
6    ResourceTransactionIdentity, ResourceTransactionState, StaticProvisioningBinding, VNextError,
7};
8
9#[derive(Debug, Clone, Copy)]
10pub(super) struct ResourceActionCursor {
11    pub(super) order: usize,
12    pub(super) action: ResourceTransactionAction,
13    pub(super) before: ResourceTransactionState,
14    pub(super) allocation_authorized: bool,
15}
16
17pub struct ResourceTransactionContext<'a, R>
18where
19    R: DeviceRuntime,
20{
21    pub(super) runtime: &'a Arc<R>,
22    pub(super) identity: &'a ResourceTransactionIdentity,
23    pub(super) binding: &'a StaticProvisioningBinding,
24    pub(super) reservations: &'a ResourceReservationBatch,
25    pub(super) cursor: Option<ResourceActionCursor>,
26    pub(super) allocation_authority: Option<&'a AtomicBool>,
27    pub(super) pending_allocation: Option<&'a RefCell<Option<CoreOwnedAllocation<R::Buffer>>>>,
28}
29
30impl<'a, R> ResourceTransactionContext<'a, R>
31where
32    R: DeviceRuntime,
33{
34    pub fn identity(&self) -> &ResourceTransactionIdentity {
35        self.identity
36    }
37
38    pub fn admission(&self) -> &StaticProvisioningBinding {
39        self.binding
40    }
41
42    pub fn reservations(&self) -> &ResourceReservationBatch {
43        self.reservations
44    }
45
46    fn allocation_permit<'permit>(
47        &'permit self,
48        request: &'permit BufferRequest,
49    ) -> Result<DeviceAllocationPermit<'permit>, VNextError> {
50        let cursor = self
51            .cursor
52            .filter(|cursor| {
53                cursor.action == ResourceTransactionAction::Commit
54                    && cursor.before == ResourceTransactionState::Reserved
55                    && cursor.allocation_authorized
56            })
57            .ok_or_else(|| {
58                invalid_resource("device allocation is authorized only during an exact commit")
59            })?;
60        let reservation = &self.reservations.reservations[cursor.order];
61        if request.resource_id() != reservation.resource_id()
62            || request.size_bytes() != reservation.size_bytes()
63            || request.alignment_bytes() != reservation.alignment_bytes()
64            || request.usage() != reservation.usage()
65            || request.element_type() != reservation.element_type()
66        {
67            return Err(invalid_resource(
68                "buffer request differs from the active admitted allocation",
69            ));
70        }
71        self.allocation_authority
72            .ok_or_else(|| invalid_resource("commit allocation authority is unavailable"))?
73            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
74            .map_err(|_| {
75                invalid_resource(
76                    "the active resource action already consumed its allocation permit",
77                )
78            })?;
79        Ok(DeviceAllocationPermit {
80            identity: self.identity,
81            binding: self.binding,
82            reservation,
83            request,
84            seal: AllocationSeal,
85        })
86    }
87
88    /// The only allocation path exposed to a transaction driver. A successful
89    /// runtime allocation is installed in core-owned pending storage before
90    /// this method returns. The receipt contains metadata only and is tied to
91    /// this commit call, so dropping it cannot lose buffer ownership.
92    pub fn allocate<'commit>(
93        &'commit self,
94        request: &BufferRequest,
95    ) -> Result<DeviceAllocationReceipt<'commit>, DeviceAllocationError<R::Error>> {
96        validate_runtime_descriptor_for_admission(
97            self.runtime.descriptor(),
98            self.binding,
99            "allocation preflight",
100        )
101        .map_err(DeviceAllocationError::Contract)?;
102        let permit = self
103            .allocation_permit(request)
104            .map_err(DeviceAllocationError::Contract)?;
105        let resource_id = permit.resource_id().clone();
106        let generation = permit.generation();
107        let allocation = self
108            .runtime
109            .allocate(permit)
110            .map_err(DeviceAllocationError::Runtime)?;
111        let reservation = &self.reservations.reservations[self
112            .cursor
113            .expect("allocation permit requires an action cursor")
114            .order];
115        let pending = self.pending_allocation.ok_or_else(|| {
116            DeviceAllocationError::Contract(invalid_resource(
117                "core pending allocation storage is unavailable",
118            ))
119        })?;
120        if pending.borrow().is_some() {
121            return Err(DeviceAllocationError::Contract(invalid_resource(
122                "core pending allocation storage is already occupied",
123            )));
124        }
125        let expected_descriptor = BufferDescriptor {
126            resource_id: reservation.resource_id.clone(),
127            size_bytes: reservation.size_bytes,
128            alignment_bytes: reservation.alignment_bytes,
129            usage: reservation.usage,
130            element_type: reservation.element_type,
131        };
132        pending.replace(Some(CoreOwnedAllocation {
133            resource_id: resource_id.clone(),
134            generation,
135            descriptor: expected_descriptor,
136            buffer: allocation,
137        }));
138        let descriptor = {
139            let pending = pending.borrow();
140            self.runtime.buffer_descriptor(
141                &pending
142                    .as_ref()
143                    .expect("allocation was installed before descriptor inspection")
144                    .buffer,
145            )
146        };
147        pending
148            .borrow_mut()
149            .as_mut()
150            .expect("allocation remains core-owned during descriptor inspection")
151            .descriptor = descriptor.clone();
152        validate_runtime_descriptor_for_admission(
153            self.runtime.descriptor(),
154            self.binding,
155            "allocation completion",
156        )
157        .map_err(DeviceAllocationError::Contract)?;
158        Ok(DeviceAllocationReceipt {
159            resource_id,
160            generation,
161            descriptor,
162            scope: PhantomData,
163        })
164    }
165}
166
167pub(super) struct AllocationSeal;
168
169#[must_use = "a device allocation permit must be consumed by DeviceRuntime::allocate"]
170pub struct DeviceAllocationPermit<'a> {
171    pub(super) identity: &'a ResourceTransactionIdentity,
172    pub(super) binding: &'a StaticProvisioningBinding,
173    pub(super) reservation: &'a ResourceReservation,
174    pub(super) request: &'a BufferRequest,
175    pub(super) seal: AllocationSeal,
176}
177
178impl<'a> DeviceAllocationPermit<'a> {
179    pub fn identity(&self) -> &ResourceTransactionIdentity {
180        self.identity
181    }
182
183    pub fn admission(&self) -> &StaticProvisioningBinding {
184        self.binding
185    }
186
187    pub fn reservation(&self) -> &ResourceReservation {
188        self.reservation
189    }
190
191    pub fn request(&self) -> &BufferRequest {
192        self.request
193    }
194
195    pub fn resource_id(&self) -> &ResourceId {
196        self.reservation.resource_id()
197    }
198
199    pub const fn generation(&self) -> u64 {
200        self.reservation.generation()
201    }
202
203    pub fn into_request(self) -> &'a BufferRequest {
204        let _ = self.seal;
205        self.request
206    }
207}
208
209#[derive(Debug)]
210pub enum DeviceAllocationError<E> {
211    Contract(VNextError),
212    Runtime(E),
213}
214
215impl<E> DeviceAllocationError<E> {
216    pub fn contract_error(&self) -> Option<&VNextError> {
217        match self {
218            Self::Contract(error) => Some(error),
219            Self::Runtime(_) => None,
220        }
221    }
222
223    pub fn runtime_error(&self) -> Option<&E> {
224        match self {
225            Self::Contract(_) => None,
226            Self::Runtime(error) => Some(error),
227        }
228    }
229}
230
231#[must_use = "an allocation receipt must be returned by the active commit call"]
232pub struct DeviceAllocationReceipt<'commit> {
233    resource_id: ResourceId,
234    generation: u64,
235    descriptor: BufferDescriptor,
236    scope: PhantomData<&'commit mut ()>,
237}
238
239impl DeviceAllocationReceipt<'_> {
240    pub fn resource_id(&self) -> &ResourceId {
241        &self.resource_id
242    }
243
244    pub const fn generation(&self) -> u64 {
245        self.generation
246    }
247
248    pub fn descriptor(&self) -> &BufferDescriptor {
249        &self.descriptor
250    }
251}
252
253pub(super) struct DriverCommitAcknowledgement {
254    resource_id: ResourceId,
255    generation: u64,
256    descriptor: BufferDescriptor,
257}
258
259impl DriverCommitAcknowledgement {
260    pub(super) fn from_receipt(receipt: &DeviceAllocationReceipt<'_>) -> Self {
261        Self {
262            resource_id: receipt.resource_id.clone(),
263            generation: receipt.generation,
264            descriptor: receipt.descriptor.clone(),
265        }
266    }
267
268    pub(super) fn matches<B>(&self, allocation: &CoreOwnedAllocation<B>) -> bool {
269        self.resource_id == allocation.resource_id
270            && self.generation == allocation.generation
271            && self.descriptor == allocation.descriptor
272    }
273}
274
275pub(super) struct CoreOwnedAllocation<B> {
276    pub(super) resource_id: ResourceId,
277    pub(super) generation: u64,
278    pub(super) descriptor: BufferDescriptor,
279    pub(super) buffer: B,
280}
281
282impl<B> CoreOwnedAllocation<B> {
283    pub(super) fn matches(&self, reservation: &ResourceReservation) -> bool {
284        self.resource_id == reservation.resource_id
285            && self.generation == reservation.generation
286            && reservation.matches_descriptor(&self.descriptor)
287    }
288}
289
290/// Borrowed view used to reconcile an invalid allocation. Core retains the
291/// actual buffer regardless of the driver's return value.
292pub struct ResourceCommitView<'a, B> {
293    pub(super) resource_id: &'a ResourceId,
294    pub(super) generation: u64,
295    pub(super) descriptor: &'a BufferDescriptor,
296    pub(super) buffer: &'a B,
297}
298
299impl<'a, B> ResourceCommitView<'a, B> {
300    pub fn resource_id(&self) -> &ResourceId {
301        self.resource_id
302    }
303
304    pub const fn generation(&self) -> u64 {
305        self.generation
306    }
307
308    pub fn descriptor(&self) -> &BufferDescriptor {
309        self.descriptor
310    }
311
312    pub fn buffer(&self) -> &B {
313        self.buffer
314    }
315}
316
317#[must_use = "owned quarantine buffers must be retained until backend cleanup"]
318pub struct ResourceOwnedBuffer<B> {
319    pub(super) order: usize,
320    pub(super) expected_resource_id: ResourceId,
321    pub(super) actual_resource_id: ResourceId,
322    pub(super) expected_generation: u64,
323    pub(super) actual_generation: u64,
324    pub(super) expected_descriptor: BufferDescriptor,
325    pub(super) actual_descriptor: BufferDescriptor,
326    pub(super) buffer: B,
327}
328
329impl<B> ResourceOwnedBuffer<B> {
330    pub fn resource_id(&self) -> &ResourceId {
331        &self.expected_resource_id
332    }
333
334    pub const fn generation(&self) -> u64 {
335        self.expected_generation
336    }
337
338    pub fn actual_resource_id(&self) -> &ResourceId {
339        &self.actual_resource_id
340    }
341
342    pub const fn actual_generation(&self) -> u64 {
343        self.actual_generation
344    }
345
346    pub fn expected_descriptor(&self) -> &BufferDescriptor {
347        &self.expected_descriptor
348    }
349
350    pub fn actual_descriptor(&self) -> &BufferDescriptor {
351        &self.actual_descriptor
352    }
353
354    pub fn buffer(&self) -> &B {
355        &self.buffer
356    }
357
358    pub(super) fn into_allocation(self) -> (usize, CoreOwnedAllocation<B>) {
359        (
360            self.order,
361            CoreOwnedAllocation {
362                resource_id: self.actual_resource_id,
363                generation: self.actual_generation,
364                descriptor: self.actual_descriptor,
365                buffer: self.buffer,
366            },
367        )
368    }
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372pub enum ResourceOwnershipReason {
373    Quarantine,
374    Abandon,
375}
376
377/// Ownership transferred out of core when normal cleanup cannot prove that
378/// buffers and their device-capacity claim are gone. Dropping this object is
379/// the durable owner's explicit cleanup point.
380#[must_use = "resource ownership must remain durable until cleanup completes"]
381pub struct ResourcePoolOwnership<R>
382where
383    R: DeviceRuntime,
384{
385    // Declaration order is the normal cleanup order. Device buffers must be
386    // gone before their capacity becomes reusable, and both must precede the
387    // backend runtime/context teardown.
388    pub(super) buffers: Vec<ResourceOwnedBuffer<R::Buffer>>,
389    pub(super) capacity_claim: Option<DeviceCapacityClaim>,
390    pub(super) pool_identity: ResourcePoolIdentity,
391    pub(super) reason: ResourceOwnershipReason,
392    pub(super) signal: Option<ResourceAbandonSignal>,
393    pub(super) runtime: Arc<R>,
394}
395
396impl<R> ResourcePoolOwnership<R>
397where
398    R: DeviceRuntime,
399{
400    pub fn runtime(&self) -> &R {
401        &self.runtime
402    }
403
404    pub fn pool_identity(&self) -> &ResourcePoolIdentity {
405        &self.pool_identity
406    }
407
408    pub const fn reason(&self) -> ResourceOwnershipReason {
409        self.reason
410    }
411
412    pub fn abandon_signal(&self) -> Option<&ResourceAbandonSignal> {
413        self.signal.as_ref()
414    }
415
416    pub fn buffers(&self) -> &[ResourceOwnedBuffer<R::Buffer>] {
417        &self.buffers
418    }
419
420    pub fn claimed_bytes(&self) -> u64 {
421        self.capacity_claim
422            .as_ref()
423            .map_or(0, DeviceCapacityClaim::bytes)
424    }
425
426    fn must_retain_on_drop(&self) -> bool {
427        std::thread::panicking()
428    }
429}
430
431impl<R> Drop for ResourcePoolOwnership<R>
432where
433    R: DeviceRuntime,
434{
435    fn drop(&mut self) {
436        if !self.must_retain_on_drop() {
437            return;
438        }
439
440        // A driver that drops unresolved ownership has violated the durable
441        // cleanup contract. Preserve memory safety by retaining device buffers
442        // and the capacity claim; the abandon signal and recovery registry made
443        // the leak observable before this last-resort path.
444        for buffer in std::mem::take(&mut self.buffers) {
445            std::mem::forget(buffer);
446        }
447        if let Some(claim) = self.capacity_claim.take() {
448            std::mem::forget(claim);
449        }
450        // Buffer and stream handles may borrow backend-owned device/context
451        // state without expressing that lifetime in their Rust types. Retain
452        // both owners as one unit; dropping either after intentionally
453        // retaining an in-flight handle would invalidate the safety fallback.
454        std::mem::forget(Arc::clone(&self.runtime));
455    }
456}
457
458#[must_use = "a failed ownership transfer must be returned to core"]
459pub struct ResourceOwnershipTransferFailure<R>
460where
461    R: DeviceRuntime,
462{
463    failure: ResourceDriverFailure,
464    ownership: ResourcePoolOwnership<R>,
465}
466
467impl<R> ResourceOwnershipTransferFailure<R>
468where
469    R: DeviceRuntime,
470{
471    pub fn new(failure: ResourceDriverFailure, ownership: ResourcePoolOwnership<R>) -> Self {
472        Self { failure, ownership }
473    }
474
475    pub fn failure(&self) -> &ResourceDriverFailure {
476        &self.failure
477    }
478
479    pub fn ownership(&self) -> &ResourcePoolOwnership<R> {
480        &self.ownership
481    }
482
483    pub(super) fn into_parts(self) -> (ResourceDriverFailure, ResourcePoolOwnership<R>) {
484        (self.failure, self.ownership)
485    }
486}
487
488/// Backend adapter for one resource at a time. Core owns action ordering,
489/// actual state, all buffers, receipts, and recovery progress. Methods must be
490/// idempotent for the full identity/action/resource/generation key.
491pub trait ResourceTransactionDriver: Send {
492    type Buffer: Send + Sync + 'static;
493    type Runtime: DeviceRuntime<Buffer = Self::Buffer>;
494
495    fn runtime(&self) -> &Arc<Self::Runtime>;
496
497    fn device_id(&self) -> &DeviceId;
498
499    fn device_runtime_implementation_fingerprint(&self) -> &str;
500
501    fn device_capacity_bytes(&self) -> u64;
502
503    fn reserve_resource(
504        &mut self,
505        context: &ResourceTransactionContext<'_, Self::Runtime>,
506        reservation: &ResourceReservation,
507    ) -> Result<(), ResourceDriverFailure>;
508
509    fn commit_resource<'commit>(
510        &mut self,
511        context: &'commit ResourceTransactionContext<'_, Self::Runtime>,
512        reservation: &ResourceReservation,
513    ) -> Result<DeviceAllocationReceipt<'commit>, ResourceDriverFailure>;
514
515    fn compensate_reserve_resource(
516        &mut self,
517        context: &ResourceTransactionContext<'_, Self::Runtime>,
518        reservation: &ResourceReservation,
519    ) -> Result<(), ResourceDriverFailure>;
520
521    fn compensate_commit_resource(
522        &mut self,
523        context: &ResourceTransactionContext<'_, Self::Runtime>,
524        reservation: &ResourceReservation,
525        buffer: &Self::Buffer,
526    ) -> Result<(), ResourceDriverFailure>;
527
528    fn rollback_resource(
529        &mut self,
530        context: &ResourceTransactionContext<'_, Self::Runtime>,
531        reservation: &ResourceReservation,
532    ) -> Result<(), ResourceDriverFailure>;
533
534    fn release_resource(
535        &mut self,
536        context: &ResourceTransactionContext<'_, Self::Runtime>,
537        reservation: &ResourceReservation,
538        buffer: &Self::Buffer,
539    ) -> Result<(), ResourceDriverFailure>;
540
541    fn reconcile_commit_outcome(
542        &mut self,
543        context: &ResourceTransactionContext<'_, Self::Runtime>,
544        expected: &ResourceReservation,
545        actual: ResourceCommitView<'_, Self::Buffer>,
546    ) -> Result<(), ResourceDriverFailure>;
547
548    fn quarantine_transaction(
549        &mut self,
550        context: &ResourceTransactionContext<'_, Self::Runtime>,
551        ownership: ResourcePoolOwnership<Self::Runtime>,
552    ) -> Result<(), ResourceOwnershipTransferFailure<Self::Runtime>>;
553
554    fn abandon_transaction(&mut self, ownership: ResourcePoolOwnership<Self::Runtime>);
555}