1use std::pin::Pin;
9use std::sync::Arc;
10use std::time::{Duration, SystemTime};
11
12use bytes::Bytes;
13use cronoq::{
14 OverlapPolicy, Recurring, TaskError, TaskFailure, TaskId, TaskRecord, TaskRef, TaskStatus,
15};
16use cronoq_proto::v1 as pb;
17use futures_util::Stream;
18use futures_util::StreamExt;
19use tokio::sync::{broadcast, mpsc};
20use tokio_stream::wrappers::ReceiverStream;
21use tokio_util::sync::CancellationToken;
22use tonic::{Request, Response, Status, Streaming};
23
24use crate::auth::{Principal, check_grpc};
25use crate::config::{LimitsConfig, Role};
26use crate::convert;
27use crate::info::TaskMeta;
28use crate::remote::channel_capacity;
29use crate::state::{AppState, Shared};
30
31const REGISTER_TIMEOUT: Duration = Duration::from_secs(10);
33
34const MAX_SCHEDULE_FIELD: usize = 256;
36
37pub fn wait_bound(requested_s: Option<f64>, limits: &LimitsConfig) -> Result<Duration, Status> {
39 let max = limits.max_wait();
40 Ok(match requested_s {
41 Some(s) => convert::secs(s)?.min(max),
42 None => max,
43 })
44}
45
46fn not_found() -> Status {
47 Status::not_found("unknown task")
48}
49
50pub fn check_pool(principal: &Principal, pool: &str) -> Result<(), Status> {
52 if principal.may_use_pool(pool) {
53 Ok(())
54 } else {
55 Err(Status::permission_denied(format!(
56 "credentials do not allow pool '{pool}'"
57 )))
58 }
59}
60
61pub fn check_pool_shard(app: &AppState, pool: &str) -> Result<(), Status> {
63 match app.shards.as_ref().and_then(|m| m.other_for_pool(pool)) {
64 Some(s) => Err(crate::shard::elsewhere(s, &format!("pool '{pool}'"))),
65 None => Ok(()),
66 }
67}
68
69fn check_id_shard(app: &AppState, target: &TaskRef) -> Result<(), Status> {
71 let TaskRef::Id(id) = target else {
72 return Ok(());
73 };
74 match app.shards.as_ref().and_then(|m| m.other_for_id(*id)) {
75 Some(s) => Err(crate::shard::elsewhere(s, &format!("task {id}"))),
76 None => Ok(()),
77 }
78}
79
80fn check_spec_shard(app: &AppState, spec: &pb::TaskSpec) -> Result<(), Status> {
83 let Some(map) = &app.shards else {
84 return Ok(());
85 };
86 check_pool_shard(app, convert::spec_pool(spec))?;
87 for r in spec.resources.keys() {
88 if let Some(s) = map.other_for_resource(r) {
89 return Err(Status::invalid_argument(format!(
90 "resource '{r}' belongs to shard '{}': a task can only use its own shard's \
91 resources",
92 s.name
93 )));
94 }
95 }
96 for d in &spec.deps {
97 if let Some(pb::task_ref::Target::Id(id)) =
98 d.target.as_ref().and_then(|t| t.target.as_ref())
99 && let Some(s) = map.other_for_id(TaskId(*id))
100 {
101 return Err(Status::invalid_argument(format!(
102 "task {id} is in shard '{}': dependencies across shards are not supported",
103 s.name
104 )));
105 }
106 }
107 Ok(())
108}
109
110pub(crate) async fn resolve_id(app: &AppState, target: &TaskRef) -> Result<Option<TaskId>, Status> {
112 match target {
113 TaskRef::Id(id) => Ok(Some(*id)),
114 TaskRef::Key(k) => {
115 if let Some(h) = app
116 .sched
117 .handle(target.clone())
118 .await
119 .map_err(convert::error)?
120 {
121 return Ok(Some(h.id()));
122 }
123 if let Some(id) = app.info.id_for_key(k) {
124 return Ok(Some(TaskId(id)));
125 }
126 match &app.queries {
127 Some(q) => Ok(q.get_task(target).await.map_err(store_error)?.map(|r| r.id)),
128 None => Ok(None),
129 }
130 }
131 }
132}
133
134fn store_error(e: cronoq::StoreError) -> Status {
135 convert::error(cronoq::Error::Store(e))
136}
137
138pub async fn authorize_task(
143 app: &AppState,
144 principal: &Principal,
145 target: TaskRef,
146) -> Result<TaskRef, Status> {
147 check_id_shard(app, &target)?;
148 if principal.pools.is_none() {
149 return Ok(target);
150 }
151 let id = resolve_id(app, &target).await?.ok_or_else(not_found)?;
152 match app.pool_of(id).await {
153 Some(pool) if principal.may_use_pool(&pool) => Ok(TaskRef::Id(id)),
154 _ => Err(not_found()),
155 }
156}
157
158pub async fn wait_result(
161 app: &AppState,
162 target: TaskRef,
163 timeout: Duration,
164 stop: &CancellationToken,
165) -> Result<pb::TaskResult, Status> {
166 let handle = app
167 .sched
168 .handle(target.clone())
169 .await
170 .map_err(convert::error)?;
171 match handle {
172 Some(h) => await_handle(app, h, timeout, stop).await,
173 None => finished_result(app, &target).await?.ok_or_else(not_found),
175 }
176}
177
178async fn finished_result(
179 app: &AppState,
180 target: &TaskRef,
181) -> Result<Option<pb::TaskResult>, Status> {
182 let Some(q) = &app.queries else {
183 return Ok(None);
184 };
185 let Some(r) = q.get_task(target).await.map_err(store_error)? else {
186 return Ok(None);
187 };
188 let Some(status) = r.status.filter(|s| s.is_terminal()) else {
189 return Err(Status::unavailable(
190 "task is queued but not loaded by this leader; retry",
191 ));
192 };
193 let output_available = status == TaskStatus::Succeeded && r.output.is_some();
194 Ok(Some(pb::TaskResult {
195 id: r.id.0,
196 status: convert::status(status) as i32,
197 output: r.output.map(|b| b.to_vec()),
198 message: r.message.unwrap_or_default(),
199 output_available,
200 }))
201}
202
203pub async fn await_handle(
205 app: &AppState,
206 handle: cronoq::TaskHandle<Bytes>,
207 timeout: Duration,
208 stop: &CancellationToken,
209) -> Result<pb::TaskResult, Status> {
210 let id = handle.id();
211 let outcome = tokio::select! {
212 r = handle => r,
213 _ = tokio::time::sleep(timeout) => {
214 return Err(Status::deadline_exceeded("task is still running; wait again or poll"));
215 }
216 _ = stop.cancelled() => {
217 return Err(Status::unavailable("server is shutting down; wait again on the new leader"));
218 }
219 };
220 Ok(match outcome {
221 Ok(out) => pb::TaskResult {
222 id: id.0,
223 status: pb::TaskStatus::Succeeded as i32,
224 output: Some(out.to_vec()),
225 message: String::new(),
226 output_available: true,
227 },
228 Err(TaskFailure::Ended {
229 status: TaskStatus::Succeeded,
230 ..
231 }) => {
232 let output = app.output(id).await;
233 pb::TaskResult {
234 id: id.0,
235 status: pb::TaskStatus::Succeeded as i32,
236 output_available: output.is_some(),
237 output: output.map(|b| b.to_vec()),
238 message: String::new(),
239 }
240 }
241 Err(TaskFailure::Ended { status, message }) => {
242 let cached = app
245 .info
246 .get(id.0)
247 .filter(|m| m.status.is_some_and(|s| s.is_terminal()))
248 .and_then(|m| m.message)
249 .filter(|m| !m.is_empty());
250 let message = cached
251 .map(|m| m.to_string())
252 .unwrap_or_else(|| message.to_string());
253 pb::TaskResult {
254 id: id.0,
255 status: convert::status(status) as i32,
256 output: None,
257 message,
258 output_available: false,
259 }
260 }
261 Err(TaskFailure::OutputUnavailable { .. }) => {
264 let output = app.output(id).await;
265 pb::TaskResult {
266 id: id.0,
267 status: pb::TaskStatus::Succeeded as i32,
268 output_available: output.is_some(),
269 output: output.map(|b| b.to_vec()),
270 message: String::new(),
271 }
272 }
273 Err(TaskFailure::Shutdown) => {
274 return Err(Status::unavailable(
275 "scheduler shut down before the task finished; wait again on the new leader",
276 ));
277 }
278 Err(e) => return Err(Status::internal(e.to_string())),
279 })
280}
281
282fn record_submit(app: &AppState, id: TaskId, spec: &pb::TaskSpec) {
283 let pool = convert::spec_pool(spec);
284 let max_attempts = match &spec.retry {
285 Some(r) => r.max_attempts.max(1),
286 None => app.pool(pool).map(|p| p.max_attempts).unwrap_or(0),
287 };
288 app.info.on_submit(
289 id.0,
290 TaskMeta {
291 pool: Arc::from(pool),
292 key: (!spec.key.is_empty()).then(|| Arc::from(spec.key.as_str())),
293 kind: Some(Arc::from(if spec.kind.is_empty() {
294 "default"
295 } else {
296 spec.kind.as_str()
297 })),
298 max_attempts,
299 created_ns: Some(convert::unix_ns(SystemTime::now())),
300 tenant: (!spec.tenant.is_empty()).then(|| Arc::from(spec.tenant.as_str())),
301 priority: convert::priority(spec.priority).ok(),
302 ..Default::default()
303 },
304 );
305}
306
307async fn check_cross_pool(
312 app: &AppState,
313 principal: &Principal,
314 spec: &pb::TaskSpec,
315 batch_keys: &[&str],
316) -> Result<(), Status> {
317 if principal.pools.is_none() {
318 return Ok(());
319 }
320 if !spec.key.is_empty() && !batch_keys.contains(&spec.key.as_str()) {
321 let key = TaskRef::Key(spec.key.as_str().into());
322 if let Some(id) = resolve_id(app, &key).await? {
323 check_duplicate(app, principal, id).await?;
324 }
325 }
326 for d in &spec.deps {
327 let target = convert::task_ref(d.target.clone(), &app.limits)?;
328 if let TaskRef::Key(k) = &target
329 && batch_keys.contains(&&**k)
330 {
331 continue;
332 }
333 if let Some(id) = resolve_id(app, &target).await? {
334 match app.pool_of(id).await {
335 Some(pool) if principal.may_use_pool(&pool) => {}
336 _ => return Err(Status::not_found("unknown dependency")),
337 }
338 }
339 }
340 Ok(())
341}
342
343fn pin_tenant(principal: &Principal, spec: &mut pb::TaskSpec) -> Result<(), Status> {
345 let Some(t) = &principal.tenant else {
346 return Ok(());
347 };
348 if spec.tenant.is_empty() {
349 spec.tenant = t.to_string();
350 Ok(())
351 } else if spec.tenant == **t {
352 Ok(())
353 } else {
354 Err(Status::permission_denied(format!(
355 "credentials do not allow tenant '{}'",
356 spec.tenant
357 )))
358 }
359}
360
361async fn check_duplicate(app: &AppState, principal: &Principal, id: TaskId) -> Result<(), Status> {
363 if principal.pools.is_none() {
364 return Ok(());
365 }
366 match app.pool_of(id).await {
367 Some(pool) if principal.may_use_pool(&pool) => Ok(()),
368 _ => Err(Status::already_exists("task key is already in use")),
369 }
370}
371
372pub async fn submit_one(
374 app: &AppState,
375 principal: &Principal,
376 mut spec: pb::TaskSpec,
377 wait: Option<Duration>,
378 stop: &CancellationToken,
379) -> Result<pb::SubmitResponse, Status> {
380 pin_tenant(principal, &mut spec)?;
381 check_pool(principal, convert::spec_pool(&spec))?;
382 check_spec_shard(app, &spec)?;
383 check_cross_pool(app, principal, &spec, &[]).await?;
384 let meta = pb::TaskSpec {
385 payload: Vec::new(),
386 ..spec.clone()
387 };
388 let task = convert::task(spec, &app.limits)?;
389 match wait {
390 Some(timeout) => {
391 let h = app.sched.submit(task).await.map_err(convert::error)?;
392 let (id, duplicate) = (h.id(), h.is_duplicate());
393 if duplicate {
394 check_duplicate(app, principal, id).await?;
396 } else {
397 record_submit(app, id, &meta);
398 }
399 let result = await_handle(app, h, timeout, stop).await?;
400 Ok(pb::SubmitResponse {
401 id: id.0,
402 duplicate,
403 result: Some(result),
404 })
405 }
406 None => {
407 let s = app.sched.enqueue(task).await.map_err(convert::error)?;
408 if s.duplicate {
409 check_duplicate(app, principal, s.id).await?;
410 } else {
411 record_submit(app, s.id, &meta);
412 }
413 Ok(pb::SubmitResponse {
414 id: s.id.0,
415 duplicate: s.duplicate,
416 result: None,
417 })
418 }
419 }
420}
421
422pub async fn submit_batch(
424 app: &AppState,
425 principal: &Principal,
426 mut specs: Vec<pb::TaskSpec>,
427) -> Result<pb::SubmitBatchResponse, Status> {
428 let max = app.limits.max_tasks_per_batch;
429 if specs.len() > max {
430 return Err(Status::invalid_argument(format!(
431 "at most {max} tasks per batch"
432 )));
433 }
434 for s in &mut specs {
435 pin_tenant(principal, s)?;
436 check_pool(principal, convert::spec_pool(s))?;
437 check_spec_shard(app, s)?;
439 }
440 let mut batch_keys: Vec<&str> = Vec::new();
444 if principal.pools.is_some() {
445 for s in specs.iter().filter(|s| !s.key.is_empty()) {
446 let key = TaskRef::Key(s.key.as_str().into());
447 if resolve_id(app, &key).await?.is_none() {
448 batch_keys.push(&s.key);
449 }
450 }
451 }
452 for spec in &specs {
453 check_pool(principal, convert::spec_pool(spec))?;
454 check_cross_pool(app, principal, spec, &batch_keys).await?;
455 }
456 let mut metas = Vec::with_capacity(specs.len());
457 let mut tasks = Vec::with_capacity(specs.len());
458 for spec in specs {
459 metas.push(pb::TaskSpec {
460 payload: Vec::new(),
461 ..spec.clone()
462 });
463 tasks.push(convert::task(spec, &app.limits)?);
464 }
465 let hs = app
466 .sched
467 .submit_batch(tasks)
468 .await
469 .map_err(convert::error)?;
470 for (h, m) in hs.iter().zip(&metas) {
471 if h.is_duplicate() {
472 check_duplicate(app, principal, h.id()).await?;
473 } else {
474 record_submit(app, h.id(), m);
475 }
476 }
477 Ok(pb::SubmitBatchResponse {
478 tasks: hs
479 .iter()
480 .map(|h| pb::SubmitResponse {
481 id: h.id().0,
482 duplicate: h.is_duplicate(),
483 result: None,
484 })
485 .collect(),
486 })
487}
488
489pub async fn task_info(app: &AppState, target: TaskRef) -> Result<pb::TaskInfo, Status> {
491 let id = resolve_id(app, &target).await?.ok_or_else(not_found)?;
492 let live = app
493 .sched
494 .status(TaskRef::Id(id))
495 .await
496 .map_err(convert::error)?;
497 let meta = app.info.get(id.0);
498 let rec = match (&app.queries, live.is_some() && meta.is_some()) {
499 (Some(q), false) => q.get_task(&TaskRef::Id(id)).await.map_err(store_error)?,
500 _ => None,
501 };
502 if live.is_none() && meta.as_ref().and_then(|m| m.status).is_none() && rec.is_none() {
503 return Err(not_found());
504 }
505 let status = live
506 .or_else(|| meta.as_ref().and_then(|m| m.status))
507 .or_else(|| rec.as_ref().and_then(|r| r.status))
508 .unwrap_or(TaskStatus::Ready);
510 Ok(info_message(app, id, status, meta.unwrap_or_default(), rec))
511}
512
513fn info_message(
515 app: &AppState,
516 id: TaskId,
517 status: TaskStatus,
518 meta: TaskMeta,
519 rec: Option<TaskRecord<Bytes>>,
520) -> pb::TaskInfo {
521 let output_available = status == TaskStatus::Succeeded
522 && (app.outputs.get(id).is_some() || rec.as_ref().is_some_and(|r| r.output.is_some()));
523 let pool = if meta.pool.is_empty() {
524 rec.as_ref().map(|r| r.pool.clone()).unwrap_or_default()
525 } else {
526 meta.pool.to_string()
527 };
528 let text = |m: Option<Arc<str>>, r: Option<String>| {
529 m.as_deref().map(str::to_string).or(r).unwrap_or_default()
530 };
531 let rec_key = rec.as_ref().and_then(|r| r.key.clone());
532 let rec_kind = rec.as_ref().map(|r| r.kind.clone());
533 let rec_tenant = rec.as_ref().and_then(|r| r.tenant.clone());
534 let priority = meta.priority.or_else(|| rec.as_ref().map(|r| r.priority));
535 let attempt = meta
536 .attempt
537 .max(rec.as_ref().map(|r| r.attempt).unwrap_or(0));
538 pb::TaskInfo {
539 id: id.0,
540 status: convert::status(status) as i32,
541 pool,
542 key: text(meta.key, rec_key),
543 kind: text(meta.kind, rec_kind),
544 attempt,
545 max_attempts: meta.max_attempts,
546 created_unix_ns: meta.created_ns,
547 started_unix_ns: meta.started_ns,
548 finished_unix_ns: meta.finished_ns,
549 message: text(meta.message, rec.and_then(|r| r.message)),
550 output_available,
551 tenant: text(meta.tenant, rec_tenant),
552 priority: priority.map_or(pb::Priority::Unspecified, convert::priority_to_pb) as i32,
553 }
554}
555
556const DEFAULT_LIST: usize = 100;
558
559pub(crate) fn list_limit(app: &AppState, requested: u32) -> usize {
561 match requested {
562 0 => DEFAULT_LIST,
563 n => n as usize,
564 }
565 .min(app.limits.max_list)
566}
567
568fn listing_pools(
571 app: &AppState,
572 principal: &Principal,
573 requested: Vec<String>,
574) -> Result<Option<Vec<String>>, Status> {
575 if requested.len() > app.limits.max_tasks_per_batch {
576 return Err(Status::invalid_argument("too many pools in the filter"));
577 }
578 for p in &requested {
579 convert::check_str("pool", p, 64)?;
580 if app.pool(p).is_none() {
581 check_pool(principal, p)?;
582 check_pool_shard(app, p)?;
583 return Err(Status::not_found(format!("unknown pool '{p}'")));
584 }
585 check_pool(principal, p)?;
586 }
587 Ok(match (&principal.pools, requested.is_empty()) {
588 (_, false) => Some(requested),
589 (Some(ps), true) => Some(ps.iter().map(|p| p.to_string()).collect()),
590 (None, true) => None,
591 })
592}
593
594pub async fn list_tasks(
596 app: &AppState,
597 principal: &Principal,
598 r: pb::ListTasksRequest,
599) -> Result<pb::ListTasksResponse, Status> {
600 let pools = listing_pools(app, principal, r.pools)?;
601 let pools: Vec<&str> = match &pools {
602 Some(v) if v.is_empty() => return Ok(pb::ListTasksResponse::default()),
604 Some(v) => v.iter().map(String::as_str).collect(),
605 None => Vec::new(),
606 };
607 let mut statuses = Vec::new();
608 for s in r.statuses {
609 let s = convert::status_from_pb(s)?;
610 if !statuses.contains(&s) {
611 statuses.push(s);
612 }
613 }
614 let (finished, live): (Vec<TaskStatus>, Vec<TaskStatus>) =
615 statuses.iter().partition(|s| s.is_terminal());
616 let limit = list_limit(app, r.limit);
617 let after = (r.after > 0).then_some(TaskId(r.after));
618 let mut page: std::collections::BTreeMap<u64, pb::TaskInfo> = Default::default();
620 if statuses.is_empty() || !live.is_empty() {
621 for t in app
622 .sched
623 .list_tasks(&pools, &live, after, limit + 1)
624 .await
625 .map_err(convert::error)?
626 {
627 let meta = app.info.get(t.id.0).unwrap_or_default();
628 let rec = TaskRecord {
629 id: t.id,
630 key: t.key.as_deref().map(str::to_string),
631 pool: t.pool.to_string(),
632 kind: t.kind.to_string(),
633 priority: t.priority,
634 tenant: t.tenant.as_deref().map(str::to_string),
635 status: Some(t.status),
636 attempt: t.attempt,
637 message: None,
638 output: None,
639 updated: SystemTime::UNIX_EPOCH,
640 };
641 page.insert(t.id.0, info_message(app, t.id, t.status, meta, Some(rec)));
642 }
643 }
644 if statuses.is_empty() || !finished.is_empty() {
645 match &app.queries {
647 Some(q) => {
648 for rec in q
649 .list_finished(&pools, &finished, after, limit + 1)
650 .await
651 .map_err(store_error)?
652 {
653 let (id, status) = (rec.id, rec.status.unwrap_or(TaskStatus::Ready));
654 let meta = app.info.get(id.0).unwrap_or_default();
655 page.insert(id.0, info_message(app, id, status, meta, Some(rec)));
656 }
657 }
658 None => {
659 for (id, meta) in app
660 .info
661 .list_finished(&pools, &finished, r.after, limit + 1)
662 {
663 let status = meta.status.unwrap_or(TaskStatus::Ready);
664 page.insert(id, info_message(app, TaskId(id), status, meta, None));
665 }
666 }
667 }
668 }
669 let more = page.len() > limit;
670 let tasks: Vec<pb::TaskInfo> = page.into_values().take(limit).collect();
671 let next_after = if more {
672 tasks.last().map_or(0, |t| t.id)
673 } else {
674 0
675 };
676 Ok(pb::ListTasksResponse {
677 tasks,
678 next_after,
679 unavailable_shards: Vec::new(),
680 })
681}
682
683pub fn list_workers(app: &AppState, principal: &Principal) -> pb::ListWorkersResponse {
685 let workers = app
686 .registry
687 .list()
688 .into_iter()
689 .filter(|w| principal.may_use_pool(&w.pool))
690 .map(|w| pb::WorkerInfo {
691 id: w.id,
692 name: w.name,
693 pool: w.pool.to_string(),
694 labels: w
695 .labels
696 .iter()
697 .map(|(k, v)| (k.to_string(), v.to_string()))
698 .collect(),
699 slots: w.slots,
700 busy: w.busy,
701 connected_unix_ns: convert::unix_ns(w.connected),
702 })
703 .collect();
704 pb::ListWorkersResponse {
705 workers,
706 unavailable_shards: Vec::new(),
707 }
708}
709
710pub async fn list_recurring(
712 app: &AppState,
713 principal: &Principal,
714) -> Result<pb::ListRecurringResponse, Status> {
715 let clock = *app.sched.clock();
716 let mut schedules: Vec<pb::RecurringInfo> = app
717 .sched
718 .list_recurring()
719 .await
720 .map_err(convert::error)?
721 .into_iter()
722 .filter(|r| principal.may_use_pool(&r.pool))
723 .map(|r| pb::RecurringInfo {
724 name: r.name.to_string(),
725 cron: r.cron.to_string(),
726 tz: r.tz.as_deref().unwrap_or_default().to_string(),
727 pool: r.pool.to_string(),
728 kind: r.kind.to_string(),
729 overlap: convert::overlap_to_pb(r.overlap) as i32,
730 next_unix_ns: r.next.map(|t| convert::unix_ns(clock.to_system(t))),
731 last_task_id: r.last_task.map(|t| t.0),
732 })
733 .collect();
734 schedules.sort_by(|a, b| a.name.cmp(&b.name));
735 Ok(pb::ListRecurringResponse {
736 schedules,
737 unavailable_shards: Vec::new(),
738 })
739}
740
741pub async fn cancel(app: &AppState, target: TaskRef) -> Result<bool, Status> {
743 app.sched.cancel(target).await.map_err(convert::error)
744}
745
746#[derive(Clone, Debug)]
748pub struct WatchFilter {
749 pub pools: Vec<String>,
751 pub ids: Vec<u64>,
753 pub keys: Vec<String>,
755 pub all_transitions: bool,
757}
758
759pub fn watch(
762 app: &Arc<AppState>,
763 principal: &Principal,
764 filter: WatchFilter,
765 stop: CancellationToken,
766) -> Result<mpsc::Receiver<Result<pb::StatusEvent, Status>>, Status> {
767 for p in &filter.pools {
768 convert::check_str("pool", p, 64)?;
769 if app.pool(p).is_none() {
770 check_pool(principal, p)?;
771 check_pool_shard(app, p)?;
772 return Err(Status::not_found(format!("unknown pool '{p}'")));
773 }
774 check_pool(principal, p)?;
775 }
776 if filter.ids.len() + filter.keys.len() > app.limits.max_tasks_per_batch {
777 return Err(Status::invalid_argument("too many ids/keys in the filter"));
778 }
779 for k in &filter.keys {
780 convert::check_str("key", k, app.limits.max_key_len)?;
781 }
782 let allowed: Option<Vec<String>> = match (&principal.pools, filter.pools.is_empty()) {
783 (_, false) => Some(filter.pools.clone()),
784 (Some(ps), true) => Some(ps.iter().map(|p| p.to_string()).collect()),
785 (None, true) => None,
786 };
787 let names: Arc<[Arc<str>]> = app.sched.pools().into();
788 let clock = *app.sched.clock();
789 let mut events = app.sched.events();
790 let (tx, rx) = mpsc::channel(256);
791 tokio::spawn(async move {
792 loop {
793 let ev = tokio::select! {
794 e = events.recv() => e,
795 _ = stop.cancelled() => {
796 let _ = tx.send(Err(Status::unavailable("server is shutting down; reconnect to the leader"))).await;
797 return;
798 }
799 _ = tx.closed() => return,
800 };
801 let e = match ev {
802 Ok(e) => e,
803 Err(broadcast::error::RecvError::Lagged(n)) => {
804 let _ = tx
805 .send(Err(Status::data_loss(format!(
806 "watch fell behind; {n} events dropped"
807 ))))
808 .await;
809 return;
810 }
811 Err(broadcast::error::RecvError::Closed) => {
812 let _ = tx.send(Err(Status::unavailable("scheduler stopped"))).await;
813 return;
814 }
815 };
816 if !filter.all_transitions && !e.status.is_terminal() {
817 continue;
818 }
819 if !filter.ids.is_empty() && !filter.ids.contains(&e.id.0) {
820 continue;
821 }
822 if !filter.keys.is_empty()
823 && !e
824 .key
825 .as_deref()
826 .is_some_and(|k| filter.keys.iter().any(|f| f == k))
827 {
828 continue;
829 }
830 let ev = convert::event(&e, &names, &clock);
831 if allowed.as_ref().is_some_and(|ps| !ps.contains(&ev.pool)) {
832 continue;
833 }
834 if tx.send(Ok(ev)).await.is_err() {
835 return;
836 }
837 }
838 });
839 Ok(rx)
840}
841
842pub struct TasksService {
844 pub shared: Arc<Shared>,
846}
847
848type EventStream = Pin<Box<dyn Stream<Item = Result<pb::StatusEvent, Status>> + Send>>;
849
850#[tonic::async_trait]
851impl pb::tasks_server::Tasks for TasksService {
852 async fn submit(
853 &self,
854 req: Request<pb::SubmitRequest>,
855 ) -> Result<Response<pb::SubmitResponse>, Status> {
856 let p = check_grpc(&self.shared.auth, &req, Role::Client)?;
857 let app = self.shared.app_or_unavailable()?;
858 let r = req.into_inner();
859 let spec = r
860 .task
861 .ok_or_else(|| Status::invalid_argument("missing task"))?;
862 let wait = if r.wait {
863 Some(wait_bound(r.wait_timeout_s, &app.limits)?)
864 } else {
865 None
866 };
867 Ok(Response::new(
868 crate::route::submit(app, &p, spec, wait, &self.shared.stop).await?,
869 ))
870 }
871
872 async fn submit_batch(
873 &self,
874 req: Request<pb::SubmitBatchRequest>,
875 ) -> Result<Response<pb::SubmitBatchResponse>, Status> {
876 let p = check_grpc(&self.shared.auth, &req, Role::Client)?;
877 let app = self.shared.app_or_unavailable()?;
878 Ok(Response::new(
879 crate::route::submit_batch(app, &p, req.into_inner().tasks).await?,
880 ))
881 }
882
883 async fn cancel(
884 &self,
885 req: Request<pb::CancelRequest>,
886 ) -> Result<Response<pb::CancelResponse>, Status> {
887 let p = check_grpc(&self.shared.auth, &req, Role::Client)?;
888 let app = self.shared.app_or_unavailable()?;
889 let target = convert::task_ref(req.into_inner().task, &app.limits)?;
890 Ok(Response::new(pb::CancelResponse {
891 cancelled: crate::route::cancel(app, &p, target).await?,
892 }))
893 }
894
895 async fn get(&self, req: Request<pb::GetRequest>) -> Result<Response<pb::TaskInfo>, Status> {
896 let p = check_grpc(&self.shared.auth, &req, Role::Client)?;
897 let app = self.shared.app_or_unavailable()?;
898 let target = convert::task_ref(req.into_inner().task, &app.limits)?;
899 let (info, missing) = crate::route::get(app, &p, target).await?;
900 Ok(crate::route::with_missing(info, &missing))
901 }
902
903 async fn list_tasks(
904 &self,
905 req: Request<pb::ListTasksRequest>,
906 ) -> Result<Response<pb::ListTasksResponse>, Status> {
907 let p = check_grpc(&self.shared.auth, &req, Role::Client)?;
908 let app = self.shared.app_or_unavailable()?;
909 Ok(Response::new(
910 crate::route::list_tasks(app, &p, req.into_inner()).await?,
911 ))
912 }
913
914 async fn wait(
915 &self,
916 req: Request<pb::WaitRequest>,
917 ) -> Result<Response<pb::TaskResult>, Status> {
918 let p = check_grpc(&self.shared.auth, &req, Role::Client)?;
919 let app = self.shared.app_or_unavailable()?;
920 let r = req.into_inner();
921 let target = convert::task_ref(r.task, &app.limits)?;
922 let timeout = wait_bound(r.timeout_s, &app.limits)?;
923 let (result, missing) =
924 crate::route::wait(app, &p, target, timeout, &self.shared.stop).await?;
925 Ok(crate::route::with_missing(result, &missing))
926 }
927
928 type WatchStream = EventStream;
929
930 async fn watch(
931 &self,
932 req: Request<pb::WatchRequest>,
933 ) -> Result<Response<Self::WatchStream>, Status> {
934 let p = check_grpc(&self.shared.auth, &req, Role::Client)?;
935 let app = self.shared.app_or_unavailable()?;
936 let r = req.into_inner();
937 let filter = WatchFilter {
938 pools: r.pools,
939 ids: r.ids,
940 keys: r.keys,
941 all_transitions: r.all_transitions,
942 };
943 let rx = crate::route::watch(app, &p, filter, self.shared.stop.clone())?;
944 Ok(Response::new(Box::pin(ReceiverStream::new(rx))))
945 }
946}
947
948pub struct WorkersService {
950 pub shared: Arc<Shared>,
952}
953
954type ServerStream = Pin<Box<dyn Stream<Item = Result<pb::ServerMessage, Status>> + Send>>;
955
956#[tonic::async_trait]
957impl pb::workers_server::Workers for WorkersService {
958 type WorkStream = ServerStream;
959
960 async fn work(
961 &self,
962 req: Request<Streaming<pb::WorkerMessage>>,
963 ) -> Result<Response<Self::WorkStream>, Status> {
964 let principal = check_grpc(&self.shared.auth, &req, Role::Worker)?;
965 let app = self.shared.app_or_unavailable()?.clone();
966 let mut inbound = req.into_inner();
967 let first = tokio::time::timeout(REGISTER_TIMEOUT, inbound.message())
968 .await
969 .map_err(|_| Status::deadline_exceeded("expected Register within 10s"))??
970 .and_then(|m| m.msg);
971 let Some(pb::worker_message::Msg::Register(reg)) = first else {
972 return Err(Status::invalid_argument(
973 "expected Register as first message",
974 ));
975 };
976 if reg.protocol_version.max(1) < cronoq_proto::PROTOCOL_VERSION {
977 return Err(Status::failed_precondition(format!(
978 "worker protocol version {} is not supported: this server needs version {} \
979 (upgrade the worker)",
980 reg.protocol_version.max(1),
981 cronoq_proto::PROTOCOL_VERSION
982 )));
983 }
984 convert::check_str("pool", ®.pool, 64)?;
985 let labels = convert::worker_labels(reg.labels, &app.limits)?;
986 if !principal.may_claim_labels(&labels) {
987 return Err(Status::permission_denied(
988 "credentials do not allow this worker's labels",
989 ));
990 }
991 let Some(info) = app.pool(®.pool).cloned() else {
992 check_pool(&principal, ®.pool)?;
993 check_pool_shard(&app, ®.pool)?;
995 return Err(Status::not_found(format!("unknown pool '{}'", reg.pool)));
996 };
997 check_pool(&principal, ®.pool)?;
998 let pool: Arc<str> = app
999 .pools
1000 .get_key_value(reg.pool.as_str())
1001 .map(|(k, _)| k.clone())
1002 .unwrap();
1003 let name = convert::sanitize_name(®.worker_name);
1004 let slots = reg.slots.min(info.slot_cap);
1005 let (tx, rx) = mpsc::channel(channel_capacity(info.slot_cap));
1006 let registry = app.registry.clone();
1007 let worker = registry.next_worker_id();
1008 let _ = tx.try_send(Ok(pb::ServerMessage {
1010 msg: Some(pb::server_message::Msg::Registered(pb::Registered {
1011 worker_id: worker,
1012 lease_ttl_s: Some(info.lease_ttl.as_secs_f64()),
1013 slots,
1014 protocol_version: cronoq_proto::PROTOCOL_VERSION,
1015 max_message_bytes: app.limits.max_message_bytes as u64,
1016 })),
1017 }));
1018 let cut = CancellationToken::new();
1019 if !registry.register(
1020 worker,
1021 pool.clone(),
1022 labels.clone(),
1023 name.clone(),
1024 slots,
1025 tx,
1026 cut.clone(),
1027 ) {
1028 return Err(Status::unavailable("server is shutting down"));
1029 }
1030 if slots < reg.slots {
1031 tracing::info!(worker, pool = %pool, requested = reg.slots, granted = slots, "worker slots capped");
1032 }
1033 tracing::info!(worker, pool = %pool, slots, name = ?name, labels = ?labels, "worker registered");
1034 let slot_cap = info.slot_cap;
1035 let inbound_cut = cut.clone();
1036 tokio::spawn(async move {
1037 loop {
1038 let msg = tokio::select! {
1039 m = inbound.message() => m,
1040 _ = inbound_cut.cancelled() => break,
1041 };
1042 let msg = match msg {
1043 Ok(Some(m)) => m,
1044 Ok(None) => break,
1045 Err(e) => {
1046 tracing::debug!(worker, error = %e, "worker stream error");
1047 break;
1048 }
1049 };
1050 use pb::worker_message::Msg;
1051 match msg.msg {
1052 Some(Msg::Heartbeat(h)) => registry.heartbeat(worker, h.lease),
1053 Some(Msg::Complete(c)) => {
1054 registry.finish(worker, c.lease, Ok(Bytes::from(c.output)));
1055 }
1056 Some(Msg::Fail(f)) => {
1057 let message = convert::sanitize_message(&f.message);
1058 let err = if f.permanent {
1059 TaskError::permanent(message)
1060 } else {
1061 TaskError::Retryable {
1062 message: message.into(),
1063 retry_after: f.retry_after_s.and_then(|s| convert::secs(s).ok()),
1064 }
1065 };
1066 registry.finish(worker, f.lease, Err(err));
1067 }
1068 Some(Msg::SetSlots(s)) => registry.set_slots(worker, s.slots.min(slot_cap)),
1069 Some(Msg::Nack(n)) => {
1070 registry.nack(worker, n.lease, &convert::sanitize_message(&n.reason))
1071 }
1072 Some(Msg::Cancelled(c)) => {
1073 registry.finish(worker, c.lease, Err(TaskError::Cancelled));
1074 }
1075 Some(Msg::Register(_)) => {
1076 registry.reject(
1077 worker,
1078 Status::invalid_argument("duplicate Register on one stream"),
1079 );
1080 break;
1081 }
1082 None => {}
1083 }
1084 }
1085 registry.unregister(worker);
1086 tracing::info!(worker, "worker disconnected");
1087 });
1088 let out = ReceiverStream::new(rx).take_until(cut.cancelled_owned());
1089 Ok(Response::new(Box::pin(out)))
1090 }
1091}
1092
1093pub struct AdminService {
1095 pub shared: Arc<Shared>,
1097}
1098
1099pub async fn snapshot(app: &AppState) -> Result<pb::SnapshotResponse, Status> {
1101 let s = app.sched.snapshot().await.map_err(convert::error)?;
1102 let registry = app.registry.clone();
1103 let mut r = convert::snapshot(&s, &|p| registry.pool_worker_count(p));
1104 if let Some(t) = &mut r.totals {
1106 t.stale_completions += registry.stale_reports();
1107 }
1108 Ok(r)
1109}
1110
1111fn check_recurring(r: &pb::RecurringRequest) -> Result<(), Status> {
1113 convert::check_str("name", &r.name, MAX_SCHEDULE_FIELD)?;
1114 convert::check_str("cron", &r.cron, MAX_SCHEDULE_FIELD)?;
1115 convert::check_str("tz", &r.tz, 64)?;
1116 if r.name.is_empty() {
1117 return Err(Status::invalid_argument("recurring schedule needs a name"));
1118 }
1119 Ok(())
1120}
1121
1122pub async fn add_recurring(app: &AppState, r: pb::RecurringRequest) -> Result<(), Status> {
1124 check_recurring(&r)?;
1125 let template = r.template.unwrap_or_default();
1126 check_spec_shard(app, &template)?;
1127 let template = convert::task(template, &app.limits)?;
1128 let overlap = match pb::Overlap::try_from(r.overlap) {
1129 Ok(pb::Overlap::Unspecified | pb::Overlap::Skip) => OverlapPolicy::Skip,
1130 Ok(pb::Overlap::Allow) => OverlapPolicy::Allow,
1131 Ok(pb::Overlap::Queue) => OverlapPolicy::Queue,
1132 Ok(pb::Overlap::QueueOnSuccess) => OverlapPolicy::QueueOnSuccess,
1133 Err(_) => return Err(Status::invalid_argument("unknown overlap policy")),
1134 };
1135 let mut rec = Recurring::new(r.name, r.cron, template).overlap(overlap);
1136 if !r.tz.is_empty() {
1137 rec = rec.tz(r.tz);
1138 }
1139 app.sched.add_recurring(rec).await.map_err(convert::error)
1140}
1141
1142pub async fn remove_recurring(app: &AppState, name: &str) -> Result<bool, Status> {
1144 app.sched
1145 .remove_recurring(name)
1146 .await
1147 .map_err(convert::error)
1148}
1149
1150#[tonic::async_trait]
1151impl pb::admin_server::Admin for AdminService {
1152 async fn snapshot(
1153 &self,
1154 req: Request<pb::Empty>,
1155 ) -> Result<Response<pb::SnapshotResponse>, Status> {
1156 let p = check_grpc(&self.shared.auth, &req, Role::Metrics)?;
1157 let app = self.shared.app_or_unavailable()?;
1158 Ok(Response::new(crate::route::snapshot(app, &p).await?))
1159 }
1160
1161 async fn add_recurring(
1162 &self,
1163 req: Request<pb::RecurringRequest>,
1164 ) -> Result<Response<pb::Empty>, Status> {
1165 let p = check_grpc(&self.shared.auth, &req, Role::Admin)?;
1166 let app = self.shared.app_or_unavailable()?;
1167 let r = req.into_inner();
1168 check_recurring(&r)?;
1169 crate::route::add_recurring(app, &p, r).await?;
1170 Ok(Response::new(pb::Empty {}))
1171 }
1172
1173 async fn remove_recurring(
1174 &self,
1175 req: Request<pb::RemoveRecurringRequest>,
1176 ) -> Result<Response<pb::RemoveRecurringResponse>, Status> {
1177 let p = check_grpc(&self.shared.auth, &req, Role::Admin)?;
1178 let app = self.shared.app_or_unavailable()?;
1179 let name = req.into_inner().name;
1180 convert::check_str("name", &name, MAX_SCHEDULE_FIELD)?;
1181 let removed = crate::route::remove_recurring(app, &p, name).await?;
1182 Ok(Response::new(pb::RemoveRecurringResponse { removed }))
1183 }
1184
1185 async fn list_workers(
1186 &self,
1187 req: Request<pb::Empty>,
1188 ) -> Result<Response<pb::ListWorkersResponse>, Status> {
1189 let p = check_grpc(&self.shared.auth, &req, Role::Metrics)?;
1190 let app = self.shared.app_or_unavailable()?;
1191 Ok(Response::new(crate::route::list_workers(app, &p).await?))
1192 }
1193
1194 async fn list_recurring(
1195 &self,
1196 req: Request<pb::Empty>,
1197 ) -> Result<Response<pb::ListRecurringResponse>, Status> {
1198 let p = check_grpc(&self.shared.auth, &req, Role::Metrics)?;
1199 let app = self.shared.app_or_unavailable()?;
1200 Ok(Response::new(crate::route::list_recurring(app, &p).await?))
1201 }
1202}