Skip to main content

ferrum_interfaces/vnext/resource/
request_state_hazard.rs

1use super::{
2    invalid_resource, AllocationLifetime, Arc, BTreeMap, Mutex, PlanNode, RequestAuthorityId,
3    ResourceId, Serialize, TensorAccess, VNextError,
4};
5use std::mem;
6use tokio::sync::watch;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum RequestStateHazardAccess {
11    Read,
12    Write,
13}
14
15impl RequestStateHazardAccess {
16    fn merge(self, other: Self) -> Self {
17        if self == Self::Write || other == Self::Write {
18            Self::Write
19        } else {
20            Self::Read
21        }
22    }
23
24    fn from_tensor_access(access: TensorAccess) -> Self {
25        match access {
26            TensorAccess::Read => Self::Read,
27            TensorAccess::Write | TensorAccess::ReadWrite => Self::Write,
28        }
29    }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33struct RequestStateHazardClaimSpec {
34    resource_id: ResourceId,
35    access: RequestStateHazardAccess,
36}
37
38/// Immutable plan-compiled projection from node state effects to physical
39/// Request-state resource closures. Runtime arbitration never infers hazards
40/// from a model family or from aggregate byte counts.
41pub(super) struct RequestStateHazardLayout {
42    node_claims: Vec<Arc<[RequestStateHazardClaimSpec]>>,
43    full_plan_claims: Arc<[RequestStateHazardClaimSpec]>,
44    resource_ids: Arc<[ResourceId]>,
45}
46
47enum SelectedRequestStateHazardClaims<'a> {
48    Borrowed(&'a [RequestStateHazardClaimSpec]),
49    Owned(Vec<RequestStateHazardClaimSpec>),
50}
51
52impl SelectedRequestStateHazardClaims<'_> {
53    fn as_slice(&self) -> &[RequestStateHazardClaimSpec] {
54        match self {
55            Self::Borrowed(claims) => claims,
56            Self::Owned(claims) => claims,
57        }
58    }
59}
60
61impl RequestStateHazardLayout {
62    fn compile(nodes: &[PlanNode]) -> Result<Self, VNextError> {
63        let mut full_plan = BTreeMap::<ResourceId, RequestStateHazardAccess>::new();
64        let mut node_claims = Vec::with_capacity(nodes.len());
65        for node in nodes {
66            let mut claims = BTreeMap::<ResourceId, RequestStateHazardAccess>::new();
67            for effect in node
68                .state_effects()
69                .iter()
70                .filter(|effect| effect.lifetime() == AllocationLifetime::Request)
71            {
72                let access = RequestStateHazardAccess::from_tensor_access(effect.access());
73                if effect.resource_ids().is_empty() {
74                    return Err(invalid_resource(format!(
75                        "request state `{}` has no physical hazard closure",
76                        effect.state_id()
77                    )));
78                }
79                for resource_id in effect.resource_ids() {
80                    claims
81                        .entry(resource_id.clone())
82                        .and_modify(|current| *current = current.merge(access))
83                        .or_insert(access);
84                    full_plan
85                        .entry(resource_id.clone())
86                        .and_modify(|current| *current = current.merge(access))
87                        .or_insert(access);
88                }
89            }
90            node_claims.push(Arc::from(
91                claims
92                    .into_iter()
93                    .map(|(resource_id, access)| RequestStateHazardClaimSpec {
94                        resource_id,
95                        access,
96                    })
97                    .collect::<Vec<_>>(),
98            ));
99        }
100        let full_plan_claims = full_plan
101            .iter()
102            .map(|(resource_id, access)| RequestStateHazardClaimSpec {
103                resource_id: resource_id.clone(),
104                access: *access,
105            })
106            .collect::<Vec<_>>();
107        let resource_ids = full_plan.keys().cloned().collect::<Vec<_>>();
108        Ok(Self {
109            node_claims,
110            full_plan_claims: Arc::from(full_plan_claims),
111            resource_ids: Arc::from(resource_ids),
112        })
113    }
114
115    fn is_empty(&self) -> bool {
116        self.resource_ids.is_empty()
117    }
118
119    fn selected(
120        &self,
121        node_indices: &[usize],
122    ) -> Result<SelectedRequestStateHazardClaims<'_>, VNextError> {
123        if node_indices.is_empty()
124            || node_indices.windows(2).any(|pair| pair[0] >= pair[1])
125            || node_indices
126                .last()
127                .is_some_and(|index| *index >= self.node_claims.len())
128        {
129            return Err(invalid_resource(
130                "request-state hazard scope must be non-empty, canonical, and plan-bound",
131            ));
132        }
133        if node_indices.len() == self.node_claims.len()
134            && node_indices.iter().copied().eq(0..self.node_claims.len())
135        {
136            return Ok(SelectedRequestStateHazardClaims::Borrowed(
137                &self.full_plan_claims,
138            ));
139        }
140        if node_indices.len() == 1 {
141            return Ok(SelectedRequestStateHazardClaims::Borrowed(
142                &self.node_claims[node_indices[0]],
143            ));
144        }
145        let mut merged = BTreeMap::<ResourceId, RequestStateHazardAccess>::new();
146        for &node_index in node_indices {
147            for claim in self.node_claims[node_index].iter() {
148                merged
149                    .entry(claim.resource_id.clone())
150                    .and_modify(|current| *current = current.merge(claim.access))
151                    .or_insert(claim.access);
152            }
153        }
154        Ok(SelectedRequestStateHazardClaims::Owned(
155            merged
156                .into_iter()
157                .map(|(resource_id, access)| RequestStateHazardClaimSpec {
158                    resource_id,
159                    access,
160                })
161                .collect(),
162        ))
163    }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
167struct RequestStateHazardKey {
168    request: RequestAuthorityId,
169    resource_id: ResourceId,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
173#[serde(rename_all = "snake_case")]
174pub enum RequestStateHazardPoisonCause {
175    FailedButQuiescent,
176    IndeterminateAfterDrain,
177    InFlightOwnerDropped,
178    CoordinatorInvariant,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
182pub struct RequestStateHazardPoison {
183    request: RequestAuthorityId,
184    resource_id: ResourceId,
185    cause: RequestStateHazardPoisonCause,
186    value_generation: u64,
187    availability_generation: u64,
188}
189
190impl RequestStateHazardPoison {
191    pub const fn request(&self) -> RequestAuthorityId {
192        self.request
193    }
194
195    pub fn resource_id(&self) -> &ResourceId {
196        &self.resource_id
197    }
198
199    pub const fn cause(&self) -> RequestStateHazardPoisonCause {
200        self.cause
201    }
202
203    pub const fn value_generation(&self) -> u64 {
204        self.value_generation
205    }
206
207    pub const fn availability_generation(&self) -> u64 {
208        self.availability_generation
209    }
210}
211
212struct RequestStateHazardCell {
213    readers: u32,
214    writer: bool,
215    waiting_writers: u32,
216    value_generation: u64,
217    availability_generation: u64,
218    poison: Option<RequestStateHazardPoisonCause>,
219}
220
221struct RequestStateHazardCoordinatorState {
222    cells: BTreeMap<RequestStateHazardKey, RequestStateHazardCell>,
223    change_epoch: u64,
224    globally_poisoned: bool,
225}
226
227/// One plan-local arbiter for every Request-state physical closure. A single
228/// mutex makes a mixed-request wave acquisition all-or-nothing; no Nth claim
229/// failure can leave an earlier request partially held.
230pub(super) struct RequestStateHazardCoordinator {
231    layout: RequestStateHazardLayout,
232    state: Mutex<RequestStateHazardCoordinatorState>,
233    changed: watch::Sender<u64>,
234}
235
236pub(super) struct RequestStateHazardParticipant<O> {
237    coordinator: Arc<RequestStateHazardCoordinator>,
238    request: RequestAuthorityId,
239    owner: O,
240}
241
242impl<O> RequestStateHazardParticipant<O> {
243    pub(super) fn new(
244        coordinator: Arc<RequestStateHazardCoordinator>,
245        request: RequestAuthorityId,
246        owner: O,
247    ) -> Self {
248        Self {
249            coordinator,
250            request,
251            owner,
252        }
253    }
254}
255
256impl RequestStateHazardCoordinator {
257    pub(super) fn compile(nodes: &[PlanNode]) -> Result<Arc<Self>, VNextError> {
258        let layout = RequestStateHazardLayout::compile(nodes)?;
259        let (changed, _) = watch::channel(1);
260        Ok(Arc::new(Self {
261            layout,
262            state: Mutex::new(RequestStateHazardCoordinatorState {
263                cells: BTreeMap::new(),
264                change_epoch: 1,
265                globally_poisoned: false,
266            }),
267            changed,
268        }))
269    }
270
271    pub(super) fn is_empty(&self) -> bool {
272        self.layout.is_empty()
273    }
274
275    pub(super) fn register_request(
276        self: &Arc<Self>,
277        request: RequestAuthorityId,
278    ) -> Result<Option<RequestStateHazardRegistration>, VNextError> {
279        if self.is_empty() {
280            return Ok(None);
281        }
282        let mut state = self
283            .state
284            .lock()
285            .map_err(|_| invalid_resource("request-state hazard coordinator is poisoned"))?;
286        if state.globally_poisoned {
287            return Err(invalid_resource(
288                "request-state hazard coordinator is fail-closed",
289            ));
290        }
291        let mut inserted = Vec::with_capacity(self.layout.resource_ids.len());
292        for resource_id in self.layout.resource_ids.iter() {
293            let key = RequestStateHazardKey {
294                request,
295                resource_id: resource_id.clone(),
296            };
297            if state.cells.contains_key(&key) {
298                for key in inserted {
299                    state.cells.remove(&key);
300                }
301                state.globally_poisoned = true;
302                return Err(invalid_resource(
303                    "request-state hazard registration reused a live request authority",
304                ));
305            }
306            state.cells.insert(
307                key.clone(),
308                RequestStateHazardCell {
309                    readers: 0,
310                    writer: false,
311                    waiting_writers: 0,
312                    value_generation: 0,
313                    availability_generation: 1,
314                    poison: None,
315                },
316            );
317            inserted.push(key);
318        }
319        Ok(Some(RequestStateHazardRegistration {
320            coordinator: Arc::clone(self),
321            request,
322            registered: true,
323        }))
324    }
325
326    fn unregister_request(&self, request: RequestAuthorityId) -> Result<(), VNextError> {
327        let mut state = self
328            .state
329            .lock()
330            .map_err(|_| invalid_resource("request-state hazard coordinator is poisoned"))?;
331        let keys = self
332            .layout
333            .resource_ids
334            .iter()
335            .map(|resource_id| RequestStateHazardKey {
336                request,
337                resource_id: resource_id.clone(),
338            })
339            .collect::<Vec<_>>();
340        let valid = keys.iter().all(|key| {
341            state
342                .cells
343                .get(key)
344                .is_some_and(|cell| cell.readers == 0 && !cell.writer && cell.waiting_writers == 0)
345        });
346        if !valid {
347            state.globally_poisoned = true;
348            return Err(invalid_resource(
349                "request-state hazard registration dropped with live claims or waiters",
350            ));
351        }
352        for key in keys {
353            state.cells.remove(&key);
354        }
355        Ok(())
356    }
357
358    pub(super) fn try_acquire<O>(
359        self: &Arc<Self>,
360        participants: &[RequestStateHazardParticipant<O>],
361        node_indices: &[usize],
362    ) -> Result<RequestStateHazardAcquireDecision<O>, VNextError>
363    where
364        O: Clone,
365    {
366        let selected = self.layout.selected(node_indices)?;
367        let claims = selected.as_slice();
368        if claims.is_empty() {
369            return Ok(RequestStateHazardAcquireDecision::Acquired(None));
370        }
371        let mut requests = BTreeMap::<RequestAuthorityId, (O, u32)>::new();
372        for participant in participants {
373            if !Arc::ptr_eq(&participant.coordinator, self) {
374                return Err(invalid_resource(
375                    "request-state hazard participant belongs to another plan coordinator",
376                ));
377            }
378            let request_authority = participant.request;
379            if let Some((_, sibling_count)) = requests.get_mut(&request_authority) {
380                *sibling_count = sibling_count
381                    .checked_add(1)
382                    .ok_or_else(|| invalid_resource("request-state sibling count exceeds u32"))?;
383            } else {
384                requests.insert(request_authority, (participant.owner.clone(), 1));
385            }
386        }
387        if requests.is_empty() {
388            return Err(invalid_resource(
389                "request-state hazard acquisition requires participants",
390            ));
391        }
392        if claims
393            .iter()
394            .any(|claim| claim.access == RequestStateHazardAccess::Write)
395        {
396            if let Some((request, (_, sibling_count))) =
397                requests.iter().find(|(_, (_, count))| *count > 1)
398            {
399                return Ok(RequestStateHazardAcquireDecision::SplitRequired(
400                    RequestStateHazardSplitRequired {
401                        request: *request,
402                        sibling_count: *sibling_count,
403                        resource_ids: claims
404                            .iter()
405                            .filter(|claim| claim.access == RequestStateHazardAccess::Write)
406                            .map(|claim| claim.resource_id.clone())
407                            .collect(),
408                    },
409                ));
410            }
411        }
412
413        let requested = requests
414            .keys()
415            .flat_map(|request| {
416                claims
417                    .iter()
418                    .map(move |claim| ActiveRequestStateHazardClaim {
419                        key: RequestStateHazardKey {
420                            request: *request,
421                            resource_id: claim.resource_id.clone(),
422                        },
423                        access: claim.access,
424                    })
425            })
426            .collect::<Vec<_>>();
427        let mut state = self
428            .state
429            .lock()
430            .map_err(|_| invalid_resource("request-state hazard coordinator is poisoned"))?;
431        if state.globally_poisoned {
432            return Err(invalid_resource(
433                "request-state hazard coordinator is fail-closed",
434            ));
435        }
436        let mut blockers = Vec::new();
437        for claim in &requested {
438            let cell = state.cells.get(&claim.key).ok_or_else(|| {
439                invalid_resource("request-state hazard claim has no live request registration")
440            })?;
441            if let Some(cause) = cell.poison {
442                return Ok(RequestStateHazardAcquireDecision::Poisoned(
443                    RequestStateHazardPoison {
444                        request: claim.key.request,
445                        resource_id: claim.key.resource_id.clone(),
446                        cause,
447                        value_generation: cell.value_generation,
448                        availability_generation: cell.availability_generation,
449                    },
450                ));
451            }
452            let conflict = match claim.access {
453                RequestStateHazardAccess::Read => cell.writer || cell.waiting_writers != 0,
454                RequestStateHazardAccess::Write => cell.writer || cell.readers != 0,
455            };
456            if conflict {
457                blockers.push(RequestStateHazardBlocker {
458                    request: claim.key.request,
459                    resource_id: claim.key.resource_id.clone(),
460                    requested_access: claim.access,
461                    active_readers: cell.readers,
462                    active_writer: cell.writer,
463                    waiting_writers: cell.waiting_writers,
464                    availability_generation: cell.availability_generation,
465                });
466            }
467        }
468        if !blockers.is_empty() {
469            return Ok(RequestStateHazardAcquireDecision::Deferred(
470                RequestStateHazardDeferral {
471                    coordinator: Arc::clone(self),
472                    observed_change_epoch: state.change_epoch,
473                    blockers,
474                },
475            ));
476        }
477        if requested.iter().any(|claim| {
478            claim.access == RequestStateHazardAccess::Read
479                && state
480                    .cells
481                    .get(&claim.key)
482                    .is_some_and(|cell| cell.readers == u32::MAX)
483        }) {
484            state.globally_poisoned = true;
485            return Err(invalid_resource("request-state reader count is exhausted"));
486        }
487        for claim in &requested {
488            let cell = state
489                .cells
490                .get_mut(&claim.key)
491                .expect("validated request-state hazard cell remains registered");
492            match claim.access {
493                RequestStateHazardAccess::Read => {
494                    cell.readers += 1;
495                }
496                RequestStateHazardAccess::Write => cell.writer = true,
497            }
498        }
499        drop(state);
500        Ok(RequestStateHazardAcquireDecision::Acquired(Some(
501            RequestStateHazardPermit {
502                coordinator: Arc::clone(self),
503                requests: Some(requests.into_values().map(|(request, _)| request).collect()),
504                claims: requested,
505                phase: RequestStateHazardPermitPhase::Prepared,
506                finished: false,
507            },
508        )))
509    }
510
511    fn release(
512        &self,
513        claims: &[ActiveRequestStateHazardClaim],
514        disposition: RequestStateHazardReleaseDisposition,
515    ) -> Result<(), VNextError> {
516        let mut state = self
517            .state
518            .lock()
519            .map_err(|_| invalid_resource("request-state hazard coordinator is poisoned"))?;
520        let valid = claims.iter().all(|claim| {
521            state
522                .cells
523                .get(&claim.key)
524                .is_some_and(|cell| match claim.access {
525                    RequestStateHazardAccess::Read => cell.readers > 0 && !cell.writer,
526                    RequestStateHazardAccess::Write => cell.writer && cell.readers == 0,
527                })
528        });
529        if !valid {
530            state.globally_poisoned = true;
531            return Err(invalid_resource(
532                "request-state hazard release does not own its exact active claims",
533            ));
534        }
535        let next_change_epoch = state
536            .change_epoch
537            .checked_add(1)
538            .ok_or_else(|| invalid_resource("request-state hazard change epoch is exhausted"))?;
539        if claims.iter().any(|claim| {
540            state.cells.get(&claim.key).is_some_and(|cell| {
541                cell.availability_generation == u64::MAX
542                    || (claim.access == RequestStateHazardAccess::Write
543                        && disposition == RequestStateHazardReleaseDisposition::Succeeded
544                        && cell.value_generation == u64::MAX)
545            })
546        }) {
547            state.globally_poisoned = true;
548            return Err(invalid_resource(
549                "request-state hazard generation is exhausted",
550            ));
551        }
552        for claim in claims {
553            let cell = state
554                .cells
555                .get_mut(&claim.key)
556                .expect("validated request-state hazard cell remains registered");
557            match claim.access {
558                RequestStateHazardAccess::Read => cell.readers -= 1,
559                RequestStateHazardAccess::Write => {
560                    cell.writer = false;
561                    match disposition {
562                        RequestStateHazardReleaseDisposition::Succeeded => {
563                            cell.value_generation += 1;
564                        }
565                        RequestStateHazardReleaseDisposition::PreparedAbandoned => {}
566                        RequestStateHazardReleaseDisposition::FailedButQuiescent => {
567                            cell.poison = Some(RequestStateHazardPoisonCause::FailedButQuiescent);
568                        }
569                        RequestStateHazardReleaseDisposition::IndeterminateAfterDrain => {
570                            cell.poison =
571                                Some(RequestStateHazardPoisonCause::IndeterminateAfterDrain);
572                        }
573                        RequestStateHazardReleaseDisposition::InFlightOwnerDropped => {
574                            cell.poison = Some(RequestStateHazardPoisonCause::InFlightOwnerDropped);
575                        }
576                    }
577                }
578            }
579            cell.availability_generation += 1;
580        }
581        state.change_epoch = next_change_epoch;
582        self.changed.send_replace(next_change_epoch);
583        Ok(())
584    }
585
586    fn recheck(&self, blockers: &[RequestStateHazardBlocker]) -> Result<bool, VNextError> {
587        let state = self
588            .state
589            .lock()
590            .map_err(|_| invalid_resource("request-state hazard coordinator is poisoned"))?;
591        if state.globally_poisoned {
592            return Err(invalid_resource(
593                "request-state hazard coordinator is fail-closed",
594            ));
595        }
596        for blocker in blockers {
597            let key = RequestStateHazardKey {
598                request: blocker.request,
599                resource_id: blocker.resource_id.clone(),
600            };
601            let cell = state.cells.get(&key).ok_or_else(|| {
602                invalid_resource("request-state hazard waiter lost its request registration")
603            })?;
604            if let Some(cause) = cell.poison {
605                return Err(invalid_resource(format!(
606                    "request-state resource `{}` is poisoned by {cause:?}",
607                    blocker.resource_id
608                )));
609            }
610            let conflict = match blocker.requested_access {
611                RequestStateHazardAccess::Read => cell.writer || cell.waiting_writers != 0,
612                RequestStateHazardAccess::Write => cell.writer || cell.readers != 0,
613            };
614            if conflict && cell.availability_generation == blocker.availability_generation {
615                return Ok(false);
616            }
617            if conflict {
618                return Ok(false);
619            }
620        }
621        Ok(true)
622    }
623
624    fn register_writer_waiters(
625        &self,
626        blockers: &[RequestStateHazardBlocker],
627    ) -> Result<(), VNextError> {
628        let mut state = self
629            .state
630            .lock()
631            .map_err(|_| invalid_resource("request-state hazard coordinator is poisoned"))?;
632        let writer_keys = blockers
633            .iter()
634            .filter(|blocker| blocker.requested_access == RequestStateHazardAccess::Write)
635            .map(|blocker| RequestStateHazardKey {
636                request: blocker.request,
637                resource_id: blocker.resource_id.clone(),
638            })
639            .collect::<Vec<_>>();
640        if writer_keys.iter().any(|key| {
641            state
642                .cells
643                .get(key)
644                .is_none_or(|cell| cell.waiting_writers == u32::MAX)
645        }) {
646            state.globally_poisoned = true;
647            return Err(invalid_resource(
648                "request-state writer waiter count is exhausted",
649            ));
650        }
651        for key in writer_keys {
652            state
653                .cells
654                .get_mut(&key)
655                .expect("validated request-state writer waiter remains registered")
656                .waiting_writers += 1;
657        }
658        Ok(())
659    }
660
661    fn unregister_writer_waiters(&self, blockers: &[RequestStateHazardBlocker]) {
662        let Ok(mut state) = self.state.lock() else {
663            return;
664        };
665        let mut changed = false;
666        for blocker in blockers
667            .iter()
668            .filter(|blocker| blocker.requested_access == RequestStateHazardAccess::Write)
669        {
670            let key = RequestStateHazardKey {
671                request: blocker.request,
672                resource_id: blocker.resource_id.clone(),
673            };
674            let Some(cell) = state.cells.get_mut(&key) else {
675                state.globally_poisoned = true;
676                continue;
677            };
678            if cell.waiting_writers == 0 {
679                state.globally_poisoned = true;
680            } else {
681                cell.waiting_writers -= 1;
682                changed = true;
683            }
684        }
685        if changed {
686            let Some(next_change_epoch) = state.change_epoch.checked_add(1) else {
687                state.globally_poisoned = true;
688                return;
689            };
690            state.change_epoch = next_change_epoch;
691            self.changed.send_replace(next_change_epoch);
692        }
693    }
694}
695
696pub(super) struct RequestStateHazardRegistration {
697    coordinator: Arc<RequestStateHazardCoordinator>,
698    request: RequestAuthorityId,
699    registered: bool,
700}
701
702impl Drop for RequestStateHazardRegistration {
703    fn drop(&mut self) {
704        if self.registered {
705            let _ = self.coordinator.unregister_request(self.request);
706            self.registered = false;
707        }
708    }
709}
710
711#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
712pub struct RequestStateHazardBlocker {
713    request: RequestAuthorityId,
714    resource_id: ResourceId,
715    requested_access: RequestStateHazardAccess,
716    active_readers: u32,
717    active_writer: bool,
718    waiting_writers: u32,
719    availability_generation: u64,
720}
721
722impl RequestStateHazardBlocker {
723    pub const fn request(&self) -> RequestAuthorityId {
724        self.request
725    }
726
727    pub fn resource_id(&self) -> &ResourceId {
728        &self.resource_id
729    }
730
731    pub const fn requested_access(&self) -> RequestStateHazardAccess {
732        self.requested_access
733    }
734
735    pub const fn active_readers(&self) -> u32 {
736        self.active_readers
737    }
738
739    pub const fn active_writer(&self) -> bool {
740        self.active_writer
741    }
742
743    pub const fn waiting_writers(&self) -> u32 {
744        self.waiting_writers
745    }
746
747    pub const fn availability_generation(&self) -> u64 {
748        self.availability_generation
749    }
750}
751
752#[derive(Clone, Serialize)]
753pub struct RequestStateHazardDeferral {
754    #[serde(skip)]
755    coordinator: Arc<RequestStateHazardCoordinator>,
756    observed_change_epoch: u64,
757    blockers: Vec<RequestStateHazardBlocker>,
758}
759
760impl std::fmt::Debug for RequestStateHazardDeferral {
761    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
762        formatter
763            .debug_struct("RequestStateHazardDeferral")
764            .field("observed_change_epoch", &self.observed_change_epoch)
765            .field("blockers", &self.blockers)
766            .finish()
767    }
768}
769
770impl RequestStateHazardDeferral {
771    pub const fn observed_change_epoch(&self) -> u64 {
772        self.observed_change_epoch
773    }
774
775    pub fn blockers(&self) -> &[RequestStateHazardBlocker] {
776        &self.blockers
777    }
778
779    pub fn register_waiter(&self) -> Result<RequestStateHazardWaitRegistration, VNextError> {
780        let receiver = self.coordinator.changed.subscribe();
781        self.coordinator.register_writer_waiters(&self.blockers)?;
782        let ready = match self.coordinator.recheck(&self.blockers) {
783            Ok(ready) => ready,
784            Err(error) => {
785                self.coordinator.unregister_writer_waiters(&self.blockers);
786                return Err(error);
787            }
788        };
789        Ok(RequestStateHazardWaitRegistration {
790            coordinator: Arc::clone(&self.coordinator),
791            blockers: self.blockers.clone(),
792            receiver,
793            ready,
794            registered: true,
795        })
796    }
797}
798
799#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
800pub struct RequestStateHazardSplitRequired {
801    request: RequestAuthorityId,
802    sibling_count: u32,
803    resource_ids: Vec<ResourceId>,
804}
805
806impl RequestStateHazardSplitRequired {
807    pub const fn request(&self) -> RequestAuthorityId {
808        self.request
809    }
810
811    pub const fn sibling_count(&self) -> u32 {
812        self.sibling_count
813    }
814
815    pub fn resource_ids(&self) -> &[ResourceId] {
816        &self.resource_ids
817    }
818}
819
820#[must_use = "a request-state hazard waiter must be awaited or dropped"]
821pub struct RequestStateHazardWaitRegistration {
822    coordinator: Arc<RequestStateHazardCoordinator>,
823    blockers: Vec<RequestStateHazardBlocker>,
824    receiver: watch::Receiver<u64>,
825    ready: bool,
826    registered: bool,
827}
828
829impl RequestStateHazardWaitRegistration {
830    pub async fn wait_for_change(mut self) -> Result<u64, VNextError> {
831        loop {
832            if self.ready || self.coordinator.recheck(&self.blockers)? {
833                return Ok(*self.receiver.borrow_and_update());
834            }
835            self.receiver.changed().await.map_err(|_| {
836                invalid_resource("request-state hazard coordinator closed while waiting")
837            })?;
838        }
839    }
840}
841
842impl Drop for RequestStateHazardWaitRegistration {
843    fn drop(&mut self) {
844        if self.registered {
845            self.coordinator.unregister_writer_waiters(&self.blockers);
846            self.registered = false;
847        }
848    }
849}
850
851#[derive(Debug, Clone, Copy, PartialEq, Eq)]
852pub(crate) enum RequestStateHazardTerminalDisposition {
853    Succeeded,
854    FailedButQuiescent,
855    IndeterminateAfterDrain,
856}
857
858#[derive(Debug, Clone, Copy, PartialEq, Eq)]
859enum RequestStateHazardReleaseDisposition {
860    PreparedAbandoned,
861    Succeeded,
862    FailedButQuiescent,
863    IndeterminateAfterDrain,
864    InFlightOwnerDropped,
865}
866
867#[derive(Debug, Clone, Copy, PartialEq, Eq)]
868enum RequestStateHazardPermitPhase {
869    Prepared,
870    InFlight,
871    Indeterminate,
872}
873
874struct ActiveRequestStateHazardClaim {
875    key: RequestStateHazardKey,
876    access: RequestStateHazardAccess,
877}
878
879#[must_use = "request-state hazards must remain owned through the device fence"]
880pub struct RequestStateHazardPermit<O> {
881    coordinator: Arc<RequestStateHazardCoordinator>,
882    requests: Option<Vec<O>>,
883    claims: Vec<ActiveRequestStateHazardClaim>,
884    phase: RequestStateHazardPermitPhase,
885    finished: bool,
886}
887
888impl<O> RequestStateHazardPermit<O> {
889    pub fn claim_count(&self) -> usize {
890        self.claims.len()
891    }
892
893    pub(crate) fn mark_submission_fence_installed(&mut self) -> Result<(), VNextError> {
894        if self.finished || self.phase != RequestStateHazardPermitPhase::Prepared {
895            return Err(invalid_resource(
896                "request-state hazards cannot install a second submission fence",
897            ));
898        }
899        self.phase = RequestStateHazardPermitPhase::InFlight;
900        Ok(())
901    }
902
903    pub(crate) fn mark_submission_indeterminate(&mut self) {
904        if !self.finished {
905            self.phase = RequestStateHazardPermitPhase::Indeterminate;
906        }
907    }
908
909    pub(crate) fn finish(
910        &mut self,
911        disposition: RequestStateHazardTerminalDisposition,
912    ) -> Result<(), VNextError> {
913        if self.finished || self.phase == RequestStateHazardPermitPhase::Prepared {
914            return Err(invalid_resource(
915                "request-state hazard terminalization requires one installed or indeterminate submission",
916            ));
917        }
918        let disposition = match disposition {
919            RequestStateHazardTerminalDisposition::Succeeded => {
920                RequestStateHazardReleaseDisposition::Succeeded
921            }
922            RequestStateHazardTerminalDisposition::FailedButQuiescent => {
923                RequestStateHazardReleaseDisposition::FailedButQuiescent
924            }
925            RequestStateHazardTerminalDisposition::IndeterminateAfterDrain => {
926                RequestStateHazardReleaseDisposition::IndeterminateAfterDrain
927            }
928        };
929        self.release(disposition)
930    }
931
932    fn release(
933        &mut self,
934        disposition: RequestStateHazardReleaseDisposition,
935    ) -> Result<(), VNextError> {
936        self.coordinator.release(&self.claims, disposition)?;
937        self.finished = true;
938        self.claims.clear();
939        drop(self.requests.take());
940        Ok(())
941    }
942}
943
944impl<O> Drop for RequestStateHazardPermit<O> {
945    fn drop(&mut self) {
946        if self.finished {
947            return;
948        }
949        let disposition = match self.phase {
950            RequestStateHazardPermitPhase::Prepared => {
951                RequestStateHazardReleaseDisposition::PreparedAbandoned
952            }
953            RequestStateHazardPermitPhase::InFlight
954            | RequestStateHazardPermitPhase::Indeterminate => {
955                RequestStateHazardReleaseDisposition::InFlightOwnerDropped
956            }
957        };
958        if self.release(disposition).is_err() {
959            // Preserve every parent request backing rather than allowing a
960            // possibly in-flight device access to observe reused storage.
961            mem::forget(self.requests.take());
962        }
963    }
964}
965
966pub(super) enum RequestStateHazardAcquireDecision<O> {
967    Acquired(Option<RequestStateHazardPermit<O>>),
968    Deferred(RequestStateHazardDeferral),
969    SplitRequired(RequestStateHazardSplitRequired),
970    Poisoned(RequestStateHazardPoison),
971}