1use crate::engine::{ContractAnnouncement, EngineError, RegisterWorker};
9use crate::model::TaskError;
10use crate::server::{
11 authorize_session, authorize_token, engine, identify, ApiError, Caller, Shared,
12};
13use serde_json::{Map, Number, Value as Json};
14use std::time::Duration;
15use tonic::{Request, Response, Status};
16
17pub mod pb {
19 tonic::include_proto!("orbit.worker.v1");
20}
21
22use pb::worker_service_server::{WorkerService, WorkerServiceServer};
23
24pub fn to_json(v: prost_types::Value) -> Json {
29 use prost_types::value::Kind;
30 match v.kind {
31 None | Some(Kind::NullValue(_)) => Json::Null,
32 Some(Kind::BoolValue(b)) => Json::Bool(b),
33 Some(Kind::NumberValue(n)) => {
34 if n.fract() == 0.0 && n.abs() <= 9_007_199_254_740_992.0 {
35 Json::from(n as i64)
36 } else {
37 Number::from_f64(n).map(Json::Number).unwrap_or(Json::Null)
38 }
39 }
40 Some(Kind::StringValue(s)) => Json::String(s),
41 Some(Kind::ListValue(l)) => Json::Array(l.values.into_iter().map(to_json).collect()),
42 Some(Kind::StructValue(s)) => Json::Object(
43 s.fields
44 .into_iter()
45 .map(|(k, v)| (k, to_json(v)))
46 .collect::<Map<_, _>>(),
47 ),
48 }
49}
50
51pub fn from_json(v: Json) -> prost_types::Value {
53 use prost_types::value::Kind;
54 let kind = match v {
55 Json::Null => Kind::NullValue(0),
56 Json::Bool(b) => Kind::BoolValue(b),
57 Json::Number(n) => Kind::NumberValue(n.as_f64().unwrap_or(0.0)),
58 Json::String(s) => Kind::StringValue(s),
59 Json::Array(a) => Kind::ListValue(prost_types::ListValue {
60 values: a.into_iter().map(from_json).collect(),
61 }),
62 Json::Object(o) => Kind::StructValue(prost_types::Struct {
63 fields: o.into_iter().map(|(k, v)| (k, from_json(v))).collect(),
64 }),
65 };
66 prost_types::Value { kind: Some(kind) }
67}
68
69fn status(e: EngineError) -> Status {
70 let code = match e.status {
71 400 => tonic::Code::InvalidArgument,
72 401 => tonic::Code::Unauthenticated,
73 403 => tonic::Code::PermissionDenied,
74 404 => tonic::Code::NotFound,
75 409 => tonic::Code::FailedPrecondition,
76 413 | 429 => tonic::Code::ResourceExhausted,
77 _ => tonic::Code::Internal,
78 };
79 let mut s = Status::new(code, format!("{}: {}", e.code, e.message));
80 if let Ok(v) = e.code.parse() {
81 s.metadata_mut().insert("orbit-error-code", v);
82 }
83 s
84}
85
86fn api(e: ApiError) -> Status {
87 status(e.0)
88}
89
90pub struct GrpcWorkers {
93 state: Shared,
94}
95
96impl GrpcWorkers {
97 pub fn new(state: Shared) -> WorkerServiceServer<GrpcWorkers> {
98 WorkerServiceServer::new(GrpcWorkers { state })
99 }
100
101 fn caller<T>(&self, req: &Request<T>) -> Result<Caller, Status> {
103 let bearer = req
104 .metadata()
105 .get("authorization")
106 .and_then(|v| v.to_str().ok())
107 .and_then(|v| v.strip_prefix("Bearer "));
108 let certs = req.peer_certs();
109 let der = certs
110 .as_ref()
111 .and_then(|c| c.first())
112 .map(|c| c.as_ref().to_vec());
113 match identify(&self.state, bearer, der.as_deref()).map_err(api)? {
114 Some(Caller::Client(c)) => Err(status(EngineError::new(
116 403,
117 "FORBIDDEN",
118 format!("client `{}` may not use the worker protocol", c.id),
119 ))),
120 Some(c) => Ok(c),
121 None => Err(Status::unauthenticated(
122 "UNAUTHENTICATED: missing or invalid credentials",
123 )),
124 }
125 }
126
127 fn identity_check_register(caller: &Caller, req: &RegisterWorker) -> Result<(), Status> {
128 if let Caller::Worker(id) = caller {
129 let deny = |m: String| Err(status(EngineError::new(403, "FORBIDDEN", m)));
130 if req.worker_id != id.id {
131 return deny(format!(
132 "this credential belongs to worker `{}`, not `{}`",
133 id.id, req.worker_id
134 ));
135 }
136 if let Some(rt) = &id.runtime {
137 if !rt.eq_ignore_ascii_case(&req.runtime) {
138 return deny(format!("worker `{}` must announce runtime `{rt}`", id.id));
139 }
140 }
141 let denied: Vec<&str> = req
142 .contracts
143 .iter()
144 .map(|c| c.task_name.as_str())
145 .filter(|t| !id.may_serve(t))
146 .collect();
147 if !denied.is_empty() {
148 return deny(format!(
149 "worker `{}` may not serve: {} (allowed: {})",
150 id.id,
151 denied.join(", "),
152 id.tasks.join(", ")
153 ));
154 }
155 }
156 Ok(())
157 }
158}
159
160#[tonic::async_trait]
161impl WorkerService for GrpcWorkers {
162 async fn register_worker(
163 &self,
164 req: Request<pb::RegisterWorkerRequest>,
165 ) -> Result<Response<pb::RegisterWorkerResponse>, Status> {
166 let caller = self.caller(&req)?;
167 let r = req.into_inner();
168 let reg = RegisterWorker {
169 worker_id: r.worker_id,
170 runtime: r.runtime,
171 sdk: (!r.sdk.is_empty()).then_some(r.sdk),
172 contracts: r
173 .contracts
174 .into_iter()
175 .map(|c| ContractAnnouncement {
176 task_name: c.task_name,
177 contract_version: c.contract_version,
178 })
179 .collect(),
180 };
181 Self::identity_check_register(&caller, ®)?;
182 let out = engine(&self.state).register_worker(reg).map_err(status)?;
183 Ok(Response::new(pb::RegisterWorkerResponse {
184 accepted: out.accepted,
185 worker_session_id: out.worker_session_id,
186 heartbeat_interval_ms: out.heartbeat_interval_ms,
187 session_timeout_ms: out.session_timeout_ms,
188 warnings: out.warnings,
189 }))
190 }
191
192 async fn heartbeat(
193 &self,
194 req: Request<pb::HeartbeatRequest>,
195 ) -> Result<Response<pb::HeartbeatResponse>, Status> {
196 let caller = self.caller(&req)?;
197 let session = req.into_inner().worker_session_id;
198 authorize_session(&self.state, &caller, &session).map_err(api)?;
199 let ack = engine(&self.state).heartbeat(&session).map_err(status)?;
200 Ok(Response::new(pb::HeartbeatResponse {
201 accepted: ack.accepted,
202 cancel: ack.cancel,
203 }))
204 }
205
206 async fn poll_task(
207 &self,
208 req: Request<pb::PollTaskRequest>,
209 ) -> Result<Response<pb::ExecuteTaskRequest>, Status> {
210 let caller = self.caller(&req)?;
211 let r = req.into_inner();
212 authorize_session(&self.state, &caller, &r.worker_session_id).map_err(api)?;
213 let wait = Duration::from_millis(if r.wait_ms == 0 {
214 20_000
215 } else {
216 r.wait_ms.min(60_000)
217 });
218 let deadline = tokio::time::Instant::now() + wait;
219 loop {
220 let notified = self.state.changed.notified();
221 tokio::pin!(notified);
222 notified.as_mut().enable();
223 if let Some(t) = engine(&self.state)
224 .poll(&r.worker_session_id)
225 .map_err(status)?
226 {
227 return Ok(Response::new(pb::ExecuteTaskRequest {
228 token: t.token,
229 execution_id: t.execution_id,
230 workflow: t.workflow,
231 workflow_version: t.workflow_version,
232 step_id: t.step_id,
233 task_name: t.task_name,
234 contract_version: t.contract_version,
235 input: Some(from_json(t.input)),
236 idempotency_key: t.idempotency_key,
237 attempt: t.attempt,
238 redelivery: t.redelivery,
239 deadline_ms: t.deadline_ms,
240 timeout_ms: t.timeout_ms,
241 }));
242 }
243 if tokio::time::Instant::now() >= deadline {
244 return Ok(Response::new(pb::ExecuteTaskRequest::default()));
246 }
247 let _ = tokio::time::timeout_at(deadline, notified).await;
248 }
249 }
250
251 async fn complete_task(
252 &self,
253 req: Request<pb::CompleteTaskRequest>,
254 ) -> Result<Response<pb::TaskResultAck>, Status> {
255 let caller = self.caller(&req)?;
256 let r = req.into_inner();
257 if r.token.is_empty() {
258 return Err(Status::invalid_argument(
259 "INVALID_REQUEST: token is required",
260 ));
261 }
262 authorize_token(&self.state, &caller, &r.token).map_err(api)?;
263 let output = r.output.map(to_json).unwrap_or(Json::Null);
264 let ack = engine(&self.state)
265 .complete(&r.token, output)
266 .map_err(status)?;
267 Ok(Response::new(pb::TaskResultAck {
268 accepted: ack.accepted,
269 reason: ack.reason.unwrap_or_default(),
270 }))
271 }
272
273 async fn fail_task(
274 &self,
275 req: Request<pb::FailTaskRequest>,
276 ) -> Result<Response<pb::TaskResultAck>, Status> {
277 let caller = self.caller(&req)?;
278 let r = req.into_inner();
279 if r.error_code.is_empty() {
280 return Err(Status::invalid_argument(
281 "INVALID_REQUEST: error_code is required",
282 ));
283 }
284 authorize_token(&self.state, &caller, &r.token).map_err(api)?;
285 let error = TaskError {
286 code: r.error_code,
287 message: r.error_message,
288 retryable: r.retryable,
289 details: r.details.map(to_json).filter(|d| !d.is_null()),
290 };
291 let ack = engine(&self.state).fail(&r.token, error).map_err(status)?;
292 Ok(Response::new(pb::TaskResultAck {
293 accepted: ack.accepted,
294 reason: ack.reason.unwrap_or_default(),
295 }))
296 }
297
298 async fn unregister_worker(
299 &self,
300 req: Request<pb::UnregisterWorkerRequest>,
301 ) -> Result<Response<pb::UnregisterWorkerResponse>, Status> {
302 let caller = self.caller(&req)?;
303 let session = req.into_inner().worker_session_id;
304 authorize_session(&self.state, &caller, &session).map_err(api)?;
305 engine(&self.state)
306 .unregister_worker(&session)
307 .map_err(status)?;
308 Ok(Response::new(pb::UnregisterWorkerResponse {
309 accepted: true,
310 }))
311 }
312}