Skip to main content

ax_task/sched/system/cpu/remote/
scheduler.rs

1use core::sync::atomic::fence;
2
3use super::*;
4
5const REQUEST_PREEMPT: u64 = 1 << 0;
6const REQUEST_OWNER_WORK: u64 = 1 << 1;
7const REQUEST_PREEMPT_LAZY: u64 = 1 << 2;
8const REQUEST_REASON_MASK: u64 = REQUEST_PREEMPT | REQUEST_PREEMPT_LAZY | REQUEST_OWNER_WORK;
9const REQUEST_IDLE_POLLING: u64 = 1 << 3;
10const REQUEST_PARK_PREEMPT_DEFERRED: u64 = 1 << 4;
11const REQUEST_PARK_PREEMPT_LAZY_DEFERRED: u64 = 1 << 5;
12const DEFERRED_SCHEDULER_WORK_OFFLINE_INVARIANT: u32 = 0x4453_574f;
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub(super) enum SchedulerRequestDelivery {
16    /// The owner is in the idle polling protocol and will observe the sticky
17    /// work bit before committing to sleep.
18    PollingOwner,
19    /// The runtime must notify the shared physical IPI delivery edge.
20    ///
21    /// The runtime transports only a coalescible edge. Logical ownership
22    /// remains in the sticky request bits and the owner inbox, matching
23    /// Linux's split between `TIF_NEED_RESCHED`/`wake_list` and the IPI.
24    DoorbellRequired,
25}
26
27/// Linux PREEMPT_RT's two reschedule classes.
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub(crate) enum RescheduleKind {
30    /// `TIF_NEED_RESCHED`: consumed by preempt-enable and IRQ return.
31    Immediate,
32    /// `TIF_NEED_RESCHED_LAZY`: consumed at an explicit scheduling point,
33    /// return to userspace, or after the periodic tick promotes it.
34    Lazy,
35}
36
37impl RescheduleKind {
38    const fn request_bit(self) -> u64 {
39        match self {
40            Self::Immediate => REQUEST_PREEMPT,
41            Self::Lazy => REQUEST_PREEMPT_LAZY,
42        }
43    }
44}
45
46/// Which logical preemption classes one scheduler entry may consume.
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub(crate) enum SchedulerRequestScope {
49    /// A kernel preempt-enable or IRQ-return safe point. Lazy Fair requests
50    /// remain pending unless an ordinary request makes `__schedule()` run.
51    Immediate,
52    /// An explicit schedule/block/yield or return-to-userspace safe point.
53    All,
54}
55
56impl SchedulerRequestScope {
57    const fn claim_mask(self) -> u64 {
58        match self {
59            Self::Immediate => REQUEST_PREEMPT | REQUEST_OWNER_WORK,
60            Self::All => REQUEST_REASON_MASK,
61        }
62    }
63}
64
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub(super) struct SchedulerRequestPublication {
67    delivery: SchedulerRequestDelivery,
68}
69
70impl SchedulerRequestPublication {}
71
72#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
73pub(crate) struct SchedulerRequestClaim {
74    immediate_preempt: bool,
75    lazy_preempt: bool,
76    owner_work: bool,
77}
78
79impl SchedulerRequestClaim {
80    pub(crate) const fn immediate_preempt_requested(self) -> bool {
81        self.immediate_preempt
82    }
83
84    pub(crate) const fn lazy_preempt_requested(self) -> bool {
85        self.lazy_preempt
86    }
87
88    pub(crate) const fn preemption_requested(self) -> bool {
89        self.immediate_preempt || self.lazy_preempt
90    }
91
92    pub(crate) const fn owner_work_requested(self) -> bool {
93        self.owner_work
94    }
95
96    pub(crate) const fn merge(self, other: Self) -> Self {
97        Self {
98            immediate_preempt: self.immediate_preempt || other.immediate_preempt,
99            lazy_preempt: self.lazy_preempt || other.lazy_preempt,
100            owner_work: self.owner_work || other.owner_work,
101        }
102    }
103}
104
105#[derive(Debug)]
106pub(super) struct SchedulerRequestState {
107    request: AtomicU64,
108}
109
110impl SchedulerRequestState {
111    pub(super) const fn new() -> Self {
112        Self {
113            request: AtomicU64::new(0),
114        }
115    }
116
117    fn publish(&self, reason: u64) -> Option<u64> {
118        debug_assert_ne!(reason & REQUEST_REASON_MASK, 0);
119        let mut observed = self.request.load(Ordering::Acquire);
120        loop {
121            // Linux `__resched_curr()` drops a lazy request while an ordinary
122            // need-resched is live: the ordinary scheduling pass covers the
123            // lazy reason, so no lazy residue may survive behind it. Retrying
124            // the CAS is essential: if the ordinary request was consumed in
125            // the meantime, the new lazy reason must be published instead.
126            let publish = if reason & REQUEST_PREEMPT_LAZY != 0
127                && reason & REQUEST_PREEMPT == 0
128                && observed & REQUEST_PREEMPT != 0
129            {
130                reason & !REQUEST_PREEMPT_LAZY
131            } else {
132                reason
133            };
134            if publish & REQUEST_REASON_MASK == 0 || observed & publish == publish {
135                return None;
136            }
137            match self.request.compare_exchange_weak(
138                observed,
139                observed | publish,
140                Ordering::AcqRel,
141                Ordering::Acquire,
142            ) {
143                Ok(previous) => return Some(previous),
144                Err(updated) => observed = updated,
145            }
146        }
147    }
148
149    fn publish_remote(&self, reason: u64) -> Option<u64> {
150        let published = self.publish(reason);
151        if reason & (REQUEST_PREEMPT | REQUEST_OWNER_WORK) == 0 {
152            return published;
153        }
154
155        // Linux may suppress a repeated reschedule IPI because need-resched
156        // belongs to the exact rq current task until that task reaches a safe
157        // point. This state is CPU-global, so the sticky bit can outlive the
158        // physical edge which first transported it. Preserve the logical bit
159        // coalescing while still offering each fresh remote scheduling
160        // decision to DeliveryEdge; an armed edge coalesces it, while a
161        // claimed edge sends again.
162        published.or_else(|| Some(self.request.load(Ordering::Acquire)))
163    }
164
165    fn publish_rq_delivery(&self, reason: u64) -> Option<u64> {
166        // Unlike Linux's task-owned TIF_NEED_RESCHED, this CPU-global bit can
167        // survive the physical edge that first carried it. Each fresh rq
168        // decision must therefore retry delivery for immediate or owner work.
169        self.publish_remote(reason)
170    }
171
172    fn claim(&self, scope: SchedulerRequestScope) -> SchedulerRequestClaim {
173        let request = self
174            .request
175            .fetch_and(!scope.claim_mask(), Ordering::AcqRel);
176        SchedulerRequestClaim {
177            immediate_preempt: request & REQUEST_PREEMPT != 0,
178            lazy_preempt: request & REQUEST_PREEMPT_LAZY != 0
179                && scope == SchedulerRequestScope::All,
180            owner_work: request & REQUEST_OWNER_WORK != 0,
181        }
182    }
183
184    fn promote_lazy(&self) -> bool {
185        let mut observed = self.request.load(Ordering::Acquire);
186        loop {
187            if observed & REQUEST_PREEMPT_LAZY == 0 {
188                return false;
189            }
190            // The tick turns the observed lazy reason into an ordinary one.
191            // Keeping both bits would let the ordinary scheduling pass clear
192            // only its own class and later consume the same reason again.
193            let promoted = (observed | REQUEST_PREEMPT) & !REQUEST_PREEMPT_LAZY;
194            match self.request.compare_exchange_weak(
195                observed,
196                promoted,
197                Ordering::AcqRel,
198                Ordering::Acquire,
199            ) {
200                Ok(_) => return true,
201                Err(updated) => observed = updated,
202            }
203        }
204    }
205
206    fn defer_park_preemption(&self, request: SchedulerRequestClaim) {
207        let mut deferred = 0;
208        if request.immediate_preempt_requested() {
209            deferred |= REQUEST_PARK_PREEMPT_DEFERRED;
210        }
211        if request.lazy_preempt_requested() {
212            deferred |= REQUEST_PARK_PREEMPT_LAZY_DEFERRED;
213        }
214        if deferred != 0 {
215            self.request.fetch_or(deferred, Ordering::Release);
216        }
217    }
218
219    fn finish_park_preemption(&self, resume_running: bool) {
220        let deferred = self.request.fetch_and(
221            !(REQUEST_PARK_PREEMPT_DEFERRED | REQUEST_PARK_PREEMPT_LAZY_DEFERRED),
222            Ordering::AcqRel,
223        );
224        if !resume_running {
225            return;
226        }
227        if deferred & REQUEST_PARK_PREEMPT_DEFERRED != 0 {
228            let _ = self.publish(REQUEST_PREEMPT);
229        }
230        if deferred & REQUEST_PARK_PREEMPT_LAZY_DEFERRED != 0 {
231            let _ = self.publish(REQUEST_PREEMPT_LAZY);
232        }
233    }
234
235    fn restore_claimed_park_preemption(&self, request: SchedulerRequestClaim) {
236        self.defer_park_preemption(request);
237        self.finish_park_preemption(true);
238    }
239}
240
241impl CpuRemote {
242    pub(crate) fn is_scheduler_ready(&self) -> bool {
243        // CPU online publication is ordered after bootstrap/current and idle
244        // installation. Do not mirror `rq->curr` in an atomic readiness bit:
245        // lifecycle plus the immutable idle identity are the stable facts
246        // remote placement needs here.
247        self.is_online() && self.idle_thread().is_some()
248    }
249
250    /// Publishes a sticky owner-CPU reschedule request.
251    pub(crate) fn request_reschedule(&self, kind: RescheduleKind) {
252        let Some(_publication) = self.begin_publication() else {
253            return;
254        };
255        let _ = self.request_reschedule_owned(kind);
256    }
257
258    fn request_reschedule_owned(
259        &self,
260        kind: RescheduleKind,
261    ) -> Option<SchedulerRequestPublication> {
262        self.publish_scheduler_request_owned(kind.request_bit())
263    }
264
265    fn publish_scheduler_reasons_owned(
266        &self,
267        reschedule: Option<RescheduleKind>,
268        owner_work: bool,
269    ) {
270        let mut reasons = reschedule.map_or(0, RescheduleKind::request_bit);
271        if owner_work {
272            reasons |= REQUEST_OWNER_WORK;
273        }
274        let publication = self
275            .scheduler_request
276            .publish_remote(reasons)
277            .map(Self::scheduler_request_publication);
278        if owner_work || reschedule == Some(RescheduleKind::Immediate) {
279            self.deliver_scheduler_work_owned(
280                publication.expect("immediate remote scheduler work must retain a publication"),
281            );
282        }
283    }
284
285    /// Publishes scheduler reasons selected while the target runqueue is locked.
286    ///
287    /// This is the direct equivalent of Linux `resched_curr(rq)`: the producer
288    /// CPU is already pinned, target placement is serialized by `p->pi_lock`,
289    /// and the target rq remains locked until the caller commits the enqueue.
290    /// Local work therefore updates the architecture preemption word directly;
291    /// only a remote target needs the scheduler doorbell.
292    pub(crate) fn publish_rq_scheduler_reasons(
293        &self,
294        reschedule: Option<RescheduleKind>,
295        owner_work: bool,
296        producer: CpuId,
297        irq_owner: &IrqOwner<'_>,
298    ) {
299        if reschedule.is_none() && !owner_work {
300            return;
301        }
302        // The caller already owns the task-scheduler IRQ-save guard and keeps
303        // it live while the target rq transaction commits. Linux publishes
304        // `TIF_NEED_RESCHED` from that same `p->pi_lock`/rq critical section;
305        // opening another runtime IRQ guard here only increments the nested
306        // depth and rechecks the same CPU owner.
307        let _ = irq_owner;
308        let mut reasons = reschedule.map_or(0, RescheduleKind::request_bit);
309        if owner_work {
310            reasons |= REQUEST_OWNER_WORK;
311        }
312        if let Some(publication) = self
313            .scheduler_request
314            .publish_rq_delivery(reasons)
315            .map(Self::scheduler_request_publication)
316            && (owner_work || reschedule == Some(RescheduleKind::Immediate))
317        {
318            if publication.delivery == SchedulerRequestDelivery::PollingOwner {
319                return;
320            }
321            if producer == self.owner {
322                let _self_serviced = task_runtime::publish_local_scheduler_work();
323            } else {
324                self.ring_scheduler_doorbell();
325            }
326        }
327    }
328
329    /// Publishes a remote preemption after the runqueue transaction is visible.
330    ///
331    /// Like Linux `__resched_curr()`, only an ordinary request rings a remote
332    /// reschedule IPI. A lazy request remains a logical task flag; idle
333    /// preemption is classified as ordinary while the target rq is locked.
334    pub(crate) fn request_remote_reschedule(&self, kind: RescheduleKind) {
335        let Some(_publication) = self.begin_owner_delivery() else {
336            return;
337        };
338        let _irq = IrqScope::enter();
339        self.publish_scheduler_reasons_owned(Some(kind), false);
340    }
341
342    /// Publishes coupled preemption and owner-work reasons before ringing one
343    /// physical scheduler doorbell.
344    ///
345    /// One rq transaction may make both facts true. They share transport but
346    /// remain separate sticky bits, matching Linux's rule that scheduler state
347    /// and deferred work are visible before the IPI.
348    pub(crate) fn request_remote_reschedule_with_scheduler_work(&self, kind: RescheduleKind) {
349        let Some(_publication) = self.begin_owner_delivery() else {
350            return;
351        };
352        let _irq = IrqScope::enter();
353        self.publish_scheduler_reasons_owned(Some(kind), true);
354    }
355
356    pub(crate) fn request_scheduler_work(&self) {
357        let _delivered = self.request_scheduler_work_delivery();
358    }
359
360    fn request_scheduler_work_delivery(&self) -> bool {
361        let Some(_publication) = self.begin_owner_delivery() else {
362            return false;
363        };
364        let _irq = IrqScope::enter();
365        self.request_scheduler_work_owned()
366            .is_none_or(|publication| self.deliver_scheduler_work_owned(publication))
367    }
368
369    pub(super) fn request_scheduler_work_owned(&self) -> Option<SchedulerRequestPublication> {
370        self.publish_scheduler_request_owned(REQUEST_OWNER_WORK)
371    }
372
373    /// Publishes scheduler state for a fresh owner-inbox head.
374    ///
375    /// Like Linux `llist_add()`, the empty-to-nonempty inbox transition itself
376    /// owns a physical notification attempt. It must therefore return a
377    /// publication even when the sticky owner-work bit was already set: that
378    /// older bit may belong to an IPI edge the target has already claimed.
379    pub(super) fn publish_owner_inbox_head_owned(&self) -> SchedulerRequestPublication {
380        let previous = self
381            .scheduler_request
382            .request
383            .fetch_or(REQUEST_OWNER_WORK, Ordering::AcqRel);
384        Self::scheduler_request_publication(previous)
385    }
386
387    fn publish_scheduler_request_owned(&self, reason: u64) -> Option<SchedulerRequestPublication> {
388        self.scheduler_request
389            .publish(reason)
390            .map(Self::scheduler_request_publication)
391    }
392
393    fn scheduler_request_publication(previous: u64) -> SchedulerRequestPublication {
394        let delivery = if previous & REQUEST_IDLE_POLLING != 0 {
395            SchedulerRequestDelivery::PollingOwner
396        } else {
397            SchedulerRequestDelivery::DoorbellRequired
398        };
399        SchedulerRequestPublication { delivery }
400    }
401
402    pub(crate) fn kick_scheduler_work(&self) -> bool {
403        let Some(_publication) = self.begin_owner_delivery() else {
404            return false;
405        };
406        let _irq = IrqScope::enter();
407        self.kick_scheduler_work_owned()
408    }
409
410    pub(super) fn kick_scheduler_work_owned(&self) -> bool {
411        self.request_scheduler_work_owned()
412            .is_none_or(|publication| self.deliver_scheduler_work_owned(publication))
413    }
414
415    /// Rearms the physical doorbell after an owner-side bounded drain.
416    ///
417    /// Unlike producer delivery, this must not suppress a local notification:
418    /// the current scheduler safe point has already consumed its delivery
419    /// edge and is about to return. A remaining batch therefore needs a fresh
420    /// interrupt even when the owner itself is the current CPU.
421    pub(crate) fn defer_scheduler_work(&self) {
422        let Some(_publication) = self.begin_owner_delivery() else {
423            task_runtime::fatal_invariant(
424                DEFERRED_SCHEDULER_WORK_OFFLINE_INVARIANT,
425                self.owner.as_u32() as usize,
426            );
427        };
428        let _irq = IrqScope::enter();
429        self.scheduler_request
430            .request
431            .fetch_or(REQUEST_OWNER_WORK, Ordering::Release);
432        self.ring_scheduler_doorbell();
433    }
434
435    pub(super) fn deliver_scheduler_work_owned(
436        &self,
437        publication: SchedulerRequestPublication,
438    ) -> bool {
439        if publication.delivery == SchedulerRequestDelivery::PollingOwner
440            || self.current_cpu_will_service_local_work()
441        {
442            return true;
443        }
444        self.ring_scheduler_doorbell()
445    }
446
447    fn ring_scheduler_doorbell(&self) -> bool {
448        match task_runtime::notify_scheduler_cpu(RuntimeCpuId::new(self.owner.as_u32())) {
449            RuntimeStatus::Success => true,
450            status => task_runtime::fatal_invariant(
451                0x4950_4900 | status as u32,
452                self.owner.as_u32() as usize,
453            ),
454        }
455    }
456
457    fn current_cpu_will_service_local_work(&self) -> bool {
458        // Every caller retains an IrqScope from before this observation through
459        // publication completion, so the runtime CPU identity cannot migrate.
460        let current = unsafe { task_runtime::current_cpu_id() };
461        if current.as_u32() != self.owner.as_u32() {
462            return false;
463        }
464        // Publish into the architecture preemption word before suppressing a
465        // self-IPI. Hard IRQ return consumes that state through its outer
466        // preemption guard. Ordinary task publication instead converts the
467        // final IRQ guard directly into the scheduler baton.
468        task_runtime::publish_local_scheduler_work()
469    }
470
471    /// Tests the sticky reschedule request without consuming it.
472    pub fn needs_reschedule(&self) -> bool {
473        self.scheduler_request.request.load(Ordering::Acquire) & REQUEST_REASON_MASK != 0
474    }
475
476    /// Returns whether a kernel preempt-enable or IRQ-return boundary must
477    /// enter the scheduler. Lazy Fair preemption alone is intentionally
478    /// excluded, matching Linux's folded architecture need-resched word.
479    pub(crate) fn needs_immediate_scheduler_work(&self) -> bool {
480        self.scheduler_request.request.load(Ordering::Acquire)
481            & (REQUEST_PREEMPT | REQUEST_OWNER_WORK)
482            != 0
483    }
484
485    pub(crate) fn scheduler_request_pending(&self, scope: SchedulerRequestScope) -> bool {
486        self.scheduler_request.request.load(Ordering::Acquire) & scope.claim_mask() != 0
487    }
488
489    /// Returns whether a sticky preemption request owns scheduler progress.
490    ///
491    /// Unlike [`Self::needs_reschedule`], owner-only deferred work does not
492    /// transfer ownership of the current task's runtime clockevent.
493    pub(crate) fn immediate_preemption_requested(&self) -> bool {
494        self.scheduler_request.request.load(Ordering::Acquire) & REQUEST_PREEMPT != 0
495    }
496
497    pub(crate) fn claim_scheduler_request(
498        &self,
499        scope: SchedulerRequestScope,
500    ) -> SchedulerRequestClaim {
501        self.scheduler_request.claim(scope)
502    }
503
504    /// Promotes Linux's lazy thread flag at the periodic scheduler tick.
505    ///
506    /// The caller is the target CPU's timer owner, so no physical delivery is
507    /// needed; IRQ return observes the newly published ordinary request.
508    pub(crate) fn promote_lazy_reschedule(&self) -> bool {
509        self.scheduler_request.promote_lazy()
510    }
511
512    pub(crate) fn finish_scheduler_request(&self) {
513        let request = self.scheduler_request.request.load(Ordering::Acquire);
514        if self.has_remote_work() && request & REQUEST_OWNER_WORK == 0 {
515            self.request_scheduler_work();
516        }
517    }
518
519    pub(crate) fn defer_park_preemption(&self, request: SchedulerRequestClaim) {
520        self.scheduler_request.defer_park_preemption(request);
521    }
522
523    pub(crate) fn finish_park_preemption(&self, resume_running: bool) {
524        self.scheduler_request
525            .finish_park_preemption(resume_running);
526    }
527
528    /// Restores a scheduler request claimed by a park that was cancelled
529    /// before it could publish Blocked.
530    pub(crate) fn restore_claimed_park_preemption(&self, request: SchedulerRequestClaim) {
531        self.scheduler_request
532            .restore_claimed_park_preemption(request);
533    }
534
535    pub(crate) fn prepare_idle_wait(&self) -> bool {
536        let previous = self
537            .scheduler_request
538            .request
539            .fetch_or(REQUEST_IDLE_POLLING, Ordering::AcqRel);
540        let may_wait = previous & REQUEST_REASON_MASK == 0
541            && !self.needs_reschedule()
542            && !self.has_remote_work()
543            && self.queued_summary() == 0;
544        if !may_wait {
545            self.finish_idle_wait();
546        }
547        may_wait
548    }
549
550    pub(crate) fn finish_idle_wait(&self) {
551        self.scheduler_request
552            .request
553            .fetch_and(!REQUEST_IDLE_POLLING, Ordering::Release);
554        // Linux `current_clr_polling()` pairs this full barrier with
555        // `resched_curr()`: work published before the clear remains visible
556        // to the final IRQ-off recheck, while a producer observing the clear
557        // must ring the physical doorbell.
558        fence(Ordering::SeqCst);
559    }
560
561    pub(crate) fn is_idle_polling(&self) -> bool {
562        self.scheduler_request.request.load(Ordering::Acquire) & REQUEST_IDLE_POLLING != 0
563    }
564
565    pub(super) fn reset_scheduler_for_offline(&self) {
566        self.scheduler_request.request.store(0, Ordering::Relaxed);
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    #[test]
575    fn ordinary_request_folds_a_later_lazy_request() {
576        let request = SchedulerRequestState::new();
577        assert!(request.publish(REQUEST_PREEMPT).is_some());
578        assert!(
579            request.publish(REQUEST_PREEMPT_LAZY).is_none(),
580            "a live ordinary request must cover the lazy reason"
581        );
582
583        let immediate = request.request.fetch_and(
584            !SchedulerRequestScope::Immediate.claim_mask(),
585            Ordering::AcqRel,
586        );
587        assert_ne!(immediate & REQUEST_PREEMPT, 0);
588        assert_eq!(
589            request.request.load(Ordering::Acquire) & REQUEST_PREEMPT_LAZY,
590            0,
591            "the ordinary scheduling pass must not leave lazy residue"
592        );
593
594        assert!(
595            request.publish(REQUEST_PREEMPT_LAZY).is_some(),
596            "a lazy reason published after ordinary consumption is new work"
597        );
598        assert_ne!(
599            request.request.load(Ordering::Acquire) & REQUEST_PREEMPT_LAZY,
600            0
601        );
602    }
603
604    #[test]
605    fn periodic_tick_moves_lazy_request_into_ordinary_class() {
606        let request = SchedulerRequestState::new();
607        assert!(request.publish(REQUEST_PREEMPT_LAZY).is_some());
608        assert!(request.promote_lazy());
609
610        let claim = request.claim(SchedulerRequestScope::Immediate);
611        assert!(claim.immediate_preempt_requested());
612        assert!(
613            !request
614                .claim(SchedulerRequestScope::All)
615                .preemption_requested(),
616            "the ordinary scheduling pass must consume the lazy reason promoted by this tick"
617        );
618    }
619
620    #[test]
621    fn notified_park_restores_its_claimed_preemption_request() {
622        let ordinary = SchedulerRequestState::new();
623        let _ = ordinary.publish(REQUEST_PREEMPT);
624        let claim = ordinary.claim(SchedulerRequestScope::All);
625        ordinary.restore_claimed_park_preemption(claim);
626
627        assert_ne!(
628            ordinary.request.load(Ordering::Acquire) & REQUEST_PREEMPT,
629            0,
630            "a notified park must restore the ordinary request claimed before cancellation"
631        );
632
633        let lazy = SchedulerRequestState::new();
634        let _ = lazy.publish(REQUEST_PREEMPT_LAZY);
635        let claim = lazy.claim(SchedulerRequestScope::All);
636        lazy.restore_claimed_park_preemption(claim);
637        assert_ne!(
638            lazy.request.load(Ordering::Acquire) & REQUEST_PREEMPT_LAZY,
639            0,
640            "a notified park must restore the lazy request claimed before cancellation"
641        );
642    }
643
644    #[test]
645    fn repeated_remote_preemption_retains_a_delivery_attempt() {
646        let request = SchedulerRequestState::new();
647
648        assert!(request.publish_remote(REQUEST_PREEMPT).is_some());
649        assert!(
650            request.publish_remote(REQUEST_PREEMPT).is_some(),
651            "a fresh remote rq decision must reach DeliveryEdge even while the CPU bit is sticky"
652        );
653        assert!(
654            request.publish_remote(REQUEST_PREEMPT_LAZY).is_none(),
655            "a repeated lazy request must remain logical-only"
656        );
657    }
658}