Skip to main content

rocketmq_runtime/blocking/
executor.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::future::Future;
16use std::num::NonZeroUsize;
17use std::sync::atomic::AtomicU64;
18use std::sync::atomic::AtomicUsize;
19use std::sync::atomic::Ordering;
20use std::sync::Arc;
21use std::time::Duration;
22use std::time::Instant;
23
24use dashmap::DashMap;
25use parking_lot::Mutex;
26use tokio::sync::OwnedSemaphorePermit;
27use tokio::sync::Semaphore;
28use tokio_util::sync::CancellationToken;
29use tokio_util::task::task_tracker::TaskTrackerToken;
30use tokio_util::task::TaskTracker;
31
32use super::admission::GlobalBlockingBudget;
33use super::admission::GlobalBlockingPermit;
34use super::diagnostics::BlockingTaskMeta;
35use super::diagnostics::BlockingTaskTable;
36use super::BlockingExecutorSnapshot;
37use super::BlockingKind;
38use super::BlockingLane;
39use super::BlockingPoolPolicy;
40use super::BlockingTaskId;
41use super::BlockingTaskState;
42use crate::error::RuntimeContractViolation;
43use crate::error::RuntimeError;
44use crate::error::RuntimeResult;
45use crate::handle::RuntimeHandle;
46use crate::shutdown_deadline::ShutdownDeadline;
47use crate::task_group::TaskGroup;
48use crate::task_group::TaskGroupLifecycleState;
49
50/// Runs short blocking work through a bounded lane and one root-owned global
51/// admission budget.
52///
53/// Cloning this value shares queue state and capacity; it never creates a new
54/// owner. Execution always uses the injected owner's Tokio runtime, including
55/// when the submission future is polled by a different runtime.
56/// Cancellation while queued removes the task immediately. Cancellation
57/// or timeout after execution begins leaves the admission permit inside the
58/// actual blocking closure, so capacity is released only when that closure
59/// exits.
60#[derive(Debug, Clone)]
61pub struct BlockingExecutor {
62    runtime: RuntimeHandle,
63    policy: Arc<BlockingPoolPolicy>,
64    lane: BlockingLane,
65    budget: GlobalBlockingBudget,
66    queue_permits: Arc<Semaphore>,
67    tasks: Arc<BlockingTaskTable>,
68    next_task_id: Arc<AtomicU64>,
69    rejected: Arc<AtomicU64>,
70    admission: BlockingAdmission,
71    isolated: Option<Arc<IsolatedLifecycle>>,
72}
73
74#[derive(Debug, Default)]
75struct IsolatedLifecycle {
76    gate: Mutex<()>,
77    closed: CancellationToken,
78    tracker: TaskTracker,
79}
80
81/// Completion evidence for one isolated executor and all of its clones.
82///
83/// Running closures cannot be interrupted. An incomplete report can be
84/// followed by another [`BlockingExecutor::shutdown_until`] call.
85#[derive(Debug, Clone)]
86pub struct BlockingExecutorShutdownReport {
87    /// Whether every accepted submission and closure has released its ownership.
88    pub completed: bool,
89    /// Submissions or closures still owned at the observation time.
90    pub pending_operations: usize,
91    /// Queued and running blocking work at the observation time.
92    pub snapshot: BlockingExecutorSnapshot,
93}
94
95#[derive(Debug, Clone)]
96enum BlockingAdmission {
97    Unscoped,
98    Scope(TaskGroup),
99    Drain(Arc<DrainLeaseState>),
100}
101
102#[derive(Debug)]
103struct DrainLeaseState {
104    scope: TaskGroup,
105    deadline: ShutdownDeadline,
106    remaining: AtomicUsize,
107}
108
109impl DrainLeaseState {
110    fn effective_deadline(&self) -> ShutdownDeadline {
111        self.scope
112            .shutdown_deadline()
113            .map_or(self.deadline, |scope_deadline| self.deadline.earliest(scope_deadline))
114    }
115}
116
117/// A bounded authority for I/O required by an operation already accepted by a
118/// service scope.
119///
120/// A lease is created while its scope is open. It may then be used during that
121/// scope's shutdown, but only until its original deadline or an earlier
122/// shutdown deadline installed on that scope, and only for its reserved number
123/// of submissions. It is deliberately not cloneable: moving it preserves one
124/// shared, non-expandable allowance.
125#[derive(Debug)]
126pub struct BlockingDrainLease {
127    executor: BlockingExecutor,
128}
129
130impl BlockingDrainLease {
131    /// Runs one short I/O operation under this lease.
132    ///
133    /// The existing lane capacity and deadline policy still apply. A running
134    /// closure retains its execution permit until it exits. This does not
135    /// reopen the scope's ordinary admission.
136    pub fn spawn_io<F, R>(
137        &self,
138        name: impl Into<Arc<str>>,
139        operation: F,
140    ) -> impl Future<Output = RuntimeResult<R>> + Send + '_
141    where
142        F: FnOnce() -> R + Send + 'static,
143        R: Send + 'static,
144    {
145        self.executor.spawn_io(name, operation)
146    }
147
148    /// Runs one short I/O operation without extending this lease's deadline.
149    pub fn spawn_io_until<F, R>(
150        &self,
151        name: impl Into<Arc<str>>,
152        deadline: ShutdownDeadline,
153        operation: F,
154    ) -> impl Future<Output = RuntimeResult<R>> + Send + '_
155    where
156        F: FnOnce() -> R + Send + 'static,
157        R: Send + 'static,
158    {
159        self.executor.spawn_io_until(name, deadline, operation)
160    }
161
162    /// Returns the number of submissions that this lease can still admit.
163    #[must_use]
164    pub fn remaining_operations(&self) -> usize {
165        let BlockingAdmission::Drain(state) = &self.executor.admission else {
166            return 0;
167        };
168        state.remaining.load(Ordering::Acquire)
169    }
170}
171
172#[derive(Debug)]
173struct DrainReservation {
174    state: Option<Arc<DrainLeaseState>>,
175}
176
177impl DrainReservation {
178    const fn none() -> Self {
179        Self { state: None }
180    }
181
182    fn committed(mut self) {
183        self.state.take();
184    }
185}
186
187impl Drop for DrainReservation {
188    fn drop(&mut self) {
189        if let Some(state) = self.state.take() {
190            state.remaining.fetch_add(1, Ordering::Release);
191        }
192    }
193}
194
195#[derive(Debug, Clone, Copy)]
196enum AdmissionFailure {
197    ScopeClosed,
198    LeaseExpired,
199    LeaseExhausted,
200    QueueCapacityExhausted,
201    QueueDeadlineExpired,
202}
203
204impl AdmissionFailure {
205    fn into_error(self) -> RuntimeError {
206        match self {
207            Self::ScopeClosed => RuntimeError::closed(crate::RuntimeOperation::BlockingQueueAdmission),
208            Self::LeaseExpired | Self::QueueDeadlineExpired => {
209                RuntimeError::timed_out(crate::RuntimeOperation::BlockingQueueAdmission)
210            }
211            Self::LeaseExhausted | Self::QueueCapacityExhausted => {
212                RuntimeError::capacity(crate::RuntimeOperation::BlockingQueueAdmission)
213            }
214        }
215    }
216}
217
218struct QueuedBlockingTaskGuard {
219    tasks: Arc<DashMap<BlockingTaskId, BlockingTaskMeta>>,
220    task_id: BlockingTaskId,
221    armed: bool,
222}
223
224impl QueuedBlockingTaskGuard {
225    fn new(tasks: Arc<DashMap<BlockingTaskId, BlockingTaskMeta>>, task_id: BlockingTaskId) -> Self {
226        Self {
227            tasks,
228            task_id,
229            armed: true,
230        }
231    }
232
233    fn disarm(mut self) {
234        self.armed = false;
235    }
236}
237
238impl Drop for QueuedBlockingTaskGuard {
239    fn drop(&mut self) {
240        if self.armed {
241            self.tasks.remove(&self.task_id);
242        }
243    }
244}
245
246/// An admitted operation whose actual completion outlives any individual wait.
247///
248/// The owner may retain this ticket after `wait_until` expires and call `wait`
249/// to observe the real result. Dropping it abandons observation, not execution.
250pub(crate) struct BlockingTask<R>
251where
252    R: Send + 'static,
253{
254    join_handle: Option<tokio::task::JoinHandle<RuntimeResult<R>>>,
255    tasks: Arc<DashMap<BlockingTaskId, BlockingTaskMeta>>,
256    task_id: BlockingTaskId,
257    wait_deadline: Instant,
258}
259
260impl<R> BlockingTask<R>
261where
262    R: Send + 'static,
263{
264    fn new(
265        join_handle: tokio::task::JoinHandle<RuntimeResult<R>>,
266        tasks: Arc<DashMap<BlockingTaskId, BlockingTaskMeta>>,
267        task_id: BlockingTaskId,
268        wait_deadline: Instant,
269    ) -> Self {
270        Self {
271            join_handle: Some(join_handle),
272            tasks,
273            task_id,
274            wait_deadline,
275        }
276    }
277
278    /// Waits for real execution completion without changing ownership on drop.
279    pub(crate) async fn wait(&mut self) -> RuntimeResult<R> {
280        let Some(join_handle) = self.join_handle.as_mut() else {
281            return Err(RuntimeError::context_unavailable(
282                crate::RuntimeOperation::RunBlockingTask,
283            ));
284        };
285        let result = join_handle.await;
286        self.join_handle.take();
287        result.map_err(|error| RuntimeError::join(crate::RuntimeOperation::RunBlockingTask, error))?
288    }
289
290    async fn wait_until(&mut self, deadline: Instant) -> RuntimeResult<R> {
291        match tokio::time::timeout_at(tokio::time::Instant::from_std(deadline), self.wait()).await {
292            Ok(result) => result,
293            Err(_elapsed) => {
294                self.mark_timed_out();
295                Err(RuntimeError::timed_out(crate::RuntimeOperation::BlockingTask))
296            }
297        }
298    }
299
300    fn mark_timed_out(&mut self) {
301        if let Some(mut meta) = self.tasks.get_mut(&self.task_id) {
302            meta.state = BlockingTaskState::TimedOutStillRunning;
303        }
304    }
305}
306
307impl<R> Drop for BlockingTask<R>
308where
309    R: Send + 'static,
310{
311    fn drop(&mut self) {
312        if self.join_handle.is_some() {
313            self.mark_timed_out();
314        }
315    }
316}
317
318// A queued submission drops user captures before its lifecycle registration.
319struct BlockingSubmission<F> {
320    operation: F,
321    registration: Option<TaskTrackerToken>,
322}
323
324// Field order also governs cancellation before Tokio invokes the closure:
325// destroy user captures, return execution capacity, then remove diagnostics.
326struct BlockingWork<F> {
327    operation: F,
328    permit: GlobalBlockingPermit,
329    completion: BlockingCompletionGuard,
330    execution_deadline: Option<ShutdownDeadline>,
331}
332
333impl<F> BlockingWork<F> {
334    fn run<R>(self) -> RuntimeResult<R>
335    where
336        F: FnOnce() -> R,
337    {
338        // Reverse local destruction order preserves the same ordering on panic.
339        let completion = self.completion;
340        let permit = self.permit;
341        let result = if self.execution_deadline.is_some_and(ShutdownDeadline::is_expired) {
342            Err(RuntimeError::timed_out(crate::RuntimeOperation::BlockingTaskDeadline))
343        } else {
344            Ok((self.operation)())
345        };
346        drop(permit);
347        drop(completion);
348        result
349    }
350}
351
352struct BlockingCompletionGuard {
353    tasks: Arc<DashMap<BlockingTaskId, BlockingTaskMeta>>,
354    task_id: BlockingTaskId,
355    _isolated_registration: Option<TaskTrackerToken>,
356}
357
358impl Drop for BlockingCompletionGuard {
359    fn drop(&mut self) {
360        self.tasks.remove(&self.task_id);
361    }
362}
363
364impl BlockingExecutor {
365    /// Creates an isolated executor through [`Self::new_isolated`].
366    ///
367    /// # Errors
368    ///
369    /// Returns a contract violation when `policy` is invalid.
370    pub fn new(policy: BlockingPoolPolicy, owner_group: TaskGroup) -> Result<Self, RuntimeContractViolation> {
371        Self::new_isolated(policy, owner_group)
372    }
373
374    /// Creates an isolated executor for tests and adapters.
375    ///
376    /// Production components use the managed lanes of a
377    /// [`ChildServiceContext`](crate::ChildServiceContext), which share one
378    /// global budget. This executor instead has its own exact capacity and task
379    /// table, outside the managed lanes and their diagnostics. `owner_group`
380    /// supplies its runtime and admission scope, and the executor is registered
381    /// with that group's tree: a closure still running when the tree's owner
382    /// assembles a shutdown report is counted in `blocking_still_running`, so
383    /// the report is not healthy while that work can still have side effects.
384    ///
385    /// # Errors
386    ///
387    /// Returns a contract violation when `policy` is invalid.
388    pub fn new_isolated(policy: BlockingPoolPolicy, owner_group: TaskGroup) -> Result<Self, RuntimeContractViolation> {
389        policy.validate()?;
390        let capacity = policy.max_concurrency;
391        let mut executor = Self::new_with_budget(
392            policy,
393            BlockingLane::StorageIo,
394            GlobalBlockingBudget::isolated(capacity),
395            owner_group.runtime().clone(),
396        );
397        executor.isolated = Some(Arc::new(IsolatedLifecycle::default()));
398        owner_group.register_isolated_blocking(&executor.tasks);
399        Ok(executor.scoped_to(owner_group))
400    }
401
402    pub(crate) fn new_managed(
403        policy: BlockingPoolPolicy,
404        lane: BlockingLane,
405        budget: GlobalBlockingBudget,
406        runtime: RuntimeHandle,
407    ) -> Result<Self, RuntimeContractViolation> {
408        policy.validate()?;
409        Ok(Self::new_with_budget(policy, lane, budget, runtime))
410    }
411
412    fn new_with_budget(
413        policy: BlockingPoolPolicy,
414        lane: BlockingLane,
415        budget: GlobalBlockingBudget,
416        runtime: RuntimeHandle,
417    ) -> Self {
418        Self {
419            runtime,
420            queue_permits: Arc::new(Semaphore::new(policy.max_queue_depth)),
421            policy: Arc::new(policy),
422            lane,
423            budget,
424            // Admission bounds the table to the lane's running and queued work.
425            tasks: Arc::new(DashMap::with_shard_amount(8)),
426            next_task_id: Arc::new(AtomicU64::new(1)),
427            rejected: Arc::new(AtomicU64::new(0)),
428            admission: BlockingAdmission::Unscoped,
429            isolated: None,
430        }
431    }
432
433    /// Stops admission to this isolated executor and wakes its queued submissions.
434    ///
435    /// All clones share this gate. The supplied task group and other executors
436    /// remain open. Already admitted closures continue to own their permits.
437    ///
438    /// # Errors
439    ///
440    /// Returns an unsupported-operation error for a managed lane; its service
441    /// context owns admission and shutdown.
442    pub fn stop_admission(&self) -> RuntimeResult<()> {
443        let lifecycle = self
444            .isolated
445            .as_ref()
446            .ok_or_else(|| RuntimeError::unsupported(crate::RuntimeOperation::ShutdownBlockingExecutor))?;
447        let _gate = lifecycle.gate.lock();
448        lifecycle.closed.cancel();
449        lifecycle.tracker.close();
450        Ok(())
451    }
452
453    /// Stops this isolated executor and asynchronously waits for owned work.
454    ///
455    /// The caller's deadline bounds waiting, not execution of already-running
456    /// closures. An incomplete report retains their counts. This method does
457    /// not block the calling thread or close the supplied task group.
458    ///
459    /// # Errors
460    ///
461    /// Returns an unsupported-operation error for a managed lane.
462    pub async fn shutdown_until(&self, deadline: ShutdownDeadline) -> RuntimeResult<BlockingExecutorShutdownReport> {
463        self.stop_admission()?;
464        let lifecycle = self
465            .isolated
466            .as_ref()
467            .expect("stop_admission validates an isolated executor");
468        let _ = tokio::time::timeout_at(deadline.instant().into(), lifecycle.tracker.wait()).await;
469        let pending_operations = lifecycle.tracker.len();
470        Ok(BlockingExecutorShutdownReport {
471            completed: pending_operations == 0,
472            pending_operations,
473            snapshot: self.snapshot(),
474        })
475    }
476
477    async fn isolated_closed(&self) {
478        match &self.isolated {
479            Some(lifecycle) => lifecycle.closed.cancelled().await,
480            None => std::future::pending().await,
481        }
482    }
483
484    pub(crate) fn scoped_to(&self, scope: TaskGroup) -> Self {
485        let mut scoped = self.clone();
486        scoped.admission = BlockingAdmission::Scope(scope);
487        scoped
488    }
489
490    /// Reserves a bounded blocking-I/O allowance for work already accepted by
491    /// this executor's service scope, or for that owner's finalization slot.
492    ///
493    /// The scope must still be open when the lease is created. The lease never
494    /// extends `deadline` or a shutdown deadline later installed on its scope,
495    /// and its non-zero submission allowance is shared if the lease is moved
496    /// through application-owned shutdown code.
497    ///
498    /// # Errors
499    ///
500    /// Returns an unavailable failure when this executor is unscoped or its
501    /// scope is no longer open, and a timeout failure when `deadline` expired.
502    pub fn try_drain_lease(
503        &self,
504        deadline: ShutdownDeadline,
505        max_operations: NonZeroUsize,
506    ) -> RuntimeResult<BlockingDrainLease> {
507        let BlockingAdmission::Scope(scope) = &self.admission else {
508            return Err(AdmissionFailure::ScopeClosed.into_error());
509        };
510        if !scope_is_open(scope) {
511            return Err(AdmissionFailure::ScopeClosed.into_error());
512        }
513        let deadline = scope
514            .shutdown_deadline()
515            .map_or(deadline, |scope_deadline| deadline.earliest(scope_deadline));
516        if deadline.is_expired() {
517            return Err(AdmissionFailure::LeaseExpired.into_error());
518        }
519        let mut leased = self.clone();
520        leased.admission = BlockingAdmission::Drain(Arc::new(DrainLeaseState {
521            scope: scope.clone(),
522            deadline,
523            remaining: AtomicUsize::new(max_operations.get()),
524        }));
525        Ok(BlockingDrainLease { executor: leased })
526    }
527
528    /// Returns the policy.
529    pub fn policy(&self) -> &BlockingPoolPolicy {
530        &self.policy
531    }
532
533    /// Runs short blocking I/O with the preparation and polling behavior of [`Self::spawn`].
534    pub fn spawn_io<F, R>(
535        &self,
536        name: impl Into<Arc<str>>,
537        operation: F,
538    ) -> impl Future<Output = RuntimeResult<R>> + Send + '_
539    where
540        F: FnOnce() -> R + Send + 'static,
541        R: Send + 'static,
542    {
543        self.spawn(name, BlockingKind::ShortIo, operation)
544    }
545
546    /// Admits short I/O and returns its execution-owned completion ticket.
547    #[cfg(test)]
548    pub(crate) fn submit_io<F, R>(
549        &self,
550        name: impl Into<Arc<str>>,
551        operation: F,
552    ) -> impl Future<Output = RuntimeResult<BlockingTask<R>>> + Send + '_
553    where
554        F: FnOnce() -> R + Send + 'static,
555        R: Send + 'static,
556    {
557        self.submit_inner(name.into(), BlockingKind::ShortIo, None, Box::new(operation))
558    }
559
560    /// Admits short I/O under an optional caller deadline and returns its
561    /// execution-owned completion ticket.
562    ///
563    /// The deadline is combined with the lane phase budgets by `phase_deadline`,
564    /// so it can only tighten them. An expired deadline refuses the submission
565    /// instead of starting the closure.
566    pub(crate) fn submit_io_until<F, R>(
567        &self,
568        name: impl Into<Arc<str>>,
569        deadline: Option<ShutdownDeadline>,
570        operation: F,
571    ) -> impl Future<Output = RuntimeResult<BlockingTask<R>>> + Send + '_
572    where
573        F: FnOnce() -> R + Send + 'static,
574        R: Send + 'static,
575    {
576        self.submit_inner(name.into(), BlockingKind::ShortIo, deadline, Box::new(operation))
577    }
578
579    /// Runs short blocking I/O without admitting or waiting for work beyond `deadline`.
580    pub fn spawn_io_until<F, R>(
581        &self,
582        name: impl Into<Arc<str>>,
583        deadline: ShutdownDeadline,
584        operation: F,
585    ) -> impl Future<Output = RuntimeResult<R>> + Send + '_
586    where
587        F: FnOnce() -> R + Send + 'static,
588        R: Send + 'static,
589    {
590        self.spawn_until(name, BlockingKind::ShortIo, deadline, operation)
591    }
592
593    /// Spawns the supplied task.
594    ///
595    /// Converts the name and boxes the closure immediately to keep large
596    /// captures out of the returned future. Admission and submission begin
597    /// only when that future is polled.
598    pub fn spawn<F, R>(
599        &self,
600        name: impl Into<Arc<str>>,
601        kind: BlockingKind,
602        operation: F,
603    ) -> impl Future<Output = RuntimeResult<R>> + Send + '_
604    where
605        F: FnOnce() -> R + Send + 'static,
606        R: Send + 'static,
607    {
608        // A synchronous boundary keeps F out of every admission/wait future.
609        // One closure allocation also avoids a size-dependent public future
610        // layout: an enum containing an inline F would still be as large as F.
611        self.spawn_inner(name.into(), kind, None, Box::new(operation))
612    }
613
614    /// Runs blocking work with one absolute deadline for admission and waiting.
615    ///
616    /// Expiry stops waiting; an already running closure retains its capacity
617    /// until it exits and may still produce side effects.
618    pub fn spawn_until<F, R>(
619        &self,
620        name: impl Into<Arc<str>>,
621        kind: BlockingKind,
622        deadline: ShutdownDeadline,
623        operation: F,
624    ) -> impl Future<Output = RuntimeResult<R>> + Send + '_
625    where
626        F: FnOnce() -> R + Send + 'static,
627        R: Send + 'static,
628    {
629        self.spawn_inner(name.into(), kind, Some(deadline), Box::new(operation))
630    }
631
632    async fn spawn_inner<F, R>(
633        &self,
634        name: Arc<str>,
635        kind: BlockingKind,
636        deadline: Option<ShutdownDeadline>,
637        operation: F,
638    ) -> RuntimeResult<R>
639    where
640        F: FnOnce() -> R + Send + 'static,
641        R: Send + 'static,
642    {
643        if std::mem::size_of::<R>() > crate::stack::MAX_INLINE_SIZE {
644            // Keep a large result out of nested JoinHandle/timeout/Result poll
645            // frames; unwrap it only once at the public return boundary.
646            let task = self
647                .submit_inner(name, kind, deadline, move || Box::new(operation()))
648                .await?;
649            self.wait_for_caller(task).await.map(|value| *value)
650        } else {
651            let task = self.submit_inner(name, kind, deadline, operation).await?;
652            self.wait_for_caller(task).await
653        }
654    }
655
656    async fn wait_for_caller<R: Send + 'static>(&self, mut task: BlockingTask<R>) -> RuntimeResult<R> {
657        let task_id = task.task_id;
658        let started_at = Instant::now();
659        let deadline = task.wait_deadline;
660        let result = task.wait_until(deadline).await;
661        if result.is_ok() {
662            let elapsed = started_at.elapsed();
663            if elapsed > self.policy.warn_after {
664                tracing::warn!(
665                    task_id = task_id.as_u64(),
666                    elapsed_ms = elapsed.as_millis(),
667                    "blocking task exceeded warn_after"
668                );
669            }
670        }
671        result
672    }
673
674    async fn submit_inner<F, R>(
675        &self,
676        name: Arc<str>,
677        kind: BlockingKind,
678        deadline: Option<ShutdownDeadline>,
679        operation: F,
680    ) -> RuntimeResult<BlockingTask<R>>
681    where
682        F: FnOnce() -> R + Send + 'static,
683        R: Send + 'static,
684    {
685        if kind == BlockingKind::LongRunning {
686            self.rejected.fetch_add(1, Ordering::Relaxed);
687            return Err(RuntimeError::unsupported(crate::RuntimeOperation::BlockingExecutorKind));
688        }
689        let caller_deadline = deadline;
690        let admission_deadline = self.admission.effective_deadline(caller_deadline);
691        if admission_deadline.is_some_and(ShutdownDeadline::is_expired) {
692            self.rejected.fetch_add(1, Ordering::Relaxed);
693            return Err(RuntimeError::timed_out(crate::RuntimeOperation::BlockingQueueAdmission));
694        }
695        let drain_reservation = self.admission.reserve().map_err(|failure| {
696            self.rejected.fetch_add(1, Ordering::Relaxed);
697            failure.into_error()
698        })?;
699        let submitted_at = Instant::now();
700        let operation_deadline = admission_deadline.map_or_else(
701            || {
702                submitted_at
703                    .checked_add(self.policy.queue_timeout.saturating_add(self.policy.task_timeout))
704                    .unwrap_or(submitted_at)
705            },
706            ShutdownDeadline::instant,
707        );
708        let queue_deadline = phase_deadline(submitted_at, self.policy.queue_timeout, operation_deadline);
709
710        let isolated_registration = if let Some(lifecycle) = &self.isolated {
711            let _gate = lifecycle.gate.lock();
712            if lifecycle.closed.is_cancelled() {
713                self.rejected.fetch_add(1, Ordering::Relaxed);
714                return Err(AdmissionFailure::ScopeClosed.into_error());
715            }
716            Some(lifecycle.tracker.token())
717        } else {
718            None
719        };
720
721        let submission = BlockingSubmission {
722            operation,
723            registration: isolated_registration,
724        };
725
726        let queue_permit = tokio::select! {
727            biased;
728            _ = self.isolated_closed() => Err(AdmissionFailure::ScopeClosed),
729            result = self.acquire_queue_permit(queue_deadline) => result,
730        }
731        .map_err(|failure| {
732            self.rejected.fetch_add(1, Ordering::Relaxed);
733            failure.into_error()
734        })?;
735        let task_id = BlockingTaskId(self.next_task_id.fetch_add(1, Ordering::Relaxed));
736        self.tasks.insert(
737            task_id,
738            BlockingTaskMeta {
739                id: task_id,
740                name: name.clone(),
741                kind,
742                state: BlockingTaskState::Queued,
743                queued_at: Instant::now(),
744                started_at: None,
745            },
746        );
747        let queued_task_guard = QueuedBlockingTaskGuard::new(self.tasks.clone(), task_id);
748
749        let permit = tokio::select! {
750            biased;
751            _ = self.isolated_closed() => Err(AdmissionFailure::ScopeClosed),
752            result = self.admit(queue_deadline) => result,
753        }
754        .map_err(|failure| {
755            self.rejected.fetch_add(1, Ordering::Relaxed);
756            failure.into_error()
757        })?;
758        drop(queue_permit);
759
760        // Serialize the final handoff with independent executor shutdown.
761        // No asynchronous wait occurs while this gate is held.
762        let _isolated_gate = self.isolated.as_ref().map(|lifecycle| lifecycle.gate.lock());
763        if self
764            .isolated
765            .as_ref()
766            .is_some_and(|lifecycle| lifecycle.closed.is_cancelled())
767        {
768            self.rejected.fetch_add(1, Ordering::Relaxed);
769            return Err(AdmissionFailure::ScopeClosed.into_error());
770        }
771
772        let execution_deadline = self.admission.effective_deadline(caller_deadline);
773        let current_operation_deadline = execution_deadline.map_or(operation_deadline, ShutdownDeadline::instant);
774        let task_deadline = phase_deadline(Instant::now(), self.policy.task_timeout, current_operation_deadline);
775        if task_deadline <= Instant::now()
776            || execution_deadline.is_some_and(ShutdownDeadline::is_expired)
777            || !self.admission.still_valid()
778        {
779            self.rejected.fetch_add(1, Ordering::Relaxed);
780            return Err(
781                if task_deadline <= Instant::now() || execution_deadline.is_some_and(ShutdownDeadline::is_expired) {
782                    RuntimeError::timed_out(crate::RuntimeOperation::BlockingTaskDeadline)
783                } else {
784                    AdmissionFailure::ScopeClosed.into_error()
785                },
786            );
787        }
788
789        let started_at = Instant::now();
790        if let Some(mut meta) = self.tasks.get_mut(&task_id) {
791            meta.state = BlockingTaskState::Running;
792            meta.started_at = Some(started_at);
793        }
794        queued_task_guard.disarm();
795
796        let BlockingSubmission {
797            operation,
798            registration,
799        } = submission;
800        let work = BlockingWork {
801            operation,
802            permit,
803            completion: BlockingCompletionGuard {
804                tasks: self.tasks.clone(),
805                task_id,
806                _isolated_registration: registration,
807            },
808            execution_deadline,
809        };
810        let join_handle = self.runtime.tokio_handle().spawn_blocking(move || work.run());
811        drain_reservation.committed();
812        Ok(BlockingTask::new(
813            join_handle,
814            self.tasks.clone(),
815            task_id,
816            task_deadline,
817        ))
818    }
819
820    async fn acquire_queue_permit(&self, deadline: Instant) -> Result<OwnedSemaphorePermit, AdmissionFailure> {
821        if !matches!(&self.admission, BlockingAdmission::Drain(_)) {
822            return self
823                .queue_permits
824                .clone()
825                .try_acquire_owned()
826                .map_err(|_error| AdmissionFailure::QueueCapacityExhausted);
827        }
828
829        let deadline = self.admission.effective_instant(deadline);
830        match tokio::time::timeout_at(
831            tokio::time::Instant::from_std(deadline),
832            self.queue_permits.clone().acquire_owned(),
833        )
834        .await
835        {
836            Ok(Ok(permit)) => Ok(permit),
837            Ok(Err(_closed)) => Err(AdmissionFailure::QueueDeadlineExpired),
838            Err(_elapsed) => Err(AdmissionFailure::QueueDeadlineExpired),
839        }
840    }
841
842    async fn admit(&self, deadline: Instant) -> Result<GlobalBlockingPermit, AdmissionFailure> {
843        match &self.admission {
844            BlockingAdmission::Scope(scope) => {
845                if !scope_is_open(scope) {
846                    return Err(AdmissionFailure::ScopeClosed);
847                }
848                let cancellation = scope.cancellation_token();
849                let permit = tokio::select! {
850                    biased;
851                    _ = cancellation.cancelled() => return Err(AdmissionFailure::ScopeClosed),
852                    permit = self.budget.acquire(self.lane, deadline) => {
853                        permit.map_err(|()| AdmissionFailure::QueueDeadlineExpired)?
854                    }
855                };
856                if !scope_is_open(scope) {
857                    drop(permit);
858                    return Err(AdmissionFailure::ScopeClosed);
859                }
860                Ok(permit)
861            }
862            BlockingAdmission::Drain(state) => {
863                if state.effective_deadline().is_expired() {
864                    return Err(AdmissionFailure::LeaseExpired);
865                }
866                let deadline = self.admission.effective_instant(deadline);
867                self.budget
868                    .acquire(self.lane, deadline)
869                    .await
870                    .map_err(|()| AdmissionFailure::QueueDeadlineExpired)
871            }
872            BlockingAdmission::Unscoped => self
873                .budget
874                .acquire(self.lane, deadline)
875                .await
876                .map_err(|()| AdmissionFailure::QueueDeadlineExpired),
877        }
878    }
879
880    /// Samples aggregate state without allocating task names or a detail list.
881    pub(crate) fn aggregate(&self) -> super::BlockingExecutorAggregate {
882        let mut aggregate =
883            super::BlockingExecutorAggregate::new(self.lane, self.policy.max_concurrency, self.policy.max_queue_depth);
884        let now = Instant::now();
885        for entry in self.tasks.iter() {
886            let task = entry.value();
887            aggregate.record_kind(
888                task.kind,
889                now.saturating_duration_since(task.started_at.unwrap_or(task.queued_at)),
890            );
891            match task.state {
892                BlockingTaskState::Queued => aggregate.queued += 1,
893                BlockingTaskState::Running => aggregate.running += 1,
894                BlockingTaskState::TimedOutStillRunning => aggregate.timed_out_still_running += 1,
895                BlockingTaskState::Completed | BlockingTaskState::JoinFailed => {}
896            }
897        }
898        aggregate.blocking_still_running = aggregate.running + aggregate.timed_out_still_running;
899        aggregate
900    }
901
902    /// Returns the snapshot, including task names and individual elapsed times.
903    pub fn snapshot(&self) -> BlockingExecutorSnapshot {
904        let tasks = self
905            .tasks
906            .iter()
907            .map(|entry| entry.value().snapshot())
908            .collect::<Vec<_>>();
909        let queued = tasks
910            .iter()
911            .filter(|task| task.state == BlockingTaskState::Queued)
912            .count();
913        let running = tasks
914            .iter()
915            .filter(|task| task.state == BlockingTaskState::Running)
916            .count();
917        let timed_out_still_running = tasks
918            .iter()
919            .filter(|task| task.state == BlockingTaskState::TimedOutStillRunning)
920            .count();
921        let oldest_queue_wait = tasks
922            .iter()
923            .filter(|task| task.state == BlockingTaskState::Queued)
924            .map(|task| task.elapsed)
925            .max()
926            .unwrap_or(Duration::ZERO);
927        let admission = self.budget.snapshot(self.lane);
928
929        BlockingExecutorSnapshot {
930            name: self.policy.name.clone(),
931            lane: self.lane,
932            max_concurrency: self.policy.max_concurrency,
933            max_queue_depth: self.policy.max_queue_depth,
934            global_capacity: admission.global_capacity,
935            global_running: admission.global_running,
936            global_available: admission.global_available,
937            lane_reserved: admission.lane_reserved,
938            lane_running: admission.lane_running,
939            lane_borrowed: admission.lane_borrowed,
940            queued,
941            running,
942            timed_out_still_running,
943            blocking_still_running: running + timed_out_still_running,
944            rejected: self.rejected.load(Ordering::Relaxed),
945            oldest_queue_wait,
946            tasks,
947        }
948    }
949
950    /// Returns the blocking still running.
951    pub fn blocking_still_running(&self) -> usize {
952        self.tasks
953            .iter()
954            .filter(|entry| {
955                matches!(
956                    entry.value().state,
957                    BlockingTaskState::Running | BlockingTaskState::TimedOutStillRunning
958                )
959            })
960            .count()
961    }
962}
963
964impl BlockingAdmission {
965    fn effective_deadline(&self, deadline: Option<ShutdownDeadline>) -> Option<ShutdownDeadline> {
966        match self {
967            Self::Unscoped => deadline,
968            Self::Scope(scope) => bound_deadline(deadline, scope.shutdown_deadline()),
969            Self::Drain(state) => bound_deadline(deadline, Some(state.effective_deadline())),
970        }
971    }
972
973    fn reserve(&self) -> Result<DrainReservation, AdmissionFailure> {
974        match self {
975            Self::Unscoped => Ok(DrainReservation::none()),
976            Self::Scope(scope) => {
977                if scope_is_open(scope) {
978                    Ok(DrainReservation::none())
979                } else {
980                    Err(AdmissionFailure::ScopeClosed)
981                }
982            }
983            Self::Drain(state) => {
984                if state.effective_deadline().is_expired() {
985                    return Err(AdmissionFailure::LeaseExpired);
986                }
987                let reserved = state
988                    .remaining
989                    .fetch_update(Ordering::AcqRel, Ordering::Acquire, |remaining| {
990                        remaining.checked_sub(1)
991                    });
992                if reserved.is_err() {
993                    return Err(AdmissionFailure::LeaseExhausted);
994                }
995                Ok(DrainReservation {
996                    state: Some(Arc::clone(state)),
997                })
998            }
999        }
1000    }
1001
1002    fn still_valid(&self) -> bool {
1003        match self {
1004            Self::Unscoped => true,
1005            Self::Scope(scope) => {
1006                scope_is_open(scope) && !scope.shutdown_deadline().is_some_and(ShutdownDeadline::is_expired)
1007            }
1008            Self::Drain(state) => !state.effective_deadline().is_expired(),
1009        }
1010    }
1011
1012    fn effective_instant(&self, deadline: Instant) -> Instant {
1013        self.effective_deadline(Some(ShutdownDeadline::at(deadline)))
1014            .map_or(deadline, ShutdownDeadline::instant)
1015    }
1016}
1017
1018fn bound_deadline(
1019    deadline: Option<ShutdownDeadline>,
1020    scope_deadline: Option<ShutdownDeadline>,
1021) -> Option<ShutdownDeadline> {
1022    match (deadline, scope_deadline) {
1023        (Some(deadline), Some(scope_deadline)) => Some(deadline.earliest(scope_deadline)),
1024        (Some(deadline), None) => Some(deadline),
1025        (None, Some(scope_deadline)) => Some(scope_deadline),
1026        (None, None) => None,
1027    }
1028}
1029
1030fn scope_is_open(scope: &TaskGroup) -> bool {
1031    scope.lifecycle_state() == TaskGroupLifecycleState::Open && !scope.cancellation_token().is_cancelled()
1032}
1033
1034fn phase_deadline(started_at: Instant, policy_timeout: Duration, operation_deadline: Instant) -> Instant {
1035    started_at
1036        .checked_add(policy_timeout)
1037        .unwrap_or(operation_deadline)
1038        .min(operation_deadline)
1039}
1040
1041#[cfg(test)]
1042mod tests;