1use std::sync::Arc;
2
3use axum::extract::{Path, State};
4use axum::routing::post;
5use axum::{Json, Router};
6use serde::{Deserialize, Serialize};
7use utoipa::ToSchema;
8
9use crate::api::workflows::AppError;
10use crate::ctx::WorkflowCtx;
11use crate::store::WorkflowStore;
12use crate::types::WorkflowWorker;
13
14pub fn router<S: WorkflowStore + 'static>() -> Router<Arc<WorkflowCtx<S>>> {
15 Router::new()
16 .route("/workers/register", post(register_worker))
17 .route("/workers/heartbeat", post(worker_heartbeat))
18 .route("/tasks/poll", post(poll_task))
19 .route("/tasks/{id}/complete", post(complete_task))
20 .route("/tasks/{id}/fail", post(fail_task))
21 .route("/tasks/{id}/heartbeat", post(heartbeat_task))
22}
23
24#[derive(Deserialize, ToSchema)]
25pub struct RegisterWorkerRequest {
26 #[serde(default = "default_namespace")]
28 pub namespace: String,
29 pub identity: String,
31 pub queue: String,
33 pub workflows: Option<Vec<String>>,
35 pub activities: Option<Vec<String>>,
37 #[serde(default = "default_concurrent")]
38 pub max_concurrent_workflows: i32,
39 #[serde(default = "default_concurrent")]
40 pub max_concurrent_activities: i32,
41}
42
43fn default_namespace() -> String {
44 "main".to_string()
45}
46
47fn default_concurrent() -> i32 {
48 10
49}
50
51#[derive(Serialize, ToSchema)]
52pub struct RegisterWorkerResponse {
53 pub worker_id: String,
55}
56
57#[utoipa::path(
58 post, path = "/api/v1/engine/workflow/workers/register",
59 tag = "tasks",
60 request_body = RegisterWorkerRequest,
61 responses(
62 (status = 200, description = "Worker registered", body = RegisterWorkerResponse),
63 ),
64)]
65pub async fn register_worker<S: WorkflowStore>(
66 State(state): State<Arc<WorkflowCtx<S>>>,
67 Json(req): Json<RegisterWorkerRequest>,
68) -> Result<Json<RegisterWorkerResponse>, AppError> {
69 let now = timestamp_now();
70 let worker_id = format!("w-{}", uuid_short());
71
72 let worker = WorkflowWorker {
73 id: worker_id.clone(),
74 namespace: req.namespace,
75 identity: req.identity,
76 task_queue: req.queue,
77 workflows: req.workflows.map(|v| serde_json::to_string(&v).unwrap()),
78 activities: req.activities.map(|v| serde_json::to_string(&v).unwrap()),
79 max_concurrent_workflows: req.max_concurrent_workflows,
80 max_concurrent_activities: req.max_concurrent_activities,
81 active_tasks: 0,
82 last_heartbeat: now,
83 registered_at: now,
84 };
85
86 state.register_worker(&worker).await?;
87 Ok(Json(RegisterWorkerResponse { worker_id }))
88}
89
90#[derive(Deserialize, ToSchema)]
91pub struct HeartbeatRequest {
92 pub worker_id: String,
93}
94
95#[utoipa::path(
96 post, path = "/api/v1/engine/workflow/workers/heartbeat",
97 tag = "tasks",
98 responses(
99 (status = 200, description = "Heartbeat recorded"),
100 (status = 404, description = "Registration is gone — register again"),
101 ),
102)]
103pub async fn worker_heartbeat<S: WorkflowStore>(
104 State(state): State<Arc<WorkflowCtx<S>>>,
105 Json(req): Json<HeartbeatRequest>,
106) -> Result<axum::http::StatusCode, AppError> {
107 if state.heartbeat_worker(&req.worker_id).await? {
112 Ok(axum::http::StatusCode::OK)
113 } else {
114 Ok(axum::http::StatusCode::NOT_FOUND)
115 }
116}
117
118#[derive(Deserialize, ToSchema)]
119pub struct PollRequest {
120 pub queue: String,
122 pub worker_id: String,
124}
125
126#[utoipa::path(
127 post, path = "/api/v1/engine/workflow/tasks/poll",
128 tag = "tasks",
129 request_body = PollRequest,
130 responses(
131 (status = 200, description = "Activity task (or null if none available)", body = WorkflowActivity),
132 ),
133)]
134pub async fn poll_task<S: WorkflowStore>(
135 State(state): State<Arc<WorkflowCtx<S>>>,
136 Json(req): Json<PollRequest>,
137) -> Result<Json<serde_json::Value>, AppError> {
138 let activity = state.claim_activity(&req.queue, &req.worker_id).await?;
139
140 match activity {
141 Some(act) => Ok(Json(serde_json::to_value(act)?)),
142 None => Ok(Json(serde_json::json!({ "task": null }))),
143 }
144}
145
146#[derive(Deserialize, ToSchema)]
147pub struct CompleteTaskBody {
148 pub result: Option<serde_json::Value>,
150}
151
152#[utoipa::path(
153 post, path = "/api/v1/engine/workflow/tasks/{id}/complete",
154 tag = "tasks",
155 params(("id" = i64, Path, description = "Activity task ID")),
156 request_body = CompleteTaskBody,
157 responses((status = 200, description = "Task completed")),
158)]
159pub async fn complete_task<S: WorkflowStore>(
160 State(state): State<Arc<WorkflowCtx<S>>>,
161 Path(id): Path<i64>,
162 Json(body): Json<CompleteTaskBody>,
163) -> Result<axum::http::StatusCode, AppError> {
164 let result = body.result.map(|v| v.to_string());
165 state
166 .complete_activity(id, result.as_deref(), None, false)
167 .await?;
168 Ok(axum::http::StatusCode::OK)
169}
170
171#[derive(Deserialize, ToSchema)]
172pub struct FailTaskBody {
173 pub error: String,
175}
176
177#[utoipa::path(
178 post, path = "/api/v1/engine/workflow/tasks/{id}/fail",
179 tag = "tasks",
180 params(("id" = i64, Path, description = "Activity task ID")),
181 request_body = FailTaskBody,
182 responses((status = 200, description = "Task marked as failed")),
183)]
184pub async fn fail_task<S: WorkflowStore>(
185 State(state): State<Arc<WorkflowCtx<S>>>,
186 Path(id): Path<i64>,
187 Json(body): Json<FailTaskBody>,
188) -> Result<axum::http::StatusCode, AppError> {
189 state.fail_activity(id, &body.error).await?;
193 Ok(axum::http::StatusCode::OK)
194}
195
196#[derive(Deserialize, ToSchema)]
197pub struct HeartbeatTaskBody {
198 pub details: Option<String>,
199}
200
201#[utoipa::path(
202 post, path = "/api/v1/engine/workflow/tasks/{id}/heartbeat",
203 tag = "tasks",
204 params(("id" = i64, Path, description = "Activity task ID")),
205 responses((status = 200, description = "Heartbeat recorded")),
206)]
207pub async fn heartbeat_task<S: WorkflowStore>(
208 State(state): State<Arc<WorkflowCtx<S>>>,
209 Path(id): Path<i64>,
210 Json(body): Json<HeartbeatTaskBody>,
211) -> Result<axum::http::StatusCode, AppError> {
212 state
213 .heartbeat_activity(id, body.details.as_deref())
214 .await?;
215 Ok(axum::http::StatusCode::OK)
216}
217
218fn timestamp_now() -> f64 {
219 std::time::SystemTime::now()
220 .duration_since(std::time::UNIX_EPOCH)
221 .unwrap()
222 .as_secs_f64()
223}
224
225fn uuid_short() -> String {
226 use std::collections::hash_map::DefaultHasher;
227 use std::hash::{Hash, Hasher};
228 let mut h = DefaultHasher::new();
229 std::time::SystemTime::now().hash(&mut h);
230 std::thread::current().id().hash(&mut h);
231 format!("{:016x}", h.finish())
232}
233
234use crate::types::WorkflowActivity;