//! Tower service for dispatching CPU-bound work to the tokio blocking thread pool with
//! cooperative cancellation.
//!
//! Use this crate to move expensive synchronous work — query planning, query parsing, image
//! resizing, anything that holds a worker thread for more than a few microseconds — off the
//! async executor. The async runtime stays responsive while the work runs on a dedicated
//! blocking thread, and cancellation propagates cooperatively into the blocking code so
//! timeouts and client disconnects do not leave ghost threads behind.
//!
//! # Quick start
//!
//! [`ServiceBuilderExt::blocking_service`] terminates a tower middleware chain with a closure
//! or [`BlockingService`]. Implementations receive a cancellation token that you should check
//! regularly to support cooperative cancellation.
//!
//! **Important:** always pair `blocking_service` with a concurrency-limit layer; see
//! [Pool exhaustion](#pool-exhaustion) below.
//!
//! ```
//! use apollo_blocking::{CancellationToken, ServiceBuilderExt};
//! use apollo_qos::concurrency_limit::ConcurrencyLimitLayer;
//! use std::convert::Infallible;
//! use std::num::NonZeroUsize;
//! use tower::ServiceBuilder;
//!
//! let svc = ServiceBuilder::new()
//!     .layer(ConcurrencyLimitLayer::new(
//!         "query_planning",
//!         NonZeroUsize::new(32).unwrap(),
//!     ))
//!     .blocking_service("query_planning", None, |req: u32, ct: CancellationToken| {
//!         // Poll ct.is_cancelled() at natural yield points to abort cooperatively.
//!         if ct.is_cancelled() { return Err(Cancelled); }
//!         Ok::<_, Cancelled>(req * 2)
//!     });
//!
//! # #[derive(Debug)] struct Cancelled;
//! # impl std::fmt::Display for Cancelled {
//! #     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("cancelled") }
//! # }
//! # impl std::error::Error for Cancelled {}
//! # let _ = svc;
//! ```
//!
//! # Graceful shutdown
//!
//! Supply a parent [`CancellationToken`] when you need external cancellation — typically a
//! shutdown token shared across the whole process — to propagate into in-flight blocking tasks
//! without waiting for their futures to be dropped.
//!
//! ```
//! use apollo_blocking::{CancellationToken, ServiceBuilderExt};
//! use std::convert::Infallible;
//! use tower::ServiceBuilder;
//!
//! let shutdown = CancellationToken::new();
//!
//! let svc = ServiceBuilder::new()
//!     .blocking_service(
//!         "query_planning",
//!         Some(shutdown.clone()),
//!         |req: u32, _ct: CancellationToken| -> Result<u32, Infallible> { Ok(req * 2) },
//!     );
//!
//! // On shutdown: cancel the token before dropping the futures so in-flight work can return early.
//! shutdown.cancel();
//! # let _ = svc;
//! ```
//!
//! # Cancellation
//!
//! Cancellation is cooperative — blocking code must poll `ct.is_cancelled()` at natural yield
//! points (loop iterations, between sub-steps) to abort. The runtime cannot abort a blocking
//! thread once it is running.
//!
//! Three cancellation paths are wired automatically once the adapter is constructed:
//!
//! - **Future drop.** When the future returned from a call is dropped before it resolves —
//!   the most common case, triggered by timeout layers, dropped responses, or client
//!   disconnects — the per-call child token is cancelled by an embedded drop guard.
//! - **Parent token.** When a parent token supplied at construction is cancelled, the child
//!   token derived from it cancels too, signalling all in-flight tasks at once.
//! - **No parent token.** A standalone child token is created per call. The drop guard still
//!   covers it; future drop remains observable.
//!
//! In all three paths the in-flight blocking task is expected to return on its next poll of
//! `ct.is_cancelled()`. The adapter records an `apollo.blocking.task.cancelled` span event
//! whenever cancellation fires before the task naturally completes.
//!
//! # Observability
//!
//! Every call emits one span and one histogram measurement. Filter by the constructor `name`
//! to attribute trace and metric data to a specific blocking workload running in the process.
//!
//! ## Span
//!
//! | Name | Attributes | Events |
//! |---|---|---|
//! | `apollo.blocking.task` | `apollo.blocking.name`, `apollo.blocking.result` (one of `success`, `cancelled`, `panicked`) | `apollo.blocking.task.cancelled`, `apollo.blocking.task.panicked` |
//!
//! The `cancelled` event fires when the per-call child token is cancelled before the task
//! completes — typically by future drop, a timeout layer, or a parent token. The `panicked`
//! event fires when the blocking closure panics.
//!
//! ## Metric
//!
//! | Name | Type | Unit | Labels |
//! |---|---|---|---|
//! | `apollo.blocking.task.duration` | histogram | `s` | `name`, `result` |
//!
//! The histogram records wall-clock duration from `call()` invocation to task completion or
//! cancellation. Use its built-in count for task volume — `count(apollo.blocking.task.duration)`
//! filtered by `result` gives task throughput per outcome with no extra metric required.
//!
//! # Panics
//!
//! A panic in the user closure propagates to the awaiting task: when the response future is
//! polled to completion, the panic is resumed on the caller's task. Panics are also recorded
//! as `result=panicked` on the `apollo.blocking.task.duration` histogram and as an
//! `apollo.blocking.task.panicked` span event before propagation.
//!
//! If the response future is dropped before the task completes — a timeout fires, the client
//! disconnects, the caller cancels — the panic is never resumed because no one is awaiting
//! the result. The default panic hook still prints to stderr and the panicked outcome is
//! still recorded in telemetry, but no structured payload reaches the application logging
//! stack.
//!
//! Install a custom panic hook via [`std::panic::set_hook`] early in `main`, before any
//! blocking task is dispatched, and pair it with a top-level panic-catching layer (e.g.
//! `tower_http::catch_panic::CatchPanicLayer`) at the entry point of your service. The hook
//! captures every panic for structured logging; the catch layer keeps the runtime task alive
//! when a panic does propagate.
//!
//! `apollo-blocking task aborted: …` is a separate panic emitted only when the runtime is
//! dropped with blocking work still queued. Let queued work drain before dropping the
//! runtime to avoid it.
//!
//! # Pool exhaustion
//!
//! [`tokio::task::spawn_blocking`] draws from a shared, runtime-wide thread pool (default cap:
//! 512 threads). A blocking workload that runs unbounded — many concurrent expensive
//! operations dispatched at once — can starve other blocking work elsewhere in the same
//! process or saturate memory.
//!
//! **Always pair `blocking_service` with [`apollo_qos::concurrency_limit::ConcurrencyLimitLayer`]**,
//! sized to the number of concurrent blocking operations the workload can sustain without
//! degrading async I/O latency. A starting point of 16–64 is reasonable for most CPU-bound
//! workloads; tune using the `apollo.blocking.task.duration` histogram filtered on the `name`
//! label.
//!
//! ```
//! use apollo_blocking::{CancellationToken, ServiceBuilderExt};
//! use apollo_qos::concurrency_limit::ConcurrencyLimitLayer;
//! use std::convert::Infallible;
//! use std::num::NonZeroUsize;
//! use tower::ServiceBuilder;
//!
//! let svc = ServiceBuilder::new()
//!     .layer(ConcurrencyLimitLayer::new(
//!         "query_planning",
//!         NonZeroUsize::new(32).unwrap(),
//!     ))
//!     .blocking_service("query_planning", None, |req: u32, _ct: CancellationToken| {
//!         Ok::<_, Infallible>(req * 2)
//!     });
//! # let _ = svc;
//! ```
//!
//! [`apollo_qos::concurrency_limit::ConcurrencyLimitLayer`]: https://docs.rs/apollo-qos

#![forbid(unsafe_code)]
#![warn(missing_docs)]

mod adapter;
mod builder;
mod instruments;
mod service;

pub use adapter::{BlockingFuture, BlockingServiceAdapter};
pub use builder::ServiceBuilderExt;
pub use service::BlockingService;
pub use tokio_util::sync::CancellationToken;
