1#![allow(clippy::extra_unused_lifetimes)]
2use std::collections::HashMap;
3use std::sync::mpsc::{SyncSender, sync_channel};
4use std::thread;
5use std::time::{Duration, SystemTime};
6
7use diesel::result::Error;
8use diesel::*;
9
10use serde::Serialize;
11
12use crate::backend::DbPool;
13use crate::models::Service;
14use crate::schema::worker_metadata;
15
16#[derive(Insertable, Debug)]
17#[diesel(table_name = worker_metadata)]
18pub struct NewWorkerMetadata {
20 pub service_id: i32,
22 pub last_dispatched_task_id: i64,
24 pub last_returned_task_id: Option<i64>,
26 pub total_dispatched: i32,
28 pub total_returned: i32,
30 pub first_seen: SystemTime,
32 pub session_seen: Option<SystemTime>,
34 pub time_last_dispatch: SystemTime,
36 pub time_last_return: Option<SystemTime>,
38 pub name: String,
40}
41
42#[derive(Identifiable, Queryable, Associations, Clone, Debug, Serialize)]
43#[diesel(table_name = worker_metadata)]
44#[diesel(belongs_to(Service, foreign_key = service_id))]
45pub struct WorkerMetadata {
47 pub id: i32,
49 pub service_id: i32,
51 pub last_dispatched_task_id: i64,
53 pub last_returned_task_id: Option<i64>,
55 pub total_dispatched: i32,
57 pub total_returned: i32,
59 pub first_seen: SystemTime,
61 pub session_seen: Option<SystemTime>,
63 pub time_last_dispatch: SystemTime,
65 pub time_last_return: Option<SystemTime>,
67 pub name: String,
69}
70
71impl From<WorkerMetadata> for HashMap<String, String> {
72 fn from(worker: WorkerMetadata) -> HashMap<String, String> {
73 let mut wh = HashMap::new();
74 wh.insert("id".to_string(), worker.id.to_string());
75 wh.insert("service_id".to_string(), worker.service_id.to_string());
76 wh.insert(
77 "last_dispatched_task_id".to_string(),
78 worker.last_dispatched_task_id.to_string(),
79 );
80 wh.insert(
81 "last_returned_task_id".to_string(),
82 match worker.last_returned_task_id {
83 Some(id) => id.to_string(),
84 _ => String::new(),
85 },
86 );
87 wh.insert(
88 "total_dispatched".to_string(),
89 worker.total_dispatched.to_string(),
90 );
91 wh.insert(
92 "total_returned".to_string(),
93 worker.total_returned.to_string(),
94 );
95 wh.insert(
100 "outstanding".to_string(),
101 worker
102 .total_dispatched
103 .saturating_sub(worker.total_returned)
104 .to_string(),
105 );
106
107 wh.insert("first_seen".to_string(), iso_utc_system(worker.first_seen));
111 wh.insert(
112 "session_seen".to_string(),
113 worker.session_seen.map(iso_utc_system).unwrap_or_default(),
114 );
115 wh.insert(
116 "time_last_dispatch".to_string(),
117 iso_utc_system(worker.time_last_dispatch),
118 );
119 wh.insert(
120 "time_last_return".to_string(),
121 worker
122 .time_last_return
123 .map(iso_utc_system)
124 .unwrap_or_default(),
125 );
126 wh.insert(
127 "fresh".to_string(),
128 if worker_is_fresh(&worker) {
129 "fresh"
130 } else {
131 "stale"
132 }
133 .to_string(),
134 );
135 wh.insert("name".to_string(), worker.name);
136 wh
137 }
138}
139
140pub fn iso_utc_system(then: SystemTime) -> String {
144 chrono::DateTime::<chrono::Utc>::from(then).to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
145}
146
147fn worker_is_fresh(worker: &WorkerMetadata) -> bool { worker.is_fresh() }
153
154impl WorkerMetadata {
155 pub fn is_fresh(&self) -> bool {
161 let last_active = match self.time_last_return {
162 Some(returned) => returned.max(self.time_last_dispatch),
163 None => self.time_last_dispatch,
164 };
165 SystemTime::now()
166 .duration_since(last_active)
167 .map(|elapsed| elapsed.as_secs() < 60)
168 .unwrap_or(true)
169 }
170
171 pub fn find_by_name(
173 identity: &str,
174 sid: i32,
175 connection: &mut PgConnection,
176 ) -> Result<WorkerMetadata, Error> {
177 use crate::schema::worker_metadata::{name, service_id};
178 worker_metadata::table
179 .filter(name.eq(identity))
180 .filter(service_id.eq(sid))
181 .get_result(connection)
182 }
183
184 pub fn fleet_summary(connection: &mut PgConnection) -> Result<(i64, i64), Error> {
193 use crate::schema::worker_metadata::dsl;
194 let active_since = SystemTime::now()
195 .checked_sub(Duration::from_secs(ACTIVE_WORKER_WINDOW_SECS))
196 .unwrap_or(SystemTime::UNIX_EPOCH);
197 let (count, in_flight): (i64, Option<i64>) = worker_metadata::table
198 .filter(
202 dsl::time_last_dispatch
203 .ge(active_since)
204 .or(dsl::time_last_return.ge(active_since)),
205 )
206 .select((
207 diesel::dsl::count_star(),
208 diesel::dsl::sum(dsl::total_dispatched - dsl::total_returned),
209 ))
210 .first(connection)?;
211 Ok((count, in_flight.unwrap_or(0)))
212 }
213
214 pub fn recent(connection: &mut PgConnection, limit: i64) -> Result<Vec<WorkerMetadata>, Error> {
218 use crate::schema::worker_metadata::dsl;
219 worker_metadata::table
220 .order(dsl::time_last_dispatch.desc())
221 .limit(limit)
222 .load(connection)
223 }
224}
225
226const ACTIVE_WORKER_WINDOW_SECS: u64 = 600;
232
233const METADATA_QUEUE_BOUND: usize = 8192;
238
239enum WorkerEvent {
242 Dispatched {
244 name: String,
246 service_id: i32,
248 task_id: i64,
250 },
251 Received {
253 name: String,
255 service_id: i32,
257 task_id: i64,
259 },
260 Heartbeat {
264 name: String,
266 service_id: i32,
268 },
269}
270
271#[derive(Clone)]
275pub struct WorkerMetadataSender {
276 tx: SyncSender<WorkerEvent>,
277}
278impl WorkerMetadataSender {
279 pub fn dispatched(&self, name: String, service_id: i32, task_id: i64) {
281 let _ = self.tx.try_send(WorkerEvent::Dispatched {
282 name,
283 service_id,
284 task_id,
285 });
286 }
287 pub fn received(&self, name: String, service_id: i32, task_id: i64) {
289 let _ = self.tx.try_send(WorkerEvent::Received {
290 name,
291 service_id,
292 task_id,
293 });
294 }
295 pub fn heartbeat(&self, name: String, service_id: i32) {
298 let _ = self
299 .tx
300 .try_send(WorkerEvent::Heartbeat { name, service_id });
301 }
302}
303
304pub fn start_metadata_writer(pool: DbPool) -> WorkerMetadataSender {
310 let (tx, rx) = sync_channel::<WorkerEvent>(METADATA_QUEUE_BOUND);
311 let _ = thread::spawn(move || {
312 for event in rx {
313 let mut pooled = match pool.get() {
315 Ok(connection) => connection,
316 Err(_) => continue,
317 };
318 let now = SystemTime::now();
319 let result = match event {
320 WorkerEvent::Dispatched {
321 name,
322 service_id,
323 task_id,
324 } => upsert_dispatched(&mut pooled, &name, service_id, task_id, now),
325 WorkerEvent::Received {
326 name,
327 service_id,
328 task_id,
329 } => upsert_received(&mut pooled, &name, service_id, task_id, now),
330 WorkerEvent::Heartbeat { name, service_id } => {
331 upsert_heartbeat(&mut pooled, &name, service_id, now)
332 },
333 };
334 if let Err(error) = result {
335 eprintln!("-- worker metadata writer: upsert failed: {error:?}");
336 }
337 }
338 });
339 WorkerMetadataSender { tx }
340}
341
342fn upsert_dispatched(
345 connection: &mut PgConnection,
346 name: &str,
347 service_id: i32,
348 last_dispatched_task_id: i64,
349 now: SystemTime,
350) -> QueryResult<usize> {
351 insert_into(worker_metadata::table)
352 .values(&NewWorkerMetadata {
353 name: name.to_string(),
354 service_id,
355 last_dispatched_task_id,
356 last_returned_task_id: None,
357 total_dispatched: 1,
358 total_returned: 0,
359 first_seen: now,
360 session_seen: Some(now),
361 time_last_dispatch: now,
362 time_last_return: None,
363 })
364 .on_conflict((worker_metadata::name, worker_metadata::service_id))
365 .do_update()
366 .set((
367 worker_metadata::last_dispatched_task_id.eq(last_dispatched_task_id),
368 worker_metadata::total_dispatched.eq(worker_metadata::total_dispatched + 1),
369 worker_metadata::time_last_dispatch.eq(now),
370 ))
371 .execute(connection)
372}
373
374fn upsert_heartbeat(
380 connection: &mut PgConnection,
381 name: &str,
382 service_id: i32,
383 now: SystemTime,
384) -> QueryResult<usize> {
385 insert_into(worker_metadata::table)
386 .values(&NewWorkerMetadata {
387 name: name.to_string(),
388 service_id,
389 last_dispatched_task_id: 0,
390 last_returned_task_id: None,
391 total_dispatched: 0,
392 total_returned: 0,
393 first_seen: now,
394 session_seen: Some(now),
395 time_last_dispatch: now,
396 time_last_return: None,
397 })
398 .on_conflict((worker_metadata::name, worker_metadata::service_id))
399 .do_update()
400 .set((worker_metadata::time_last_dispatch.eq(now),))
401 .execute(connection)
402}
403
404fn upsert_received(
409 connection: &mut PgConnection,
410 name: &str,
411 service_id: i32,
412 last_returned_task_id: i64,
413 now: SystemTime,
414) -> QueryResult<usize> {
415 insert_into(worker_metadata::table)
416 .values(&NewWorkerMetadata {
417 name: name.to_string(),
418 service_id,
419 last_dispatched_task_id: 0,
420 last_returned_task_id: Some(last_returned_task_id),
421 total_dispatched: 0,
422 total_returned: 1,
423 first_seen: now,
424 session_seen: Some(now),
425 time_last_dispatch: now,
426 time_last_return: Some(now),
427 })
428 .on_conflict((worker_metadata::name, worker_metadata::service_id))
429 .do_update()
430 .set((
431 worker_metadata::last_returned_task_id.eq(last_returned_task_id),
432 worker_metadata::total_returned.eq(worker_metadata::total_returned + 1),
433 worker_metadata::time_last_return.eq(now),
434 ))
435 .execute(connection)
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::backend;
442 use crate::schema::worker_metadata as wm;
443
444 const SERVICE_ID: i32 = 1; fn clear(connection: &mut PgConnection, worker: &str) {
447 diesel::delete(wm::table.filter(wm::name.eq(worker)))
448 .execute(connection)
449 .ok();
450 }
451
452 fn load(connection: &mut PgConnection, worker: &str) -> WorkerMetadata {
453 WorkerMetadata::find_by_name(worker, SERVICE_ID, connection).expect("worker row")
454 }
455
456 #[test]
457 fn received_before_dispatched_does_not_drop_the_count() {
458 let worker = "wm-test:out-of-order:1";
459 let mut backend = backend::testdb();
460 let connection = &mut backend.connection;
461 clear(connection, worker);
462 let now = SystemTime::now();
463
464 upsert_received(connection, worker, SERVICE_ID, 42, now).expect("received upsert");
467 let row = load(connection, worker);
468 assert_eq!(row.total_returned, 1, "the early return is not dropped");
469 assert_eq!(row.total_dispatched, 0, "no dispatch counted yet");
470 assert_eq!(row.last_returned_task_id, Some(42));
471
472 upsert_dispatched(connection, worker, SERVICE_ID, 99, now).expect("dispatched upsert");
474 let row = load(connection, worker);
475 assert_eq!(row.total_dispatched, 1, "dispatch now counted");
476 assert_eq!(
477 row.total_returned, 1,
478 "return preserved across the dispatch upsert"
479 );
480 assert_eq!(row.last_dispatched_task_id, 99);
481
482 clear(connection, worker);
483 }
484
485 #[test]
486 fn heartbeat_refreshes_liveness_without_counting_a_dispatch() {
487 let worker = "wm-test:heartbeat:1";
488 let mut backend = backend::testdb();
489 let connection = &mut backend.connection;
490 clear(connection, worker);
491 let early = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
492 let later = SystemTime::UNIX_EPOCH + Duration::from_secs(2_000);
493
494 upsert_heartbeat(connection, worker, SERVICE_ID, early).expect("heartbeat seed");
496 let row = load(connection, worker);
497 assert_eq!(row.total_dispatched, 0, "a ping is not a dispatch");
498 assert_eq!(row.total_returned, 0);
499
500 upsert_dispatched(connection, worker, SERVICE_ID, 7, early).expect("dispatched");
502 upsert_heartbeat(connection, worker, SERVICE_ID, later).expect("heartbeat refresh");
504 upsert_heartbeat(connection, worker, SERVICE_ID, later).expect("heartbeat refresh");
505 let row = load(connection, worker);
506 assert_eq!(
507 row.total_dispatched, 1,
508 "idle pings did not inflate dispatched"
509 );
510 assert_eq!(
511 row.total_dispatched - row.total_returned,
512 1,
513 "outstanding reflects the one real in-flight task, not the pings"
514 );
515 assert_eq!(
516 row.time_last_dispatch, later,
517 "the ping refreshed last-dispatch-requested"
518 );
519 assert_eq!(
520 row.last_dispatched_task_id, 7,
521 "the real lease id is preserved"
522 );
523
524 clear(connection, worker);
525 }
526
527 #[test]
528 fn repeated_events_accumulate_in_one_row() {
529 let worker = "wm-test:accumulate:1";
530 let mut backend = backend::testdb();
531 let connection = &mut backend.connection;
532 clear(connection, worker);
533 let now = SystemTime::now();
534
535 for task in 0..3 {
536 upsert_dispatched(connection, worker, SERVICE_ID, task, now).expect("dispatched");
537 }
538 for task in 0..2 {
539 upsert_received(connection, worker, SERVICE_ID, task, now).expect("received");
540 }
541 let rows: i64 = wm::table
543 .filter(wm::name.eq(worker))
544 .count()
545 .get_result(connection)
546 .unwrap();
547 assert_eq!(rows, 1, "one row per (name, service_id)");
548 let row = load(connection, worker);
549 assert_eq!(row.total_dispatched, 3);
550 assert_eq!(row.total_returned, 2);
551
552 clear(connection, worker);
553 }
554}