worklane_core/observer.rs
1//! The observer SPI: a telemetry extension point for job resolution.
2//!
3//! A [`JobObserver`] is the symmetric counterpart to a [`Broker`](crate::Broker):
4//! where a broker is the storage extension point, an observer is the telemetry
5//! extension point. It lives in `worklane-core` so a telemetry integration (for
6//! example `worklane-metrics`) can depend on the contract alone, without pulling
7//! in the `worklane` facade and its runtime.
8//!
9//! The `worklane` facade re-exports these types, and its `Worker` calls the
10//! observer inline as it resolves jobs.
11
12use std::time::Duration;
13
14/// What ultimately happened to a job, reported to a [`JobObserver`].
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum JobOutcome {
18 /// The handler succeeded and the job was acked.
19 Acked,
20 /// The handler failed and the job was scheduled for a retry.
21 Retried,
22 /// The job was moved to the dead-letter store (attempts exhausted, an
23 /// unrecoverable payload, an unknown kind, or a timeout with no retries left).
24 DeadLettered,
25}
26
27/// A finished-job event handed to a [`JobObserver`].
28///
29/// Reported only when the resolution actually took effect — a resolution rejected
30/// as stale (the lease was lost and the job will be redelivered) is *not* reported,
31/// so a redelivered job is counted once, when it finally resolves.
32#[derive(Debug, Clone, Copy)]
33#[non_exhaustive]
34pub struct JobEvent<'a> {
35 /// The lane the job ran on.
36 pub lane: &'a str,
37 /// The job kind.
38 pub kind: &'a str,
39 /// The terminal outcome.
40 pub outcome: JobOutcome,
41 /// Time from dispatch start to resolution (handler plus result-store and
42 /// broker calls). Near zero for an unknown-kind job (no handler runs).
43 pub duration: Duration,
44}
45
46impl<'a> JobEvent<'a> {
47 /// Construct a job event. The worker builds these; this constructor lets
48 /// observer implementors build one in their own tests despite the type being
49 /// `#[non_exhaustive]`.
50 pub fn new(lane: &'a str, kind: &'a str, outcome: JobOutcome, duration: Duration) -> Self {
51 JobEvent {
52 lane,
53 kind,
54 outcome,
55 duration,
56 }
57 }
58}
59
60/// A per-attempt in-flight event handed to a [`JobObserver`].
61#[derive(Debug, Clone, Copy)]
62#[non_exhaustive]
63pub struct JobAttemptEvent<'a> {
64 /// The lane the job attempt is running on.
65 pub lane: &'a str,
66 /// The job kind.
67 pub kind: &'a str,
68}
69
70impl<'a> JobAttemptEvent<'a> {
71 /// Construct an attempt event. The worker builds these; this constructor lets
72 /// observer implementors build one in their own tests despite the type being
73 /// `#[non_exhaustive]`.
74 pub fn new(lane: &'a str, kind: &'a str) -> Self {
75 JobAttemptEvent { lane, kind }
76 }
77}
78
79/// Observes the outcome of every job a `Worker` resolves.
80///
81/// A hook for telemetry — most usefully metrics (job counts by outcome, a
82/// processing-duration histogram). The `worklane-metrics` crate provides an
83/// implementation over the `metrics` facade. The callback runs inline on the
84/// worker, so it must be cheap and non-blocking (record and return).
85pub trait JobObserver: Send + Sync {
86 /// Called when a reserved job attempt enters the worker's in-flight set.
87 fn on_job_started(&self, _event: JobAttemptEvent<'_>) {}
88
89 /// Called when an in-flight job attempt leaves the worker, including stale
90 /// resolution, defer, timeout, or future cancellation. This pairs with
91 /// [`on_job_started`](JobObserver::on_job_started) for in-flight gauges.
92 fn on_job_stopped(&self, _event: JobAttemptEvent<'_>) {}
93
94 /// Called once per job, after its resolution takes effect.
95 fn on_job_finished(&self, event: JobEvent<'_>);
96}