1use headgate_core::{
8 AdmissionExplain, BlockedBy, BulkRequest, Checkpoint, CheckpointInspect,
9 ConcurrencyLimitConfig, HistoryBucket, Inspect, JobFilter, JobOutput, JobPage, JobProgress,
10 JobResult, JobSummary, MissedPolicy, OperationStatus, OutputInspect, PartitionState,
11 ProgressInspect, QuarantineEntry, QueueStats, QuietGroupMetrics, RateClassConfig,
12 RateClassState, ResultInspect, SCHEDULE_EVENT_LIMIT, SaturationStrategy, Schedule,
13 ScheduleEvent, ScheduleEventOutcome, StateCounts, StoreError, WorkerMeta, noisy_partition_keys,
14};
15use tokio_postgres::types::ToSql;
16
17use crate::{NOW_MS, PgStore, decode_headers, map_pg_err};
18
19const SAMPLE_LIMIT: i64 = 50_000;
21const POSITION_LIMIT: i64 = 1_000;
23const QUIET_PARTITION_LIMIT: i64 = 1_000;
24pub(crate) const MAX_PAGE: u32 = 200;
25
26fn job_from_row(row: &tokio_postgres::Row, include_payload: bool) -> JobSummary {
27 JobSummary {
28 id: row.get("ulid"),
29 kind: row.get("kind"),
30 queue: row.get("queue"),
31 state: row.get("state_text"),
32 schema_version: row.get::<_, i32>("schema_version") as u32,
33 priority: row.get("priority"),
34 attempt: row.get::<_, i32>("attempt") as u32,
35 crash_attempt: row.get::<_, i32>("crash_attempt") as u32,
36 max_attempts: row.get::<_, i32>("max_attempts") as u32,
37 partition_key: row.get("partition_key"),
38 rate_class: row.get("rate_class"),
39 sticky_worker: row.get("sticky_worker"),
40 weight: row.get::<_, i32>("weight") as u32,
41 fingerprint: row.get("fingerprint"),
42 enqueued_at_ms: row.get("enqueued_at_ms"),
43 scheduled_at_ms: row.get("scheduled_at_ms"),
44 claimed_at_ms: row.get("claimed_at_ms"),
45 periodic_schedule_id: row.get("periodic_schedule_id"),
46 periodic_tick_ms: row.get("periodic_tick_ms"),
47 finalized_at_ms: row.get("finalized_at_ms"),
48 payload: if include_payload {
49 Some(row.get("payload"))
50 } else {
51 None
52 },
53 headers: if include_payload {
54 decode_headers(row.get("headers"))
55 } else {
56 Default::default()
57 },
58 errors_json: row.get("errors_text"),
59 tags: serde_json::from_str(row.get("tags_text")).unwrap_or_default(),
60 }
61}
62
63const JOB_COLS: &str = "j.ulid, j.kind, j.queue, j.state::text AS state_text, \
64 j.schema_version, j.priority, j.attempt, j.crash_attempt, j.max_attempts, \
65 j.partition_key, j.rate_class, j.sticky_worker, j.weight, j.fingerprint, j.enqueued_at_ms, j.scheduled_at_ms, j.claimed_at_ms, \
66 j.periodic_schedule_id, j.periodic_tick_ms, j.finalized_at_ms, j.payload, j.headers, \
67 j.errors::text AS errors_text, j.id, COALESCE((SELECT json_agg(t.tag ORDER BY t.tag) FROM headgate_job_tag t WHERE t.job_id=j.id),'[]')::text AS tags_text";
68
69#[async_trait::async_trait]
70impl Inspect for PgStore {
71 fn as_result_inspect(&self) -> Option<&dyn ResultInspect> {
72 Some(self)
73 }
74
75 fn as_output_inspect(&self) -> Option<&dyn OutputInspect> {
76 Some(self)
77 }
78
79 fn as_progress_inspect(&self) -> Option<&dyn ProgressInspect> {
80 Some(self)
81 }
82
83 fn as_checkpoint_inspect(&self) -> Option<&dyn CheckpointInspect> {
84 Some(self)
85 }
86
87 async fn get_job(
88 &self,
89 id: &str,
90 include_payload: bool,
91 ) -> Result<Option<JobSummary>, StoreError> {
92 let c = self.client().await?;
93 let row = c
94 .query_opt(
95 &format!("SELECT {JOB_COLS} FROM headgate_job j WHERE j.ulid = $1"),
96 &[&id],
97 )
98 .await
99 .map_err(map_pg_err)?;
100 Ok(row.map(|r| job_from_row(&r, include_payload)))
101 }
102
103 async fn list_jobs(
104 &self,
105 filter: &JobFilter,
106 cursor: Option<&str>,
107 limit: u32,
108 ) -> Result<JobPage, StoreError> {
109 let limit = limit.clamp(1, MAX_PAGE) as i64;
110 let c = self.client().await?;
111 let mut clauses: Vec<String> = Vec::new();
112 let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new();
113 macro_rules! bind {
114 ($v:expr, $sql:expr) => {
115 if let Some(v) = $v {
116 params.push(v);
117 clauses.push(format!($sql, params.len()));
118 }
119 };
120 }
121 bind!(filter.queue.as_ref(), "j.queue = ${}");
122 bind!(filter.kind.as_ref(), "j.kind = ${}");
123 bind!(filter.kind_prefix.as_ref(), "starts_with(j.kind, ${})");
124 bind!(filter.partition_key.as_ref(), "j.partition_key = ${}");
125 bind!(filter.state.as_ref(), "j.state::text = ${}");
126 bind!(filter.id.as_ref(), "j.ulid = ${}");
127 bind!(filter.fingerprint.as_ref(), "j.fingerprint = ${}");
128 bind!(filter.rate_class.as_ref(), "j.rate_class = ${}");
129 bind!(filter.priority.as_ref(), "j.priority = ${}");
130 if !filter.tags_all.is_empty() {
131 params.push(&filter.tags_all);
132 clauses.push(format!("NOT EXISTS (SELECT 1 FROM unnest(${}::text[]) want(tag) WHERE NOT EXISTS (SELECT 1 FROM headgate_job_tag jt WHERE jt.job_id=j.id AND jt.tag=want.tag))", params.len()));
133 }
134 if !filter.tags_any.is_empty() {
135 params.push(&filter.tags_any);
136 clauses.push(format!("EXISTS (SELECT 1 FROM headgate_job_tag jt WHERE jt.job_id=j.id AND jt.tag=ANY(${}::text[]))", params.len()));
137 }
138 let cursor_id: i64 = match cursor {
140 Some(cur) => cur
141 .parse()
142 .map_err(|_| StoreError::Invalid("bad cursor".into()))?,
143 None => i64::MAX,
144 };
145 params.push(&cursor_id);
146 clauses.push(format!("j.id < ${}", params.len()));
147 params.push(&limit);
148 let sql = format!(
149 "SELECT {JOB_COLS} FROM headgate_job j WHERE {} ORDER BY j.id DESC LIMIT ${}",
150 clauses.join(" AND "),
151 params.len()
152 );
153 let rows = c.query(&sql, ¶ms).await.map_err(map_pg_err)?;
154 let next_cursor = if rows.len() as i64 == limit {
155 rows.last().map(|r| r.get::<_, i64>("id").to_string())
156 } else {
157 None
158 };
159 Ok(JobPage {
160 jobs: rows.iter().map(|r| job_from_row(r, false)).collect(),
161 next_cursor,
162 })
163 }
164
165 async fn counts(&self, queue: Option<&str>) -> Result<StateCounts, StoreError> {
166 let c = self.client().await?;
167 let rows = c
168 .query(
169 "WITH sample AS (
170 SELECT state FROM headgate_job
171 WHERE ($1::text IS NULL OR queue = $1)
172 LIMIT $2
173 )
174 SELECT state::text, count(*)::bigint FROM sample GROUP BY 1",
175 &[&queue, &SAMPLE_LIMIT],
176 )
177 .await
178 .map_err(map_pg_err)?;
179 let counts: Vec<(String, i64)> = rows.iter().map(|r| (r.get(0), r.get(1))).collect();
180 let total: i64 = counts.iter().map(|(_, n)| n).sum();
181 Ok(StateCounts {
182 counts,
183 approximate: total >= SAMPLE_LIMIT,
184 })
185 }
186
187 async fn queue_stats(&self) -> Result<Vec<QueueStats>, StoreError> {
188 let c = self.client().await?;
189 let sql = format!(
192 r#"
193 WITH p AS (SELECT {NOW_MS} AS now_ms),
194 sample AS (SELECT queue, state FROM headgate_job LIMIT $1),
195 names AS (
196 SELECT queue FROM headgate_queue_state
197 UNION SELECT queue FROM headgate_enqueue_policy
198 UNION SELECT queue FROM headgate_queue_counter, p
199 WHERE bucket_ms >= p.now_ms - 3600000
200 UNION SELECT DISTINCT queue FROM sample
201 ),
202 by_state AS (
203 SELECT queue, state::text AS state, count(*)::bigint AS n
204 FROM sample GROUP BY 1, 2
205 ),
206 rates AS (
207 SELECT c.queue,
208 sum(c.arrived)::float8 / 60.0 AS arrival,
209 sum(c.completed)::float8 / 60.0 AS drain
210 FROM headgate_queue_counter c, p
211 WHERE c.bucket_ms >= (p.now_ms / 60000 * 60000) - 60000
212 GROUP BY 1
213 )
214 SELECT n.queue,
215 p.now_ms,
216 COALESCE(qs.paused, false) AS paused,
217 COALESCE(qs.weight, 1) AS weight,
218 COALESCE(r.arrival, 0) AS arrival,
219 COALESCE(r.drain, 0) AS drain,
220 COALESCE((SELECT json_agg(json_build_array(b.state, b.n))
221 FROM by_state b WHERE b.queue = n.queue), '[]'::json)::text AS states,
222 (SELECT count(*) FROM sample) >= $1 AS approx,
223 (SELECT j.scheduled_at_ms FROM headgate_job j
224 WHERE j.queue = n.queue AND j.state = 'available'
225 ORDER BY j.scheduled_at_ms, j.id LIMIT 1) AS oldest_available_at_ms,
226 ep.max_unfinished_jobs,
227 COALESCE(ent.n, 0) AS entered,
228 COALESCE(ext.n, 0) AS exited,
229 samp.memory_bytes
230 FROM names n CROSS JOIN p
231 LEFT JOIN headgate_queue_state qs ON qs.queue = n.queue
232 LEFT JOIN rates r ON r.queue = n.queue
233 LEFT JOIN headgate_enqueue_policy ep ON ep.queue = n.queue
234 LEFT JOIN headgate_enqueue_counter ent
235 ON ent.queue = n.queue AND ent.counter_kind = 'entered'
236 LEFT JOIN headgate_enqueue_counter ext
237 ON ext.queue = n.queue AND ext.counter_kind = 'exited'
238 LEFT JOIN headgate_queue_sample samp ON samp.queue = n.queue
239 ORDER BY n.queue
240 "#
241 );
242 let rows = c.query(&sql, &[&SAMPLE_LIMIT]).await.map_err(map_pg_err)?;
243 let mut out = Vec::with_capacity(rows.len());
244 for row in &rows {
245 let queue: String = row.get("queue");
246 let arrival: f64 = row.get("arrival");
247 let drain: f64 = row.get("drain");
248 let now_ms: i64 = row.get("now_ms");
249 let oldest_available_ms = row
250 .get::<_, Option<i64>>("oldest_available_at_ms")
251 .map(|at| (now_ms - at).max(0));
252 let states: Vec<(String, i64)> =
253 serde_json::from_str::<serde_json::Value>(row.get("states"))
254 .ok()
255 .and_then(|v| v.as_array().cloned())
256 .map(|arr| {
257 arr.iter()
258 .filter_map(|pair| {
259 Some((pair.get(0)?.as_str()?.to_string(), pair.get(1)?.as_i64()?))
260 })
261 .collect()
262 })
263 .unwrap_or_default();
264 let entered: i64 = row.get("entered");
265 let exited: i64 = row.get("exited");
266 let unfinished_jobs = entered.saturating_sub(exited).max(0) as u64;
267 let backlog = unfinished_jobs.min(i64::MAX as u64) as i64;
268 let ttd = if drain > arrival && drain > 0.0 {
270 Some(((backlog as f64) / (drain - arrival) * 1000.0) as i64)
271 } else {
272 None
273 };
274 let part_rows = c
278 .query(
279 &format!(
280 r#"
281 WITH names AS (
282 SELECT partition_key FROM headgate_active_partition WHERE queue = $1
283 UNION SELECT partition_key FROM headgate_inflight
284 WHERE queue = $1 AND n > 0
285 UNION SELECT partition_key FROM headgate_partition_counter
286 WHERE queue = $1 AND bucket_ms >= $2
287 ORDER BY 1 LIMIT $3
288 ), rates AS (
289 SELECT partition_key, sum(arrived)::bigint AS arrived,
290 sum(completed)::bigint AS completed
291 FROM headgate_partition_counter
292 WHERE queue = $1 AND bucket_ms >= $2 GROUP BY 1
293 )
294 SELECT n.partition_key, COALESCE(i.n, 0)::bigint AS inflight,
295 COALESCE(r.arrived, 0)::bigint AS arrived,
296 COALESCE(r.completed, 0)::bigint AS completed,
297 (SELECT j.scheduled_at_ms FROM headgate_job j
298 WHERE j.queue = $1 AND j.partition_key = n.partition_key
299 AND j.state = 'available'
300 ORDER BY j.scheduled_at_ms, j.id LIMIT 1) AS oldest_at
301 FROM names n
302 LEFT JOIN headgate_inflight i
303 ON i.queue = $1 AND i.partition_key = n.partition_key
304 LEFT JOIN rates r ON r.partition_key = n.partition_key
305 ORDER BY n.partition_key
306 "#
307 ),
308 &[
309 &queue,
310 &(now_ms / 60000 * 60000 - 60000),
311 &(QUIET_PARTITION_LIMIT + 1),
312 ],
313 )
314 .await
315 .map_err(map_pg_err)?;
316 let part_approx = part_rows.len() as i64 > QUIET_PARTITION_LIMIT;
317 let part_rows = &part_rows[..part_rows.len().min(QUIET_PARTITION_LIMIT as usize)];
318 let loads: Vec<(String, i64)> = part_rows
319 .iter()
320 .map(|r| (r.get("partition_key"), r.get("inflight")))
321 .collect();
322 let noisy = noisy_partition_keys(&loads);
323 let quiet_parts: Vec<String> = loads
324 .iter()
325 .filter(|(p, _)| !noisy.contains(p))
326 .map(|(p, _)| p.clone())
327 .collect();
328 let quiet_arrived: i64 = part_rows
329 .iter()
330 .filter(|r| !noisy.contains(&r.get::<_, String>("partition_key")))
331 .map(|r| r.get::<_, i64>("arrived"))
332 .sum();
333 let quiet_completed: i64 = part_rows
334 .iter()
335 .filter(|r| !noisy.contains(&r.get::<_, String>("partition_key")))
336 .map(|r| r.get::<_, i64>("completed"))
337 .sum();
338 let quiet_oldest_at = part_rows
339 .iter()
340 .filter(|r| !noisy.contains(&r.get::<_, String>("partition_key")))
341 .filter_map(|r| r.get::<_, Option<i64>>("oldest_at"))
342 .min();
343 let quiet_backlog: i64 = if quiet_parts.is_empty() {
344 0
345 } else {
346 c.query_one(
347 "SELECT count(*)::bigint FROM (
348 SELECT 1 FROM headgate_job
349 WHERE queue = $1 AND partition_key = ANY($2)
350 AND state = ANY(ARRAY['pending','scheduled','available','running','retryable']::headgate_state[])
351 LIMIT $3
352 ) bounded",
353 &[&queue, &quiet_parts, &SAMPLE_LIMIT],
354 )
355 .await
356 .map_err(map_pg_err)?
357 .get(0)
358 };
359 let (quiet_arrival, quiet_drain) =
360 (quiet_arrived as f64 / 60.0, quiet_completed as f64 / 60.0);
361 let quiet_ttd = if quiet_drain > quiet_arrival && quiet_drain > 0.0 {
362 Some((quiet_backlog as f64 / (quiet_drain - quiet_arrival) * 1000.0) as i64)
363 } else {
364 None
365 };
366 let quiet_groups = QuietGroupMetrics {
367 arrival_rate: quiet_arrival,
368 drain_rate: quiet_drain,
369 time_to_drain_ms: quiet_ttd,
370 oldest_available_ms: quiet_oldest_at.map(|at| (now_ms - at).max(0)),
371 noisy_partitions: noisy.len() as u32,
372 approximate: part_approx || quiet_backlog >= SAMPLE_LIMIT,
373 };
374 out.push(QueueStats {
375 queue,
376 weight: row.get::<_, i32>("weight") as u32,
377 unfinished_jobs,
378 max_unfinished_jobs: row
379 .get::<_, Option<i64>>("max_unfinished_jobs")
380 .map(|n| n as u64),
381 by_state: states,
382 counts_approximate: row.get("approx"),
383 arrival_rate: arrival,
384 drain_rate: drain,
385 time_to_drain_ms: ttd,
386 oldest_available_ms,
387 quiet_groups,
388 paused: row.get("paused"),
389 memory_bytes: row.get::<_, Option<i64>>("memory_bytes").map(|n| n as u64),
390 });
391 }
392 Ok(out)
393 }
394
395 async fn set_queue_paused(&self, queue: &str, paused: bool) -> Result<(), StoreError> {
396 let c = self.client().await?;
397 c.execute(
398 "INSERT INTO headgate_queue_state (queue, paused) VALUES ($1, $2)
399 ON CONFLICT (queue) DO UPDATE SET paused = EXCLUDED.paused",
400 &[&queue, &paused],
401 )
402 .await
403 .map_err(map_pg_err)?;
404 Ok(())
405 }
406
407 async fn set_queue_weight(&self, queue: &str, weight: u32) -> Result<(), StoreError> {
408 if weight == 0 {
409 return Err(StoreError::Invalid("weight must be >= 1".into()));
410 }
411 let weight =
412 i32::try_from(weight).map_err(|_| StoreError::Invalid("weight is too large".into()))?;
413 let c = self.client().await?;
414 c.execute(
415 "INSERT INTO headgate_queue_state (queue, weight) VALUES ($1, $2)
416 ON CONFLICT (queue) DO UPDATE SET
417 dispatch_count = floor(headgate_queue_state.dispatch_count::numeric
418 * EXCLUDED.weight / headgate_queue_state.weight)::bigint,
419 weight = EXCLUDED.weight",
420 &[&queue, &weight],
421 )
422 .await
423 .map_err(map_pg_err)?;
424 Ok(())
425 }
426
427 async fn set_enqueue_limit(
428 &self,
429 queue: &str,
430 max_unfinished_jobs: Option<u64>,
431 ) -> Result<(), StoreError> {
432 let limit = max_unfinished_jobs
433 .map(i64::try_from)
434 .transpose()
435 .map_err(|_| StoreError::Invalid("max_unfinished_jobs is too large".into()))?;
436 let c = self.client().await?;
437 c.execute(
438 "INSERT INTO headgate_enqueue_policy (queue, max_unfinished_jobs)
439 VALUES ($1, $2)
440 ON CONFLICT (queue) DO UPDATE
441 SET max_unfinished_jobs = EXCLUDED.max_unfinished_jobs",
442 &[&queue, &limit],
443 )
444 .await
445 .map_err(map_pg_err)?;
446 Ok(())
447 }
448
449 async fn rate_classes(&self) -> Result<Vec<RateClassState>, StoreError> {
450 let c = self.client().await?;
451 let sql = format!(
452 r#"
453 WITH p AS (SELECT {NOW_MS} AS now_ms)
454 SELECT b.name, b.burst, b.limit_per_window, b.window_ms,
455 CASE WHEN b.limit_per_window > 0 AND b.window_ms > 0
456 THEN LEAST(b.burst, b.tokens +
457 ((p.now_ms - b.refilled_at_ms) * b.limit_per_window / b.window_ms))
458 ELSE b.tokens END AS avail,
459 (SELECT count(*) FROM (
460 SELECT 1 FROM headgate_job w
461 WHERE w.state = 'available' AND w.rate_class = b.name LIMIT $1
462 ) t)::bigint AS waiting
463 FROM headgate_rate_bucket b, p
464 ORDER BY b.name
465 "#
466 );
467 let rows = c
468 .query(&sql, &[&POSITION_LIMIT])
469 .await
470 .map_err(map_pg_err)?;
471 Ok(rows
472 .iter()
473 .map(|r| {
474 let limit: i64 = r.get("limit_per_window");
475 RateClassState {
476 name: r.get("name"),
477 tokens_available: r.get("avail"),
478 burst: r.get("burst"),
479 limit_per_window: limit,
480 window_ms: r.get("window_ms"),
481 jobs_waiting: r.get("waiting"),
482 paused: limit == 0,
485 }
486 })
487 .collect())
488 }
489
490 async fn upsert_rate_class(&self, cfg: &RateClassConfig) -> Result<(), StoreError> {
491 if cfg.window_ms < 1 {
492 return Err(StoreError::Invalid("window_ms must be >= 1".into()));
495 }
496 if cfg.limit < 0 || cfg.burst < 1 {
497 return Err(StoreError::Invalid(
498 "limit must be >= 0 and burst >= 1".into(),
499 ));
500 }
501 let c = self.client().await?;
502 let (limit, tokens_insert) = if cfg.paused {
506 (0i64, 0i64)
507 } else {
508 (cfg.limit, cfg.burst)
509 };
510 let sql = format!(
511 r#"
512 INSERT INTO headgate_rate_bucket
513 (name, tokens, burst, limit_per_window, window_ms, refilled_at_ms)
514 SELECT $1, $2, $3, $4, $5, {NOW_MS}
515 ON CONFLICT (name) DO UPDATE SET
516 burst = EXCLUDED.burst,
517 limit_per_window = EXCLUDED.limit_per_window,
518 window_ms = EXCLUDED.window_ms,
519 tokens = CASE WHEN $6 THEN 0
520 ELSE LEAST(headgate_rate_bucket.tokens, EXCLUDED.burst) END,
521 refilled_at_ms = EXCLUDED.refilled_at_ms
522 "#
523 );
524 c.execute(
525 &sql,
526 &[
527 &cfg.name,
528 &tokens_insert,
529 &cfg.burst,
530 &limit,
531 &cfg.window_ms,
532 &cfg.paused,
533 ],
534 )
535 .await
536 .map_err(map_pg_err)?;
537 Ok(())
538 }
539
540 async fn concurrency_limits(&self) -> Result<Vec<ConcurrencyLimitConfig>, StoreError> {
541 let c = self.client().await?;
542 let rows = c
543 .query(
544 "SELECT name, queue, max_concurrent, on_saturated
545 FROM headgate_concurrency_limit ORDER BY name",
546 &[],
547 )
548 .await
549 .map_err(map_pg_err)?;
550 rows.iter()
551 .map(|r| {
552 let strategy: String = r.get("on_saturated");
553 Ok(ConcurrencyLimitConfig {
554 name: r.get("name"),
555 queue: r.get("queue"),
556 max_concurrent: r.get::<_, i64>("max_concurrent") as u64,
557 on_saturated: SaturationStrategy::try_from(strategy.as_str())?,
558 })
559 })
560 .collect()
561 }
562
563 async fn upsert_concurrency_limit(
564 &self,
565 cfg: &ConcurrencyLimitConfig,
566 ) -> Result<(), StoreError> {
567 if cfg.name.is_empty() || cfg.queue.is_empty() {
568 return Err(StoreError::Invalid(
569 "name and queue must not be empty".into(),
570 ));
571 }
572 if cfg.max_concurrent == 0 {
573 return Err(StoreError::Invalid("max_concurrent must be >= 1".into()));
574 }
575 let max_concurrent = i64::try_from(cfg.max_concurrent)
576 .map_err(|_| StoreError::Invalid("max_concurrent is too large".into()))?;
577 let c = self.client().await?;
578 c.execute(
579 "INSERT INTO headgate_concurrency_limit
580 (name, queue, max_concurrent, on_saturated)
581 VALUES ($1, $2, $3, $4)
582 ON CONFLICT (name) DO UPDATE SET
583 queue = EXCLUDED.queue,
584 max_concurrent = EXCLUDED.max_concurrent,
585 on_saturated = EXCLUDED.on_saturated",
586 &[
587 &cfg.name,
588 &cfg.queue,
589 &max_concurrent,
590 &cfg.on_saturated.as_str(),
591 ],
592 )
593 .await
594 .map_err(map_pg_err)?;
595 Ok(())
596 }
597
598 async fn partitions(&self, queue: &str) -> Result<Vec<PartitionState>, StoreError> {
599 let c = self.client().await?;
600 let rows = c
601 .query(
602 "WITH sample AS (
603 SELECT partition_key FROM headgate_job
604 WHERE queue = $1 AND state = 'available' LIMIT $2
605 ),
606 waiting AS (
607 SELECT partition_key, count(*)::bigint AS n FROM sample GROUP BY 1
608 )
609 SELECT COALESCE(w.partition_key, d.partition_key) AS partition_key,
610 COALESCE(d.deficit, 0) AS deficit,
611 COALESCE(w.n, 0) AS waiting
612 FROM waiting w
613 FULL OUTER JOIN headgate_partition_deficit d
614 ON d.queue = $1 AND d.partition_key = w.partition_key
615 WHERE d.queue IS NULL OR d.queue = $1
616 ORDER BY 1",
617 &[&queue, &SAMPLE_LIMIT],
618 )
619 .await
620 .map_err(map_pg_err)?;
621 Ok(rows
622 .iter()
623 .map(|r| PartitionState {
624 partition_key: r.get("partition_key"),
625 deficit: r.get("deficit"),
626 waiting: r.get("waiting"),
627 })
628 .collect())
629 }
630
631 async fn quarantine_list(&self) -> Result<Vec<QuarantineEntry>, StoreError> {
632 let c = self.client().await?;
633 let rows = c
634 .query(
635 "SELECT fingerprint, kind, crash_count, quarantined_at_ms,
636 COALESCE(reason, '') AS reason
637 FROM headgate_quarantine ORDER BY quarantined_at_ms DESC LIMIT $1",
638 &[&SAMPLE_LIMIT],
639 )
640 .await
641 .map_err(map_pg_err)?;
642 Ok(rows
643 .iter()
644 .map(|r| QuarantineEntry {
645 fingerprint: r.get("fingerprint"),
646 kind: r.get("kind"),
647 crash_count: r.get::<_, i32>("crash_count") as i64,
648 quarantined_at_ms: r.get("quarantined_at_ms"),
649 reason: r.get("reason"),
650 })
651 .collect())
652 }
653
654 async fn quarantine_release(&self, fingerprint: &str) -> Result<u64, StoreError> {
655 let c = self.client().await?;
656 let sql = format!(
657 r#"
658 WITH p AS (SELECT {NOW_MS} AS now_ms),
659 rel AS ( -- quarantined + operator_release -> available (the table's row)
660 UPDATE headgate_job j SET state = 'available', scheduled_at_ms = p.now_ms,
661 finalized_at_ms = NULL
662 FROM p WHERE j.fingerprint = $1 AND j.state = 'quarantined'
663 RETURNING j.queue, j.partition_key
664 ),
665 -- tenant fairness/adaptive admission released jobs are available again, so their partitions rejoin the
666 -- gate's set — in this statement, never a follow-up one.
667 active AS (
668 INSERT INTO headgate_active_partition (queue, partition_key)
669 SELECT DISTINCT queue, partition_key FROM rel
670 ON CONFLICT (queue, partition_key) DO UPDATE SET queue = EXCLUDED.queue
671 ),
672 del AS (
673 DELETE FROM headgate_quarantine WHERE fingerprint = $1 RETURNING 1
674 )
675 SELECT (SELECT count(*) FROM rel)::bigint AS released,
676 (SELECT count(*) FROM del)::bigint AS deleted
677 "#
678 );
679 let row = c
680 .query_one(&sql, &[&fingerprint])
681 .await
682 .map_err(map_pg_err)?;
683 let released: i64 = row.get("released");
684 let deleted: i64 = row.get("deleted");
685 if released == 0 && deleted == 0 {
686 return Err(StoreError::NotFound(format!(
687 "fingerprint {fingerprint} is not quarantined"
688 )));
689 }
690 Ok(released as u64)
691 }
692
693 async fn operator_retry(&self, id: &str) -> Result<(), StoreError> {
694 let c = self.client().await?;
695 let sql = format!(
696 r#"
697 WITH upd AS (
698 UPDATE headgate_job SET state = 'available', scheduled_at_ms = {NOW_MS},
699 finalized_at_ms = NULL
700 WHERE ulid = $1 AND state = 'archived'
701 RETURNING queue, partition_key
702 ),
703 -- tenant fairness/adaptive admission retry-now makes the row available; list its partition here.
704 active AS (
705 INSERT INTO headgate_active_partition (queue, partition_key)
706 SELECT queue, partition_key FROM upd
707 ON CONFLICT (queue, partition_key) DO UPDATE SET queue = EXCLUDED.queue
708 )
709 SELECT count(*)::bigint FROM upd
710 "#
711 );
712 let retried: i64 = c.query_one(&sql, &[&id]).await.map_err(map_pg_err)?.get(0);
713 if retried == 1 {
714 return Ok(());
715 }
716 match self.job_state(&c, id).await? {
717 None => Err(StoreError::NotFound(format!("job {id}"))),
718 Some(state) => Err(StoreError::Invalid(format!(
719 "operator_retry is only defined from archived; job {id} is {state}"
720 ))),
721 }
722 }
723
724 async fn operator_cancel(&self, id: &str) -> Result<(), StoreError> {
725 let c = self.client().await?;
726 let sql = format!(
732 "WITH pick AS (
733 SELECT j.id, j.queue, j.partition_key, (j.state = 'running') AS was_running
734 FROM headgate_job j
735 WHERE j.ulid = $1 AND j.state IN ('pending', 'scheduled', 'available', 'running')
736 FOR UPDATE
737 ),
738 upd AS (
739 UPDATE headgate_job j SET state = 'cancelled', lease_id = NULL,
740 lease_expires_at_ms = NULL, claimed_by = NULL,
741 finalized_at_ms = {NOW_MS}
742 WHERE j.id IN (SELECT id FROM pick)
743 RETURNING 1
744 ),
745 infl AS ({dec})
746 SELECT count(*)::bigint FROM upd",
747 dec = crate::inflight_dec_sql(
748 "(SELECT queue, partition_key FROM pick WHERE was_running)"
749 )
750 );
751 if c.query_one(&sql, &[&id])
752 .await
753 .map_err(map_pg_err)?
754 .get::<_, i64>(0)
755 == 1
756 {
757 return Ok(());
758 }
759 match self.job_state(&c, id).await? {
760 None => Err(StoreError::NotFound(format!("job {id}"))),
761 Some(state) => Err(StoreError::Invalid(format!(
762 "operator_cancel is not defined from {state}"
763 ))),
764 }
765 }
766
767 async fn delete_job(&self, id: &str) -> Result<(), StoreError> {
768 let c = self.client().await?;
769 if c.execute(
770 "DELETE FROM headgate_job WHERE ulid = $1 AND state <> 'running'",
771 &[&id],
772 )
773 .await
774 .map_err(map_pg_err)?
775 == 1
776 {
777 return Ok(());
778 }
779 match self.job_state(&c, id).await? {
780 None => Err(StoreError::NotFound(format!("job {id}"))),
781 Some(_) => Err(StoreError::Invalid(
782 "cannot delete a running job; cancel it first".into(),
783 )),
784 }
785 }
786
787 async fn explain_admission(&self, id: &str) -> Result<Option<AdmissionExplain>, StoreError> {
788 let c = self.client().await?;
789 let sql = format!(
790 r#"
791 SELECT j.state::text AS state, j.queue, j.scheduled_at_ms, j.priority,
792 j.rate_class, j.partition_key, j.fingerprint, j.id,
793 j.weight::bigint AS weight,
794 {NOW_MS} AS now_ms,
795 COALESCE(qs.paused, false) AS paused,
796 (q.fingerprint IS NOT NULL) AS quarantined,
797 b.burst, b.limit_per_window, b.window_ms,
798 CASE WHEN b.name IS NULL THEN NULL
799 WHEN b.limit_per_window > 0 AND b.window_ms > 0
800 THEN LEAST(b.burst, b.tokens +
801 (({NOW_MS} - b.refilled_at_ms) * b.limit_per_window / b.window_ms))
802 ELSE b.tokens END AS avail,
803 COALESCE(d.deficit, 0) AS deficit,
804 cl.max_concurrent, cl.on_saturated,
805 -- adaptive admission read the counter the GATE reads, not a fresh count of running
806 -- rows. "Why is this job not running" must answer for the gate that
807 -- is actually deciding: if headgate_inflight ever drifts, an explain
808 -- that quietly recomputed the truth would report a ceiling as clear
809 -- while admission kept refusing, which is the one failure this
810 -- endpoint exists to make visible. Also O(1) instead of O(running).
811 COALESCE((SELECT f.n FROM headgate_inflight f
812 WHERE f.queue = j.queue
813 AND f.partition_key = j.partition_key), 0) AS inflight,
814 (SELECT COALESCE(sum(t.weight), 0)::bigint FROM (
815 SELECT a.weight FROM headgate_job a
816 WHERE a.state = 'available' AND a.queue = j.queue
817 AND a.rate_class = j.rate_class
818 AND (a.priority > j.priority
819 OR (a.priority = j.priority
820 AND (a.scheduled_at_ms, a.id) < (j.scheduled_at_ms, j.id)))
821 ORDER BY a.priority DESC, a.scheduled_at_ms, a.id
822 LIMIT $2
823 ) t) AS cost_ahead_in_class,
824 (SELECT count(*) FROM (
825 SELECT 1 FROM headgate_job a
826 WHERE a.state = 'available' AND a.queue = j.queue
827 AND a.partition_key = j.partition_key
828 AND (a.priority > j.priority
829 OR (a.priority = j.priority
830 AND (a.scheduled_at_ms, a.id) < (j.scheduled_at_ms, j.id)))
831 LIMIT $2
832 ) t)::bigint AS ahead_in_partition
833 FROM headgate_job j
834 LEFT JOIN headgate_queue_state qs ON qs.queue = j.queue
835 LEFT JOIN headgate_quarantine q ON q.fingerprint = j.fingerprint
836 LEFT JOIN headgate_rate_bucket b ON b.name = j.rate_class AND j.rate_class <> ''
837 LEFT JOIN headgate_partition_deficit d
838 ON d.queue = j.queue AND d.partition_key = j.partition_key
839 LEFT JOIN headgate_concurrency_limit cl ON cl.queue = j.queue
840 WHERE j.ulid = $1
841 "#
842 );
843 let Some(row) = c
844 .query_opt(&sql, &[&id, &POSITION_LIMIT])
845 .await
846 .map_err(map_pg_err)?
847 else {
848 return Ok(None);
849 };
850 Ok(Some(assemble_explain(&row)))
851 }
852
853 async fn history(
854 &self,
855 queue: &str,
856 since_ms: i64,
857 bucket_ms: i64,
858 ) -> Result<Vec<HistoryBucket>, StoreError> {
859 if bucket_ms < 60_000 {
860 return Err(StoreError::Invalid(
861 "bucket_ms must be >= 60000 (the stored granularity)".into(),
862 ));
863 }
864 let c = self.client().await?;
865 let rows = c
866 .query(
867 "SELECT (bucket_ms / $2) * $2 AS at_ms,
868 sum(arrived)::bigint AS arrived, sum(completed)::bigint AS completed
869 FROM headgate_queue_counter
870 WHERE queue = $1 AND bucket_ms >= $3
871 GROUP BY 1 ORDER BY 1 LIMIT 10000",
872 &[&queue, &bucket_ms, &since_ms],
873 )
874 .await
875 .map_err(map_pg_err)?;
876 Ok(rows
877 .iter()
878 .map(|r| HistoryBucket {
879 at_ms: r.get("at_ms"),
880 arrived: r.get("arrived"),
881 completed: r.get("completed"),
882 })
883 .collect())
884 }
885
886 async fn quarantine_sweep(&self, limit: i64) -> Result<u64, StoreError> {
887 let c = self.client().await?;
888 let sql = format!(
891 r#"
892 WITH pick AS (
893 SELECT j.id FROM headgate_job j
894 WHERE j.state IN ('pending', 'available', 'scheduled', 'retryable')
895 AND j.fingerprint IN (SELECT fingerprint FROM headgate_quarantine)
896 LIMIT $1
897 FOR UPDATE SKIP LOCKED
898 )
899 UPDATE headgate_job j
900 SET state = 'quarantined', finalized_at_ms = {NOW_MS}
901 WHERE j.id IN (SELECT id FROM pick)
902 "#
903 );
904 c.execute(&sql, &[&limit]).await.map_err(map_pg_err)
905 }
906
907 async fn reschedule_job(&self, id: &str, at_ms: i64) -> Result<(), StoreError> {
908 let c = self.client().await?;
909 let n = c
912 .execute(
913 "UPDATE headgate_job SET scheduled_at_ms = $2
914 WHERE ulid = $1 AND state IN ('scheduled', 'retryable')",
915 &[&id, &at_ms],
916 )
917 .await
918 .map_err(map_pg_err)?;
919 if n == 1 {
920 return Ok(());
921 }
922 match self.job_state(&c, id).await? {
923 None => Err(StoreError::NotFound(format!("job {id}"))),
924 Some(state) => Err(StoreError::Invalid(format!(
925 "reschedule is only defined for scheduled/retryable; job {id} is {state}"
926 ))),
927 }
928 }
929
930 async fn edit_payload(
931 &self,
932 id: &str,
933 payload: &[u8],
934 schema_version: u32,
935 fingerprint: &str,
936 ) -> Result<(), StoreError> {
937 let c = self.client().await?;
938 let n = c
939 .execute(
940 "UPDATE headgate_job
941 SET payload = $2, schema_version = $3, fingerprint = $4
942 WHERE ulid = $1 AND state <> 'running'",
943 &[&id, &payload, &(schema_version as i32), &fingerprint],
944 )
945 .await
946 .map_err(map_pg_err)?;
947 if n == 1 {
948 return Ok(());
949 }
950 match self.job_state(&c, id).await? {
951 None => Err(StoreError::NotFound(format!("job {id}"))),
952 Some(_) => Err(StoreError::Invalid(
953 "cannot edit a running job's payload".into(),
954 )),
955 }
956 }
957
958 async fn upsert_schedule(&self, s: &Schedule) -> Result<(), StoreError> {
959 let c = self.client().await?;
960 let sql = format!(
961 r#"
962 INSERT INTO headgate_schedule AS d
963 (id, kind, payload, queue, partition_key, rate_class, priority,
964 max_attempts, retention_ms, spec, next_run_ms, on_missed,
965 backfill_limit, paused, updated_at_ms)
966 SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, {NOW_MS}
967 ON CONFLICT (id) DO UPDATE SET
968 kind = EXCLUDED.kind, payload = EXCLUDED.payload, queue = EXCLUDED.queue,
969 partition_key = EXCLUDED.partition_key, rate_class = EXCLUDED.rate_class,
970 priority = EXCLUDED.priority, max_attempts = EXCLUDED.max_attempts,
971 retention_ms = EXCLUDED.retention_ms, spec = EXCLUDED.spec,
972 -- Idempotent (BullMQ upsertJobScheduler): an unchanged spec keeps its
973 -- phase; only a NEW spec resets next_run.
974 next_run_ms = CASE WHEN d.spec = EXCLUDED.spec
975 THEN d.next_run_ms ELSE EXCLUDED.next_run_ms END,
976 on_missed = EXCLUDED.on_missed, backfill_limit = EXCLUDED.backfill_limit,
977 paused = EXCLUDED.paused, updated_at_ms = EXCLUDED.updated_at_ms
978 "#
979 );
980 c.execute(
981 &sql,
982 &[
983 &s.id,
984 &s.kind,
985 &s.payload,
986 &s.queue,
987 &s.partition_key,
988 &s.rate_class,
989 &s.priority,
990 &(s.max_attempts as i32),
991 &s.retention_ms,
992 &s.spec,
993 &s.next_run_ms,
994 &s.on_missed.as_str(),
995 &(s.backfill_limit as i32),
996 &s.paused,
997 ],
998 )
999 .await
1000 .map_err(map_pg_err)?;
1001 Ok(())
1002 }
1003
1004 async fn delete_schedule(&self, id: &str) -> Result<(), StoreError> {
1005 let c = self.client().await?;
1006 if c.execute("DELETE FROM headgate_schedule WHERE id = $1", &[&id])
1007 .await
1008 .map_err(map_pg_err)?
1009 == 0
1010 {
1011 return Err(StoreError::NotFound(format!("schedule {id}")));
1012 }
1013 Ok(())
1014 }
1015
1016 async fn list_schedules(&self) -> Result<Vec<Schedule>, StoreError> {
1017 let c = self.client().await?;
1018 let rows = c
1019 .query(
1020 "SELECT * FROM headgate_schedule ORDER BY id LIMIT 10000",
1021 &[],
1022 )
1023 .await
1024 .map_err(map_pg_err)?;
1025 Ok(rows.iter().map(schedule_from_row).collect())
1026 }
1027
1028 async fn due_schedules(&self, limit: i64) -> Result<(Vec<Schedule>, i64), StoreError> {
1029 let c = self.client().await?;
1030 let sql = format!(
1031 "SELECT *, {NOW_MS} AS now_ms FROM headgate_schedule
1032 WHERE NOT paused AND next_run_ms <= {NOW_MS}
1033 ORDER BY next_run_ms LIMIT $1"
1034 );
1035 let rows = c.query(&sql, &[&limit]).await.map_err(map_pg_err)?;
1036 let now = rows.first().map(|r| r.get("now_ms")).unwrap_or(0);
1037 Ok((rows.iter().map(schedule_from_row).collect(), now))
1038 }
1039
1040 async fn advance_schedule(
1041 &self,
1042 id: &str,
1043 from_next_run_ms: i64,
1044 to_next_run_ms: i64,
1045 ) -> Result<bool, StoreError> {
1046 let c = self.client().await?;
1047 let sql = format!(
1048 "UPDATE headgate_schedule
1049 SET next_run_ms = $3, last_enqueued_ms = {NOW_MS}
1050 WHERE id = $1 AND next_run_ms = $2"
1051 );
1052 let n = c
1053 .execute(&sql, &[&id, &from_next_run_ms, &to_next_run_ms])
1054 .await
1055 .map_err(map_pg_err)?;
1056 Ok(n == 1)
1057 }
1058
1059 async fn record_schedule_event(&self, event: &ScheduleEvent) -> Result<(), StoreError> {
1060 if event.reason.len() > 64 {
1061 return Err(StoreError::Invalid(
1062 "schedule event reason exceeds 64 bytes".into(),
1063 ));
1064 }
1065 let mut c = self.client().await?;
1066 let tx = c.transaction().await.map_err(map_pg_err)?;
1067 let _ = tx
1071 .query(
1072 "SELECT id FROM headgate_schedule WHERE id = $1 FOR UPDATE",
1073 &[&event.schedule_id],
1074 )
1075 .await
1076 .map_err(map_pg_err)?;
1077 let sql = format!(
1078 "INSERT INTO headgate_schedule_event
1079 (schedule_id, tick_ms, job_id, outcome, reason, recorded_at_ms)
1080 VALUES ($1, $2, $3, $4, $5, {NOW_MS})"
1081 );
1082 tx.execute(
1083 &sql,
1084 &[
1085 &event.schedule_id,
1086 &event.tick_ms,
1087 &event.job_id,
1088 &event.outcome.as_str(),
1089 &event.reason,
1090 ],
1091 )
1092 .await
1093 .map_err(map_pg_err)?;
1094 tx.execute(
1095 "DELETE FROM headgate_schedule_event
1096 WHERE schedule_id = $1 AND id NOT IN (
1097 SELECT id FROM headgate_schedule_event WHERE schedule_id = $1
1098 ORDER BY id DESC LIMIT $2
1099 )",
1100 &[&event.schedule_id, &(SCHEDULE_EVENT_LIMIT as i64)],
1101 )
1102 .await
1103 .map_err(map_pg_err)?;
1104 tx.commit().await.map_err(map_pg_err)
1105 }
1106
1107 async fn list_schedule_events(
1108 &self,
1109 schedule_id: &str,
1110 before_event_id: Option<u64>,
1111 limit: u32,
1112 ) -> Result<Vec<ScheduleEvent>, StoreError> {
1113 if limit == 0 || limit > SCHEDULE_EVENT_LIMIT {
1114 return Err(StoreError::Invalid(
1115 "schedule event limit must be between 1 and 100".into(),
1116 ));
1117 }
1118 let c = self.client().await?;
1119 let rows = c
1120 .query(
1121 "SELECT id, schedule_id, tick_ms, job_id, outcome, reason, recorded_at_ms
1122 FROM headgate_schedule_event WHERE schedule_id = $1
1123 AND ($2::bigint IS NULL OR id < $2)
1124 ORDER BY id DESC LIMIT $3",
1125 &[
1126 &schedule_id,
1127 &before_event_id.map(|id| id as i64),
1128 &(limit as i64),
1129 ],
1130 )
1131 .await
1132 .map_err(map_pg_err)?;
1133 rows.into_iter()
1134 .map(|row| {
1135 let raw: String = row.get("outcome");
1136 let outcome = ScheduleEventOutcome::parse(&raw).ok_or_else(|| {
1137 StoreError::Invalid(format!("invalid stored schedule outcome {raw}"))
1138 })?;
1139 Ok(ScheduleEvent {
1140 event_id: row.get::<_, i64>("id") as u64,
1141 schedule_id: row.get("schedule_id"),
1142 tick_ms: row.get("tick_ms"),
1143 job_id: row.get("job_id"),
1144 outcome,
1145 reason: row.get("reason"),
1146 recorded_at_ms: row.get("recorded_at_ms"),
1147 })
1148 })
1149 .collect()
1150 }
1151
1152 async fn heartbeat_worker(&self, w: &WorkerMeta) -> Result<Option<String>, StoreError> {
1153 let c = self.client().await?;
1154 let status = if w.status.is_empty() {
1155 "running"
1156 } else {
1157 &w.status
1158 };
1159 let sql = format!(
1160 r#"
1161 INSERT INTO headgate_worker
1162 (worker_id, host, pid, queues, concurrency, started_at_ms, heartbeat_at_ms,
1163 inflight, polls, empty_polls, status, duties_active)
1164 SELECT $1, $2, $3, $4, $5, $6, {NOW_MS}, $7, $8, $9, $10, $11
1165 ON CONFLICT (worker_id) DO UPDATE SET
1166 queues = EXCLUDED.queues, concurrency = EXCLUDED.concurrency,
1167 heartbeat_at_ms = EXCLUDED.heartbeat_at_ms,
1168 -- ADDITIVE: the cluster view's and backlog metrics's inputs are LEVELS,
1169 -- so the beat overwrites them rather than accumulating. A worker that
1170 -- stops beating keeps its last reported level and ages out as stale.
1171 inflight = EXCLUDED.inflight, polls = EXCLUDED.polls,
1172 empty_polls = EXCLUDED.empty_polls, status = EXCLUDED.status,
1173 duties_active = EXCLUDED.duties_active
1174 RETURNING command
1175 "#
1176 );
1177 let row = c
1178 .query_one(
1179 &sql,
1180 &[
1181 &w.worker_id,
1182 &w.host,
1183 &w.pid,
1184 &w.queues,
1185 &(w.concurrency as i32),
1186 &w.started_at_ms,
1187 &(w.inflight as i32),
1188 &(w.polls as i64),
1189 &(w.empty_polls as i64),
1190 &status,
1191 &w.duties_active,
1192 ],
1193 )
1194 .await
1195 .map_err(map_pg_err)?;
1196 Ok(row.get(0))
1197 }
1198
1199 async fn signal_worker(
1200 &self,
1201 worker_id: &str,
1202 command: Option<&str>,
1203 ) -> Result<(), StoreError> {
1204 if let Some(cmd) = command {
1205 if !matches!(cmd, "quiet" | "resume" | "restart" | "terminate" | "resign") {
1206 return Err(StoreError::Invalid(
1207 "command must be quiet, resume, restart, terminate, or resign".into(),
1208 ));
1209 }
1210 }
1211 let c = self.client().await?;
1212 let n = c
1213 .execute(
1214 "UPDATE headgate_worker SET command = $2 WHERE worker_id = $1",
1215 &[&worker_id, &command],
1216 )
1217 .await
1218 .map_err(map_pg_err)?;
1219 if n == 0 {
1220 return Err(StoreError::NotFound(format!("worker {worker_id}")));
1221 }
1222 Ok(())
1223 }
1224
1225 async fn distinct_kinds(&self, limit: i64) -> Result<Vec<String>, StoreError> {
1226 let c = self.client().await?;
1227 let rows = c
1229 .query(
1230 "SELECT DISTINCT kind FROM (
1231 SELECT kind FROM headgate_job
1232 WHERE state IN ('available', 'scheduled', 'retryable')
1233 LIMIT $1
1234 ) t ORDER BY kind",
1235 &[&limit],
1236 )
1237 .await
1238 .map_err(map_pg_err)?;
1239 Ok(rows.iter().map(|r| r.get(0)).collect())
1240 }
1241
1242 async fn list_workers(&self, stale_after_ms: i64) -> Result<Vec<WorkerMeta>, StoreError> {
1243 let c = self.client().await?;
1244 let sql = format!(
1245 "SELECT * FROM headgate_worker
1246 WHERE heartbeat_at_ms >= {NOW_MS} - $1
1247 ORDER BY worker_id LIMIT 10000"
1248 );
1249 let rows = c
1250 .query(&sql, &[&stale_after_ms])
1251 .await
1252 .map_err(map_pg_err)?;
1253 Ok(rows
1254 .iter()
1255 .map(|r| WorkerMeta {
1256 worker_id: r.get("worker_id"),
1257 host: r.get("host"),
1258 pid: r.get("pid"),
1259 queues: r.get("queues"),
1260 concurrency: r.get::<_, i32>("concurrency") as u32,
1261 started_at_ms: r.get("started_at_ms"),
1262 heartbeat_at_ms: r.get("heartbeat_at_ms"),
1263 inflight: r.get::<_, i32>("inflight") as u32,
1264 polls: r.get::<_, i64>("polls") as u64,
1265 empty_polls: r.get::<_, i64>("empty_polls") as u64,
1266 status: r.get("status"),
1267 duties_active: r.get("duties_active"),
1268 pending_command: r.get("command"),
1269 })
1270 .collect())
1271 }
1272
1273 async fn create_operation(&self, req: &BulkRequest) -> Result<(), StoreError> {
1274 if req.queue.is_none()
1275 && req.state.is_none()
1276 && req.kind.is_none()
1277 && req.partition_key.is_none()
1278 && req.older_than_ms.is_none()
1279 {
1280 return Err(StoreError::Invalid("empty selector is rejected".into()));
1282 }
1283 let allowed = action_states(&req.action)
1284 .ok_or_else(|| StoreError::Invalid(format!("unknown action `{}`", req.action)))?;
1285 let c = self.client().await?;
1286 let (where_sql, params) = selector_where(req, allowed, 2);
1288 let est_sql = format!(
1289 "SELECT count(*)::bigint FROM (SELECT 1 FROM headgate_job j WHERE {where_sql} LIMIT $1) t"
1290 );
1291 let mut est_params: Vec<&(dyn ToSql + Sync)> = vec![&SAMPLE_LIMIT];
1292 est_params.extend(params.iter().map(|p| &**p as &(dyn ToSql + Sync)));
1293 let estimated: i64 = c
1294 .query_one(&est_sql, &est_params)
1295 .await
1296 .map_err(map_pg_err)?
1297 .get(0);
1298 let selector = serde_json::json!({
1299 "queue": req.queue, "state": req.state, "kind": req.kind,
1300 "partition_key": req.partition_key, "older_than_ms": req.older_than_ms,
1301 });
1302 let status = if req.dry_run { "completed" } else { "pending" };
1303 let sql = format!(
1304 "INSERT INTO headgate_operation
1305 (id, action, selector, status, total_estimated, dry_run, created_at_ms)
1306 VALUES ($1, $2, $3, $4, $5, $6, {NOW_MS})"
1307 );
1308 c.execute(
1309 &sql,
1310 &[
1311 &req.id,
1312 &req.action,
1313 &selector,
1314 &status,
1315 &estimated,
1316 &req.dry_run,
1317 ],
1318 )
1319 .await
1320 .map_err(map_pg_err)?;
1321 Ok(())
1322 }
1323
1324 async fn get_operation(&self, id: &str) -> Result<Option<OperationStatus>, StoreError> {
1325 let c = self.client().await?;
1326 Ok(
1327 c.query_opt("SELECT * FROM headgate_operation WHERE id = $1", &[&id])
1328 .await
1329 .map_err(map_pg_err)?
1330 .map(|r| OperationStatus {
1331 id: r.get("id"),
1332 status: r.get("status"),
1333 affected: r.get("affected"),
1334 total_estimated: r.get("total_estimated"),
1335 dry_run: r.get("dry_run"),
1336 error: r.get("error"),
1337 }),
1338 )
1339 }
1340
1341 async fn run_pending_operations(&self, batch: i64) -> Result<u64, StoreError> {
1342 let c = self.client().await?;
1343 let ops = c
1344 .query(
1345 "SELECT id, action, selector FROM headgate_operation
1346 WHERE status IN ('pending', 'running')
1347 ORDER BY created_at_ms LIMIT 5",
1348 &[],
1349 )
1350 .await
1351 .map_err(map_pg_err)?;
1352 let mut total = 0u64;
1353 for op in &ops {
1354 let id: String = op.get("id");
1355 let action: String = op.get("action");
1356 let selector: serde_json::Value = op.get("selector");
1357 let req = BulkRequest {
1358 id: id.clone(),
1359 action: action.clone(),
1360 queue: selector
1361 .get("queue")
1362 .and_then(|v| v.as_str())
1363 .map(String::from),
1364 state: selector
1365 .get("state")
1366 .and_then(|v| v.as_str())
1367 .map(String::from),
1368 kind: selector
1369 .get("kind")
1370 .and_then(|v| v.as_str())
1371 .map(String::from),
1372 partition_key: selector
1373 .get("partition_key")
1374 .and_then(|v| v.as_str())
1375 .map(String::from),
1376 older_than_ms: selector.get("older_than_ms").and_then(|v| v.as_i64()),
1377 dry_run: false,
1378 };
1379 let n = match self.run_operation_batch(&c, &req, batch).await {
1380 Ok(n) => n,
1381 Err(e) => {
1382 let _ = c
1383 .execute(
1384 "UPDATE headgate_operation SET status = 'failed', error = $2 WHERE id = $1",
1385 &[&id, &e.to_string()],
1386 )
1387 .await;
1388 continue;
1389 }
1390 };
1391 total += n;
1392 let done = (n as i64) < batch;
1393 let status = if done { "completed" } else { "running" };
1394 c.execute(
1395 "UPDATE headgate_operation SET status = $2, affected = affected + $3 WHERE id = $1",
1396 &[&id, &status, &(n as i64)],
1397 )
1398 .await
1399 .map_err(map_pg_err)?;
1400 }
1401 Ok(total)
1402 }
1403
1404 async fn promote_job(&self, id: &str) -> Result<(), StoreError> {
1405 let c = self.client().await?;
1406 let n: i64 = c
1407 .query_one(
1408 &format!(
1409 "WITH moved AS (
1410 UPDATE headgate_job SET state = 'available', scheduled_at_ms = {NOW_MS}
1411 WHERE ulid = $1 AND state = 'pending'
1412 RETURNING queue, partition_key
1413 ), active AS (
1414 INSERT INTO headgate_active_partition (queue, partition_key)
1415 SELECT queue, partition_key FROM moved
1416 ON CONFLICT (queue, partition_key) DO UPDATE SET queue = EXCLUDED.queue
1417 ) SELECT count(*) FROM moved"
1418 ),
1419 &[&id],
1420 )
1421 .await
1422 .map_err(map_pg_err)?
1423 .get(0);
1424 if n == 0 {
1425 return Err(StoreError::Invalid(
1426 "operator_promote is defined only from pending".into(),
1427 ));
1428 }
1429 Ok(())
1430 }
1431
1432 async fn delete_queue(&self, queue: &str, force: bool) -> Result<Option<String>, StoreError> {
1433 let mut c = self.client().await?;
1434 let tx = c.transaction().await.map_err(map_pg_err)?;
1435 tx.execute(
1436 "INSERT INTO headgate_enqueue_policy(queue) VALUES($1) ON CONFLICT(queue) DO NOTHING",
1437 &[&queue],
1438 )
1439 .await
1440 .map_err(map_pg_err)?;
1441 tx.query(
1442 "SELECT queue FROM headgate_enqueue_policy WHERE queue=$1 FOR UPDATE",
1443 &[&queue],
1444 )
1445 .await
1446 .map_err(map_pg_err)?;
1447 let depth: i64 = tx.query_one(
1448 "SELECT GREATEST(0,
1449 COALESCE((SELECT n FROM headgate_enqueue_counter WHERE queue=$1 AND counter_kind='entered'),0) -
1450 COALESCE((SELECT n FROM headgate_enqueue_counter WHERE queue=$1 AND counter_kind='exited'),0))",
1451 &[&queue],
1452 ).await.map_err(map_pg_err)?.get(0);
1453 if depth > 0 && !force {
1454 return Err(StoreError::Invalid(
1455 "queue is not empty; retry with force=true".into(),
1456 ));
1457 }
1458 if depth == 0 {
1459 tx.execute("DELETE FROM headgate_queue_state WHERE queue=$1", &[&queue])
1460 .await
1461 .map_err(map_pg_err)?;
1462 tx.execute(
1463 "DELETE FROM headgate_enqueue_policy WHERE queue=$1",
1464 &[&queue],
1465 )
1466 .await
1467 .map_err(map_pg_err)?;
1468 tx.commit().await.map_err(map_pg_err)?;
1469 return Ok(None);
1470 }
1471 tx.execute(
1472 "UPDATE headgate_enqueue_policy SET max_unfinished_jobs=0 WHERE queue=$1",
1473 &[&queue],
1474 )
1475 .await
1476 .map_err(map_pg_err)?;
1477 let now: i64 = tx
1478 .query_one(&format!("SELECT {NOW_MS}"), &[])
1479 .await
1480 .map_err(map_pg_err)?
1481 .get(0);
1482 tx.commit().await.map_err(map_pg_err)?;
1483 let id = format!(
1484 "qdel-{now}-{}",
1485 queue.replace(|c: char| !c.is_ascii_alphanumeric(), "_")
1486 );
1487 self.create_operation(&BulkRequest {
1488 id: id.clone(),
1489 action: "delete".into(),
1490 queue: Some(queue.into()),
1491 state: None,
1492 kind: None,
1493 partition_key: None,
1494 older_than_ms: None,
1495 dry_run: false,
1496 })
1497 .await?;
1498 Ok(Some(id))
1499 }
1500
1501 async fn sample_queue_memory(&self, limit: u32) -> Result<u32, StoreError> {
1502 let limit = limit.clamp(1, 1_000) as i64;
1503 let c = self.client().await?;
1504 let rows = c.query(
1505 &format!(
1506 "WITH queues AS (SELECT queue FROM headgate_queue_state ORDER BY queue LIMIT 200),
1507 samples AS (
1508 SELECT q.queue, COALESCE(sum(pg_column_size(j.*)),0)::bigint AS bytes, count(*)::int AS n
1509 FROM queues q LEFT JOIN LATERAL (
1510 SELECT j FROM headgate_job j WHERE j.queue=q.queue ORDER BY j.id DESC LIMIT $1
1511 ) x(j) ON TRUE GROUP BY q.queue
1512 )
1513 INSERT INTO headgate_queue_sample(queue,memory_bytes,sampled_jobs,sampled_at_ms)
1514 SELECT queue,bytes,n,{NOW_MS} FROM samples
1515 ON CONFLICT(queue) DO UPDATE SET memory_bytes=EXCLUDED.memory_bytes,
1516 sampled_jobs=EXCLUDED.sampled_jobs,sampled_at_ms=EXCLUDED.sampled_at_ms
1517 RETURNING queue"
1518 ), &[&limit]
1519 ).await.map_err(map_pg_err)?;
1520 Ok(rows.len() as u32)
1521 }
1522}
1523
1524#[async_trait::async_trait]
1525impl ResultInspect for PgStore {
1526 async fn get_job_result(&self, id: &str) -> Result<Option<JobResult>, StoreError> {
1527 let c = self.client().await?;
1528 let row = c
1529 .query_opt(
1530 "SELECT result_schema_version, result_bytes
1531 FROM headgate_job
1532 WHERE ulid = $1 AND result_schema_version IS NOT NULL",
1533 &[&id],
1534 )
1535 .await
1536 .map_err(map_pg_err)?;
1537 Ok(row.map(|row| JobResult {
1538 schema_version: row.get::<_, i32>(0) as u32,
1539 bytes: row.get(1),
1540 }))
1541 }
1542}
1543
1544#[async_trait::async_trait]
1545impl OutputInspect for PgStore {
1546 async fn get_job_output(&self, id: &str) -> Result<Option<JobOutput>, StoreError> {
1547 let c = self.client().await?;
1548 let row = c
1549 .query_opt(
1550 "SELECT output_schema_version, output_bytes, output_fence, output_updated_at_ms
1551 FROM headgate_job
1552 WHERE ulid = $1 AND output_schema_version IS NOT NULL",
1553 &[&id],
1554 )
1555 .await
1556 .map_err(map_pg_err)?;
1557 Ok(row.map(|row| JobOutput {
1558 schema_version: row.get::<_, i32>(0) as u32,
1559 bytes: row.get(1),
1560 fence: row.get::<_, i64>(2) as u64,
1561 updated_at_ms: row.get(3),
1562 }))
1563 }
1564}
1565
1566#[async_trait::async_trait]
1567impl ProgressInspect for PgStore {
1568 async fn get_job_progress(&self, id: &str) -> Result<Option<JobProgress>, StoreError> {
1569 let c = self.client().await?;
1570 let row = c
1571 .query_opt(
1572 "SELECT progress_current, progress_total, progress_message,
1573 progress_fence, progress_updated_at_ms
1574 FROM headgate_job
1575 WHERE ulid = $1 AND progress_current IS NOT NULL",
1576 &[&id],
1577 )
1578 .await
1579 .map_err(map_pg_err)?;
1580 Ok(row.map(|row| JobProgress {
1581 current: row.get::<_, i64>(0) as u64,
1582 total: row.get::<_, i64>(1) as u64,
1583 message: row.get(2),
1584 fence: row.get::<_, i64>(3) as u64,
1585 updated_at_ms: row.get(4),
1586 }))
1587 }
1588}
1589
1590#[async_trait::async_trait]
1591impl CheckpointInspect for PgStore {
1592 async fn get_job_checkpoint(&self, id: &str) -> Result<Option<Checkpoint>, StoreError> {
1593 let c = self.client().await?;
1594 let row = c
1595 .query_opt(
1596 "SELECT checkpoint, cp_cursor FROM headgate_job WHERE ulid = $1",
1597 &[&id],
1598 )
1599 .await
1600 .map_err(map_pg_err)?;
1601 Ok(row.map(|row| {
1602 crate::decode_checkpoint(
1603 row.get::<_, Option<serde_json::Value>>(0),
1604 row.get::<_, Option<Vec<u8>>>(1),
1605 )
1606 }))
1607 }
1608}
1609
1610fn schedule_from_row(r: &tokio_postgres::Row) -> Schedule {
1611 Schedule {
1612 id: r.get("id"),
1613 kind: r.get("kind"),
1614 payload: r.get("payload"),
1615 queue: r.get("queue"),
1616 partition_key: r.get("partition_key"),
1617 rate_class: r.get("rate_class"),
1618 priority: r.get("priority"),
1619 max_attempts: r.get::<_, i32>("max_attempts") as u32,
1620 retention_ms: r.get("retention_ms"),
1621 spec: r.get("spec"),
1622 next_run_ms: r.get("next_run_ms"),
1623 last_enqueued_ms: r.get("last_enqueued_ms"),
1624 on_missed: MissedPolicy::parse(r.get("on_missed")).unwrap_or(MissedPolicy::Skip),
1625 backfill_limit: r.get::<_, i32>("backfill_limit") as u32,
1626 paused: r.get("paused"),
1627 }
1628}
1629
1630fn action_states(action: &str) -> Option<&'static str> {
1632 match action {
1633 "retry" => Some("('archived')"),
1634 "cancel" => Some("('scheduled', 'available', 'running')"),
1635 "delete" => Some(
1636 "('scheduled', 'available', 'retryable', 'completed', 'archived', 'cancelled', 'quarantined', 'undecodable')",
1637 ),
1638 _ => None,
1639 }
1640}
1641
1642fn selector_where(
1644 req: &BulkRequest,
1645 allowed_states: &'static str,
1646 first_param: usize,
1647) -> (String, Vec<Box<dyn ToSql + Sync + Send>>) {
1648 let mut clauses = vec![format!("j.state IN {allowed_states}")];
1649 let mut params: Vec<Box<dyn ToSql + Sync + Send>> = Vec::new();
1650 let push = |clauses: &mut Vec<String>,
1651 params: &mut Vec<Box<dyn ToSql + Sync + Send>>,
1652 sql: &str,
1653 v: Box<dyn ToSql + Sync + Send>| {
1654 params.push(v);
1655 clauses.push(sql.replace("{}", &(params.len() + first_param - 1).to_string()));
1656 };
1657 if let Some(q) = &req.queue {
1658 push(
1659 &mut clauses,
1660 &mut params,
1661 "j.queue = ${}",
1662 Box::new(q.clone()),
1663 );
1664 }
1665 if let Some(s) = &req.state {
1666 push(
1667 &mut clauses,
1668 &mut params,
1669 "j.state::text = ${}",
1670 Box::new(s.clone()),
1671 );
1672 }
1673 if let Some(k) = &req.kind {
1674 push(
1675 &mut clauses,
1676 &mut params,
1677 "j.kind = ${}",
1678 Box::new(k.clone()),
1679 );
1680 }
1681 if let Some(p) = &req.partition_key {
1682 push(
1683 &mut clauses,
1684 &mut params,
1685 "j.partition_key = ${}",
1686 Box::new(p.clone()),
1687 );
1688 }
1689 if let Some(age) = req.older_than_ms {
1690 push(
1691 &mut clauses,
1692 &mut params,
1693 &format!("j.enqueued_at_ms < {NOW_MS} - ${{}}"),
1694 Box::new(age),
1695 );
1696 }
1697 (clauses.join(" AND "), params)
1698}
1699
1700impl PgStore {
1701 async fn run_operation_batch(
1704 &self,
1705 c: &crate::PgClient,
1706 req: &BulkRequest,
1707 batch: i64,
1708 ) -> Result<u64, StoreError> {
1709 let allowed = action_states(&req.action)
1710 .ok_or_else(|| StoreError::Invalid(format!("unknown action `{}`", req.action)))?;
1711 let (where_sql, params) = selector_where(req, allowed, 2);
1712 let pick = format!(
1713 "SELECT j.id FROM headgate_job j WHERE {where_sql} ORDER BY j.id LIMIT $1 FOR UPDATE SKIP LOCKED"
1714 );
1715 let pick_state = format!(
1719 "SELECT j.id, j.queue, j.partition_key, (j.state = 'running') AS was_running
1720 FROM headgate_job j WHERE {where_sql} ORDER BY j.id LIMIT $1 FOR UPDATE SKIP LOCKED"
1721 );
1722 let stmt = match req.action.as_str() {
1723 "retry" => format!(
1728 "WITH picked AS ({pick}),
1729 act AS (
1730 INSERT INTO headgate_active_partition (queue, partition_key)
1731 SELECT DISTINCT j.queue, j.partition_key FROM headgate_job j
1732 WHERE j.id IN (SELECT id FROM picked)
1733 ON CONFLICT (queue, partition_key) DO UPDATE SET queue = EXCLUDED.queue
1734 )
1735 UPDATE headgate_job j SET state = 'available', scheduled_at_ms = {NOW_MS},
1736 finalized_at_ms = NULL
1737 WHERE j.id IN (SELECT id FROM picked)"
1738 ),
1739 "cancel" => format!(
1740 "WITH picked AS ({pick_state}),
1741 infl AS ({dec})
1742 UPDATE headgate_job j SET state = 'cancelled', lease_id = NULL,
1743 lease_expires_at_ms = NULL, claimed_by = NULL,
1744 finalized_at_ms = {NOW_MS}
1745 WHERE j.id IN (SELECT id FROM picked)",
1746 dec = crate::inflight_dec_sql(
1747 "(SELECT queue, partition_key FROM picked WHERE was_running)"
1748 )
1749 ),
1750 "delete" => format!(
1751 "WITH picked AS ({pick})
1752 DELETE FROM headgate_job j WHERE j.id IN (SELECT id FROM picked)"
1753 ),
1754 other => return Err(StoreError::Invalid(format!("unknown action `{other}`"))),
1755 };
1756 let mut all_params: Vec<&(dyn ToSql + Sync)> = vec![&batch];
1757 all_params.extend(params.iter().map(|p| &**p as &(dyn ToSql + Sync)));
1758 c.execute(&stmt, &all_params).await.map_err(map_pg_err)
1759 }
1760}
1761
1762impl PgStore {
1763 async fn job_state(&self, c: &crate::PgClient, id: &str) -> Result<Option<String>, StoreError> {
1764 Ok(c.query_opt(
1765 "SELECT state::text FROM headgate_job WHERE ulid = $1",
1766 &[&id],
1767 )
1768 .await
1769 .map_err(map_pg_err)?
1770 .map(|r| r.get(0)))
1771 }
1772}
1773
1774fn assemble_explain(row: &tokio_postgres::Row) -> AdmissionExplain {
1776 let state: String = row.get("state");
1777 let now_ms: i64 = row.get("now_ms");
1778 let mut detail: Vec<(String, String)> = vec![("state".into(), state.clone())];
1779
1780 match state.as_str() {
1781 "running" => {
1782 return AdmissionExplain {
1783 state,
1784 admissible: true,
1785 blocked_by: None,
1786 detail,
1787 estimated_admission_ms: Some(0),
1788 };
1789 }
1790 "scheduled" | "retryable" => {
1791 let at: i64 = row.get("scheduled_at_ms");
1792 detail.push(("scheduled_at_ms".into(), at.to_string()));
1793 return AdmissionExplain {
1794 state,
1795 admissible: false,
1796 blocked_by: Some(BlockedBy::Schedule),
1797 detail,
1798 estimated_admission_ms: Some((at - now_ms).max(0)),
1799 };
1800 }
1801 "quarantined" => {
1802 return AdmissionExplain {
1803 state,
1804 admissible: false,
1805 blocked_by: Some(BlockedBy::Quarantine),
1806 detail,
1807 estimated_admission_ms: None, };
1809 }
1810 "available" => {}
1811 _terminal => {
1812 return AdmissionExplain {
1813 state,
1814 admissible: false,
1815 blocked_by: None,
1816 detail,
1817 estimated_admission_ms: None,
1818 };
1819 }
1820 }
1821
1822 if row.get::<_, bool>("paused") {
1824 return AdmissionExplain {
1825 state,
1826 admissible: false,
1827 blocked_by: Some(BlockedBy::QueuePaused),
1828 detail,
1829 estimated_admission_ms: None,
1830 };
1831 }
1832 let scheduled_at: i64 = row.get("scheduled_at_ms");
1833 if scheduled_at > now_ms {
1834 detail.push(("scheduled_at_ms".into(), scheduled_at.to_string()));
1835 return AdmissionExplain {
1836 state,
1837 admissible: false,
1838 blocked_by: Some(BlockedBy::Schedule),
1839 detail,
1840 estimated_admission_ms: Some(scheduled_at - now_ms),
1841 };
1842 }
1843 if row.get::<_, bool>("quarantined") {
1844 detail.push(("fingerprint".into(), row.get::<_, String>("fingerprint")));
1845 return AdmissionExplain {
1846 state,
1847 admissible: false,
1848 blocked_by: Some(BlockedBy::Quarantine),
1849 detail,
1850 estimated_admission_ms: None,
1851 };
1852 }
1853 let rate_class: String = row.get("rate_class");
1854 if !rate_class.is_empty() {
1855 let avail: Option<i64> = row.get("avail");
1856 let ahead: i64 = row.get("cost_ahead_in_class");
1857 let weight: i64 = row.get("weight");
1858 let required = ahead + weight;
1859 detail.push(("rate_class".into(), rate_class));
1860 detail.push(("weight".into(), weight.to_string()));
1861 detail.push(("tokens_ahead_in_class".into(), ahead.to_string()));
1862 match avail {
1863 None => {
1867 detail.push((
1868 "tokens_available".into(),
1869 "unlimited (no such rate class)".into(),
1870 ));
1871 }
1872 Some(avail) => {
1873 detail.push(("tokens_available".into(), avail.to_string()));
1874 if avail < required {
1875 let limit: i64 = row.get("limit_per_window");
1876 let window: i64 = row.get("window_ms");
1877 let est = if limit > 0 {
1878 Some(((required - avail).max(1)) * window / limit)
1879 } else {
1880 None };
1882 return AdmissionExplain {
1883 state,
1884 admissible: false,
1885 blocked_by: Some(BlockedBy::RateClass),
1886 detail,
1887 estimated_admission_ms: est,
1888 };
1889 }
1890 }
1891 }
1892 }
1893 if let Some(max) = row.get::<_, Option<i64>>("max_concurrent") {
1894 let inflight: i64 = row.get("inflight");
1895 let strategy: String = row
1896 .get::<_, Option<String>>("on_saturated")
1897 .unwrap_or_else(|| "queue".into());
1898 detail.push(("max_concurrent".into(), max.to_string()));
1899 detail.push(("inflight".into(), inflight.to_string()));
1900 detail.push(("on_saturated".into(), strategy.clone()));
1901 if inflight >= max && strategy != SaturationStrategy::CancelRunning.as_str() {
1902 return AdmissionExplain {
1903 state,
1904 admissible: false,
1905 blocked_by: Some(BlockedBy::ConcurrencyLimit),
1906 detail,
1907 estimated_admission_ms: None, };
1909 }
1910 }
1911 detail.push((
1914 "position_in_partition".into(),
1915 row.get::<_, i64>("ahead_in_partition").to_string(),
1916 ));
1917 detail.push((
1918 "partition_deficit".into(),
1919 row.get::<_, i64>("deficit").to_string(),
1920 ));
1921 AdmissionExplain {
1922 state,
1923 admissible: true,
1924 blocked_by: None,
1925 detail,
1926 estimated_admission_ms: Some(0),
1927 }
1928}