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((status = 200, description = "Heartbeat recorded")),
99)]
100pub async fn worker_heartbeat<S: WorkflowStore>(
101 State(state): State<Arc<WorkflowCtx<S>>>,
102 Json(req): Json<HeartbeatRequest>,
103) -> Result<axum::http::StatusCode, AppError> {
104 state.heartbeat_worker(&req.worker_id).await?;
105 Ok(axum::http::StatusCode::OK)
106}
107
108#[derive(Deserialize, ToSchema)]
109pub struct PollRequest {
110 pub queue: String,
112 pub worker_id: String,
114}
115
116#[utoipa::path(
117 post, path = "/api/v1/engine/workflow/tasks/poll",
118 tag = "tasks",
119 request_body = PollRequest,
120 responses(
121 (status = 200, description = "Activity task (or null if none available)", body = WorkflowActivity),
122 ),
123)]
124pub async fn poll_task<S: WorkflowStore>(
125 State(state): State<Arc<WorkflowCtx<S>>>,
126 Json(req): Json<PollRequest>,
127) -> Result<Json<serde_json::Value>, AppError> {
128 let activity = state
129 .claim_activity(&req.queue, &req.worker_id)
130 .await?;
131
132 match activity {
133 Some(act) => Ok(Json(serde_json::to_value(act)?)),
134 None => Ok(Json(serde_json::json!({ "task": null }))),
135 }
136}
137
138#[derive(Deserialize, ToSchema)]
139pub struct CompleteTaskBody {
140 pub result: Option<serde_json::Value>,
142}
143
144#[utoipa::path(
145 post, path = "/api/v1/engine/workflow/tasks/{id}/complete",
146 tag = "tasks",
147 params(("id" = i64, Path, description = "Activity task ID")),
148 request_body = CompleteTaskBody,
149 responses((status = 200, description = "Task completed")),
150)]
151pub async fn complete_task<S: WorkflowStore>(
152 State(state): State<Arc<WorkflowCtx<S>>>,
153 Path(id): Path<i64>,
154 Json(body): Json<CompleteTaskBody>,
155) -> Result<axum::http::StatusCode, AppError> {
156 let result = body.result.map(|v| v.to_string());
157 state
158 .complete_activity(id, result.as_deref(), None, false)
159 .await?;
160 Ok(axum::http::StatusCode::OK)
161}
162
163#[derive(Deserialize, ToSchema)]
164pub struct FailTaskBody {
165 pub error: String,
167}
168
169#[utoipa::path(
170 post, path = "/api/v1/engine/workflow/tasks/{id}/fail",
171 tag = "tasks",
172 params(("id" = i64, Path, description = "Activity task ID")),
173 request_body = FailTaskBody,
174 responses((status = 200, description = "Task marked as failed")),
175)]
176pub async fn fail_task<S: WorkflowStore>(
177 State(state): State<Arc<WorkflowCtx<S>>>,
178 Path(id): Path<i64>,
179 Json(body): Json<FailTaskBody>,
180) -> Result<axum::http::StatusCode, AppError> {
181 state.fail_activity(id, &body.error).await?;
185 Ok(axum::http::StatusCode::OK)
186}
187
188#[derive(Deserialize, ToSchema)]
189pub struct HeartbeatTaskBody {
190 pub details: Option<String>,
191}
192
193#[utoipa::path(
194 post, path = "/api/v1/engine/workflow/tasks/{id}/heartbeat",
195 tag = "tasks",
196 params(("id" = i64, Path, description = "Activity task ID")),
197 responses((status = 200, description = "Heartbeat recorded")),
198)]
199pub async fn heartbeat_task<S: WorkflowStore>(
200 State(state): State<Arc<WorkflowCtx<S>>>,
201 Path(id): Path<i64>,
202 Json(body): Json<HeartbeatTaskBody>,
203) -> Result<axum::http::StatusCode, AppError> {
204 state
205 .heartbeat_activity(id, body.details.as_deref())
206 .await?;
207 Ok(axum::http::StatusCode::OK)
208}
209
210fn timestamp_now() -> f64 {
211 std::time::SystemTime::now()
212 .duration_since(std::time::UNIX_EPOCH)
213 .unwrap()
214 .as_secs_f64()
215}
216
217fn uuid_short() -> String {
218 use std::collections::hash_map::DefaultHasher;
219 use std::hash::{Hash, Hasher};
220 let mut h = DefaultHasher::new();
221 std::time::SystemTime::now().hash(&mut h);
222 std::thread::current().id().hash(&mut h);
223 format!("{:016x}", h.finish())
224}
225
226use crate::types::WorkflowActivity;