modkit/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 modkit and only used by modules with the "system" capability.
10/// Normal user modules don't implement this.
11#[async_trait]
12pub trait SystemCapability: Send + Sync {
13 /// Optional pre-init hook for system modules.
14 ///
15 /// This runs BEFORE `init()` has completed for ALL modules, and only for system modules.
16 ///
17 /// Default implementation is a no-op so most modules 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 modules.
26 ///
27 /// This runs AFTER `init()` has completed for ALL modules, and only for system modules.
28 ///
29 /// Default implementation is a no-op so most modules 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 module: DI/wiring; do not rely on migrated schema here.
36#[async_trait]
37pub trait Module: Send + Sync + 'static {
38 async fn init(&self, ctx: &crate::context::ModuleCtx) -> anyhow::Result<()>;
39}
40
41/// Database capability: modules provide migrations, runtime executes them.
42///
43/// # Security
44///
45/// Modules 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 module.
54 ///
55 /// # Errors
56 /// Returns an error if route registration fails.
57 fn register_rest(
58 &self,
59 ctx: &crate::context::ModuleCtx,
60 router: Router,
61 openapi: &dyn OpenApiRegistry,
62 ) -> anyhow::Result<Router>;
63}
64
65/// API Gateway capability: handles gateway hosting with prepare/finalize phases.
66/// Must be sync. Runs during REST phase, but doesn't start the server.
67#[allow(dead_code)]
68pub trait ApiGatewayCapability: Send + Sync + 'static {
69 /// Prepare a base Router (e.g., global middlewares, /healthz) and optionally touch `OpenAPI` meta.
70 /// Do NOT start the server here.
71 ///
72 /// # Errors
73 /// Returns an error if router preparation fails.
74 fn rest_prepare(
75 &self,
76 ctx: &crate::context::ModuleCtx,
77 router: Router,
78 ) -> anyhow::Result<Router>;
79
80 /// Finalize before start: attach /openapi.json, /docs, persist the Router internally if needed.
81 /// Do NOT start the server here.
82 ///
83 /// # Errors
84 /// Returns an error if router finalization fails.
85 fn rest_finalize(
86 &self,
87 ctx: &crate::context::ModuleCtx,
88 router: Router,
89 ) -> anyhow::Result<Router>;
90
91 // Return OpenAPI registry of the module, e.g., to register endpoints
92 fn as_registry(&self) -> &dyn OpenApiRegistry;
93}
94
95/// Capability for modules that have a long-running background task.
96///
97/// # Shutdown Contract
98///
99/// The `stop` method receives a **deadline token** that implements two-phase shutdown:
100///
101/// 1. **Graceful stop request**: When `stop(deadline_token)` is called, the `deadline_token`
102/// is *not* cancelled. This is the signal to begin graceful shutdown.
103///
104/// 2. **Hard-stop deadline**: After the runtime's `shutdown_deadline` expires (default 30s),
105/// the `deadline_token` is cancelled. This signals that graceful shutdown time is over
106/// and the module should abort immediately.
107///
108/// ## Recommended Implementation Pattern
109///
110/// ```ignore
111/// async fn stop(&self, deadline_token: CancellationToken) -> anyhow::Result<()> {
112/// // 1. Request cooperative shutdown of child tasks
113/// self.request_graceful_shutdown();
114///
115/// // 2. Wait for graceful completion OR hard-stop deadline
116/// tokio::select! {
117/// _ = self.wait_for_graceful_completion() => {
118/// // Graceful shutdown succeeded
119/// }
120/// _ = deadline_token.cancelled() => {
121/// // Deadline reached, force abort
122/// self.force_abort();
123/// }
124/// }
125/// Ok(())
126/// }
127/// ```
128///
129/// ## Important Notes
130///
131/// - The `deadline_token` passed to `stop()` is a **fresh token**, not the root cancellation
132/// token that triggered the shutdown. This allows modules to implement real graceful shutdown.
133/// - Modules should NOT assume the token is already cancelled when `stop()` is called.
134/// - The `WithLifecycle` wrapper handles this contract automatically via its `stop_timeout`.
135#[async_trait]
136pub trait RunnableCapability: Send + Sync {
137 /// Start the module's background task.
138 ///
139 /// The `cancel` token is a child of the runtime's root cancellation token.
140 /// When cancelled, the module should stop its background work.
141 async fn start(&self, cancel: CancellationToken) -> anyhow::Result<()>;
142
143 /// Stop the module's background task.
144 ///
145 /// The `deadline_token` implements two-phase shutdown:
146 /// - Initially not cancelled: begin graceful shutdown
147 /// - When cancelled: graceful period expired, abort immediately
148 ///
149 /// See trait-level documentation for the full shutdown contract.
150 async fn stop(&self, deadline_token: CancellationToken) -> anyhow::Result<()>;
151}
152
153/// Represents a gRPC service registration callback used by the gRPC hub.
154///
155/// Each module that exposes gRPC services provides one or more of these.
156/// The `register` closure adds the service into the provided `RoutesBuilder`.
157#[cfg(feature = "otel")]
158pub struct RegisterGrpcServiceFn {
159 pub service_name: &'static str,
160 pub register: Box<dyn Fn(&mut tonic::service::RoutesBuilder) + Send + Sync>,
161}
162
163#[cfg(not(feature = "otel"))]
164pub struct RegisterGrpcServiceFn {
165 pub service_name: &'static str,
166}
167
168/// gRPC Service capability: modules that export gRPC services.
169///
170/// The runtime will call this during the gRPC registration phase to collect
171/// all services that should be exposed on the shared gRPC server.
172#[async_trait]
173pub trait GrpcServiceCapability: Send + Sync {
174 /// Returns all gRPC services this module wants to expose.
175 ///
176 /// Each installer adds one service to the `tonic::Server` builder.
177 async fn get_grpc_services(
178 &self,
179 ctx: &crate::context::ModuleCtx,
180 ) -> anyhow::Result<Vec<RegisterGrpcServiceFn>>;
181}
182
183/// gRPC Hub capability: hosts the gRPC server.
184///
185/// This trait is implemented by the single module responsible for hosting
186/// the `tonic::Server` instance. Only one module per process should implement this.
187pub trait GrpcHubCapability: Send + Sync {
188 /// Returns the bound endpoint after the server starts listening.
189 ///
190 /// Examples:
191 /// - TCP: `http://127.0.0.1:50652`
192 /// - Unix socket: `unix:///path/to/socket`
193 /// - Named pipe: `pipe://\\.\pipe\name`
194 ///
195 /// Returns `None` if the server hasn't started listening yet.
196 fn bound_endpoint(&self) -> Option<String>;
197}