Skip to main content

toolkit/
contracts.rs

1use async_trait::async_trait;
2use axum::Router;
3use tokio_util::sync::CancellationToken;
4
5pub use crate::api::openapi_registry::OpenApiRegistry;
6
7/// System capability: receives runtime internals before init.
8///
9/// This trait is internal to toolkit and only used by gears with the "system" capability.
10/// Normal user gears don't implement this.
11#[async_trait]
12pub trait SystemCapability: Send + Sync {
13    /// Optional pre-init hook for system gears.
14    ///
15    /// This runs BEFORE `init()` has completed for ALL gears, and only for system gears.
16    ///
17    /// Default implementation is a no-op so most gears don't need to implement it.
18    ///
19    /// # Errors
20    /// Returns an error if system wiring fails.
21    fn pre_init(&self, _sys: &crate::runtime::SystemContext) -> anyhow::Result<()> {
22        Ok(())
23    }
24
25    /// Optional post-init hook for system gears.
26    ///
27    /// This runs AFTER `init()` has completed for ALL gears, and only for system gears.
28    ///
29    /// Default implementation is a no-op so most gears don't need to implement it.
30    async fn post_init(&self, _sys: &crate::runtime::SystemContext) -> anyhow::Result<()> {
31        Ok(())
32    }
33}
34
35/// Core gear: DI/wiring.
36///
37/// For a gear with a configured database handle, DB migrations have already
38/// run, so such a gear may rely on its own migrated schema here. Gears without
39/// a configured database get no such guarantee. Use `init` to build the gear's
40/// own state and register its own
41/// client(s). Do **not** call other gears here: consumed-contract clients are
42/// wired *after* all `init`s complete, so resolve them lazily on first use or in
43/// a later phase.
44#[async_trait]
45pub trait Gear: Send + Sync + 'static {
46    async fn init(&self, ctx: &crate::context::GearCtx) -> anyhow::Result<()>;
47}
48
49/// Database capability: gears provide migrations, runtime executes them.
50///
51/// # Security
52///
53/// Gears MUST NOT receive raw database connections. They only return migration definitions.
54#[cfg(feature = "db")]
55pub trait DatabaseCapability: Send + Sync {
56    fn migrations(&self) -> Vec<Box<dyn sea_orm_migration::MigrationTrait>>;
57}
58
59/// REST API capability: Pure wiring; must be sync. Runs AFTER DB migrations.
60pub trait RestApiCapability: Send + Sync {
61    /// Register REST routes for this gear.
62    ///
63    /// # Errors
64    /// Returns an error if route registration fails.
65    fn register_rest(
66        &self,
67        ctx: &crate::context::GearCtx,
68        router: Router,
69        openapi: &dyn OpenApiRegistry,
70    ) -> anyhow::Result<Router>;
71
72    /// Readiness healthcheck for this gear, run on every `/readyz` and `/health`.
73    /// `None` (default) opts out.
74    ///
75    /// Return **one composite check per gear** that aggregates the gear's critical
76    /// dependencies, rather than many fine-grained probes — the aggregate is what decides
77    /// whether the pod stays in traffic. External SaaS/LLM dependencies should usually **not**
78    /// gate readiness: a provider outage would evict every pod from rotation and take the
79    /// whole service down. Report such dependencies as `degraded` (kept in rotation), reserving
80    /// `unhealthy` for failures the gear itself cannot serve through.
81    fn healthcheck(
82        &self,
83        _ctx: &crate::context::GearCtx,
84    ) -> Option<std::sync::Arc<dyn crate::healthcheck::Healthcheck>> {
85        None
86    }
87}
88
89/// API Gateway capability: handles gateway hosting with prepare/finalize phases.
90/// Must be sync. Runs during REST phase, but doesn't start the server.
91#[allow(dead_code)]
92pub trait ApiGatewayCapability: Send + Sync + 'static {
93    /// Prepare a base Router (e.g., global middlewares) and optionally touch `OpenAPI` meta.
94    /// Do NOT start the server here. `hc_registry` is the runtime's shared healthcheck
95    /// registry, passed explicitly (not via `ClientHub`, which is for inter-gear clients).
96    ///
97    /// # Errors
98    /// Returns an error if router preparation fails.
99    fn rest_prepare(
100        &self,
101        ctx: &crate::context::GearCtx,
102        router: Router,
103        hc_registry: std::sync::Arc<crate::healthcheck::RestHealthcheckRegistry>,
104    ) -> anyhow::Result<Router>;
105
106    /// Finalize before start: attach /openapi.json, /docs, health endpoints, persist the
107    /// Router internally if needed. Do NOT start the server here.
108    ///
109    /// `hc_registry` is the same registry instance passed to [`rest_prepare`](Self::rest_prepare).
110    ///
111    /// # Errors
112    /// Returns an error if router finalization fails.
113    fn rest_finalize(
114        &self,
115        ctx: &crate::context::GearCtx,
116        router: Router,
117        hc_registry: std::sync::Arc<crate::healthcheck::RestHealthcheckRegistry>,
118    ) -> anyhow::Result<Router>;
119
120    // Return OpenAPI registry of the gear, e.g., to register endpoints
121    fn as_registry(&self) -> &dyn OpenApiRegistry;
122
123    /// The bound REST base URL (e.g. `http://127.0.0.1:8080`) once the gateway's
124    /// server is listening, or `None` before the listener binds.
125    ///
126    /// Mirrors [`GrpcHubCapability::bound_endpoint`]. The runtime polls this in
127    /// its directory-register phase to advertise REST providers in the service
128    /// directory. Defaults to `None` for hosts that don't expose an address.
129    fn bound_endpoint(&self) -> Option<String> {
130        None
131    }
132}
133
134/// Capability for gears that have a long-running background task.
135///
136/// # Shutdown Contract
137///
138/// The `stop` method receives a **deadline token** that implements two-phase shutdown:
139///
140/// 1. **Graceful stop request**: When `stop(deadline_token)` is called, the `deadline_token`
141///    is *not* cancelled. This is the signal to begin graceful shutdown.
142///
143/// 2. **Hard-stop deadline**: After the runtime's `shutdown_deadline` expires (default 35s),
144///    the `deadline_token` is cancelled. This signals that graceful shutdown time is over
145///    and the gear should abort immediately.
146///
147/// ## Recommended Implementation Pattern
148///
149/// ```ignore
150/// async fn stop(&self, deadline_token: CancellationToken) -> anyhow::Result<()> {
151///     // 1. Request cooperative shutdown of child tasks
152///     self.request_graceful_shutdown();
153///
154///     // 2. Wait for graceful completion OR hard-stop deadline
155///     tokio::select! {
156///         _ = self.wait_for_graceful_completion() => {
157///             // Graceful shutdown succeeded
158///         }
159///         _ = deadline_token.cancelled() => {
160///             // Deadline reached, force abort
161///             self.force_abort();
162///         }
163///     }
164///     Ok(())
165/// }
166/// ```
167///
168/// ## Important Notes
169///
170/// - The `deadline_token` passed to `stop()` is a **fresh token**, not the root cancellation
171///   token that triggered the shutdown. This allows gears to implement real graceful shutdown.
172/// - Gears should NOT assume the token is already cancelled when `stop()` is called.
173/// - The `WithLifecycle` wrapper handles this contract automatically via its `stop_timeout`.
174#[async_trait]
175pub trait RunnableCapability: Send + Sync {
176    /// Start the gear's background task.
177    ///
178    /// The `cancel` token is a child of the runtime's root cancellation token.
179    /// When cancelled, the gear should stop its background work.
180    async fn start(&self, cancel: CancellationToken) -> anyhow::Result<()>;
181
182    /// Stop the gear's background task.
183    ///
184    /// The `deadline_token` implements two-phase shutdown:
185    /// - Initially not cancelled: begin graceful shutdown
186    /// - When cancelled: graceful period expired, abort immediately
187    ///
188    /// See trait-level documentation for the full shutdown contract.
189    async fn stop(&self, deadline_token: CancellationToken) -> anyhow::Result<()>;
190}
191
192/// Represents a gRPC service registration callback used by the gRPC hub.
193///
194/// Each gear that exposes gRPC services provides one or more of these.
195/// The `register` closure adds the service into the provided `RoutesBuilder`.
196#[cfg(feature = "otel")]
197pub struct RegisterGrpcServiceFn {
198    pub service_name: &'static str,
199    pub register: Box<dyn Fn(&mut tonic::service::RoutesBuilder) + Send + Sync>,
200}
201
202#[cfg(not(feature = "otel"))]
203pub struct RegisterGrpcServiceFn {
204    pub service_name: &'static str,
205}
206
207/// gRPC Service capability: gears that export gRPC services.
208///
209/// The runtime will call this during the gRPC registration phase to collect
210/// all services that should be exposed on the shared gRPC server.
211#[async_trait]
212pub trait GrpcServiceCapability: Send + Sync {
213    /// Returns all gRPC services this gear wants to expose.
214    ///
215    /// Each installer adds one service to the `tonic::Server` builder.
216    async fn get_grpc_services(
217        &self,
218        ctx: &crate::context::GearCtx,
219    ) -> anyhow::Result<Vec<RegisterGrpcServiceFn>>;
220}
221
222/// gRPC Hub capability: hosts the gRPC server.
223///
224/// This trait is implemented by the single gear responsible for hosting
225/// the `tonic::Server` instance. Only one gear per process should implement this.
226pub trait GrpcHubCapability: Send + Sync {
227    /// Returns the bound endpoint after the server starts listening.
228    ///
229    /// Examples:
230    /// - TCP: `http://127.0.0.1:50652`
231    /// - Unix socket: `unix:///path/to/socket`
232    /// - Named pipe: `pipe://\\.\pipe\name`
233    ///
234    /// Returns `None` if the server hasn't started listening yet.
235    fn bound_endpoint(&self) -> Option<String>;
236
237    /// Whether the hub has settled on binding nothing (`listen_addr: none`, or
238    /// no services to serve), so [`bound_endpoint`](Self::bound_endpoint)
239    /// stays `None` for good.
240    fn serves_no_endpoint(&self) -> bool {
241        false
242    }
243}