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