Skip to main content

queuey_core/
envelope.rs

1//! The [`Envelope`]: the wire format every job travels in.
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use crate::{error::Result, job::Job, queue::QueueSet};
7
8/// Wire format for a job message. Serialized as JSON in the message body.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct Envelope {
11    /// Unique id, stable across retries.
12    pub job_id: Uuid,
13    /// `Job::NAME` of the payload.
14    pub job_type: String,
15    /// Broker queue name this envelope was published to.
16    pub queue: String,
17    /// 1-based attempt counter. First delivery is attempt 1.
18    pub attempt: u32,
19    /// Unix epoch milliseconds when the job was first enqueued.
20    pub enqueued_at_ms: u64,
21    /// How often this job was deferred (see [`crate::JobError::Deferred`]).
22    ///
23    /// Independent of [`Envelope::attempt`]: a deferral is not a failed attempt.
24    /// Defaults to `0` so envelopes written before this field existed still decode.
25    #[serde(default)]
26    pub deferrals: u32,
27    /// Broker message priority; `0` (the default) is normal work.
28    ///
29    /// Deferred envelopes carry the highest level their queue supports
30    /// ([`crate::QueueConfig::max_priority`]) so they run ahead of the backlog. A queue
31    /// that is not a priority queue makes the broker ignore this. Only the scheduling
32    /// action that just happened decides this: a deferral raises it,
33    /// [`Envelope::next_attempt`] puts it back to `0`. Defaults to `0` so envelopes
34    /// written before this field existed still decode.
35    #[serde(default)]
36    pub priority: u8,
37    /// The serialized job.
38    pub payload: serde_json::Value,
39}
40
41impl Envelope {
42    /// Build a first-attempt envelope for `job`.
43    pub fn new<J: Job>(job: &J) -> Result<Self> {
44        Ok(Self {
45            job_id: Uuid::new_v4(),
46            job_type: J::NAME.to_owned(),
47            queue: J::QUEUE.name().to_owned(),
48            attempt: 1,
49            enqueued_at_ms: now_ms(),
50            deferrals: 0,
51            priority: 0,
52            payload: serde_json::to_value(job)?,
53        })
54    }
55
56    /// Deserialize the payload as `J`. Does not check `job_type`.
57    pub fn decode<J: Job>(&self) -> Result<J> {
58        Ok(serde_json::from_value(self.payload.clone())?)
59    }
60
61    /// Copy of this envelope with `attempt` incremented and `priority` back at `0`.
62    ///
63    /// `deferrals` rides along untouched, so a retry still knows how often the job was
64    /// deferred. `priority` does not: a retry is scheduled like any other failed
65    /// attempt and must not jump the backlog just because the job happened to defer
66    /// itself earlier. Only the scheduling action that just happened decides the
67    /// priority. See [`Envelope::deferred`] for the one that raises it.
68    pub fn next_attempt(&self) -> Self {
69        Self {
70            attempt: self.attempt + 1,
71            priority: 0,
72            ..self.clone()
73        }
74    }
75
76    /// Copy of this envelope for a deferral: `deferrals + 1` and `priority` set.
77    ///
78    /// `attempt`, `job_id`, `job_type`, `queue` and `enqueued_at_ms` are unchanged:
79    /// a deferral is not a failed attempt, and `age` keeps measuring the time since
80    /// the job was first enqueued. `priority` is normally
81    /// `QueueConfig::max_priority.unwrap_or(0)` of the job's queue, so the job comes
82    /// back ahead of everything published normally. It lasts only until the job is
83    /// next scheduled some other way: [`Envelope::next_attempt`] drops it back to `0`.
84    /// See [`crate::JobError::Deferred`].
85    pub fn deferred(&self, priority: u8) -> Self {
86        Self {
87            deferrals: self.deferrals + 1,
88            priority,
89            ..self.clone()
90        }
91    }
92
93    /// Serialize the envelope to the JSON message body.
94    pub fn to_bytes(&self) -> Result<Vec<u8>> {
95        Ok(serde_json::to_vec(self)?)
96    }
97
98    /// Parse an envelope from a JSON message body.
99    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
100        Ok(serde_json::from_slice(bytes)?)
101    }
102}
103
104pub(crate) fn now_ms() -> u64 {
105    use std::time::{SystemTime, UNIX_EPOCH};
106    SystemTime::now()
107        .duration_since(UNIX_EPOCH)
108        .map(|d| d.as_millis() as u64)
109        .unwrap_or(0)
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::test_support::{Greet, Ping};
116
117    #[test]
118    fn new_fills_in_the_job_metadata() {
119        let env = Envelope::new(&Greet::new("ada")).unwrap();
120        assert_eq!(env.job_type, Greet::NAME);
121        assert_eq!(env.queue, "test.alpha");
122        assert_eq!(env.attempt, 1);
123        assert_ne!(env.job_id, Uuid::nil());
124        assert!(env.enqueued_at_ms > 0);
125        assert_eq!(env.deferrals, 0);
126        assert_eq!(env.priority, 0);
127        assert_eq!(env.payload, serde_json::json!({ "name": "ada" }));
128    }
129
130    #[test]
131    fn distinct_envelopes_get_distinct_ids() {
132        let a = Envelope::new(&Greet::new("a")).unwrap();
133        let b = Envelope::new(&Greet::new("a")).unwrap();
134        assert_ne!(a.job_id, b.job_id);
135    }
136
137    #[test]
138    fn bytes_round_trip() {
139        let env = Envelope::new(&Greet::new("grace")).unwrap();
140        let bytes = env.to_bytes().unwrap();
141        let back = Envelope::from_bytes(&bytes).unwrap();
142        assert_eq!(env, back);
143    }
144
145    #[test]
146    fn from_bytes_rejects_garbage() {
147        assert!(Envelope::from_bytes(b"not json").is_err());
148        assert!(Envelope::from_bytes(br#"{"job_id":"nope"}"#).is_err());
149    }
150
151    #[test]
152    fn decode_returns_the_original_job() {
153        let job = Greet::new("linus");
154        let env = Envelope::new(&job).unwrap();
155        assert_eq!(env.decode::<Greet>().unwrap(), job);
156    }
157
158    #[test]
159    fn decode_with_wrong_shape_errors() {
160        let env = Envelope::new(&Greet::new("linus")).unwrap();
161        // `Ping { seq: u32 }` cannot be built from `{ "name": "linus" }`.
162        let err = env.decode::<Ping>().unwrap_err();
163        assert!(
164            matches!(err, crate::error::Error::Serde(_)),
165            "unexpected error: {err}"
166        );
167    }
168
169    #[test]
170    fn decode_after_round_trip_still_works() {
171        let env = Envelope::new(&Ping { seq: 7 }).unwrap();
172        let back = Envelope::from_bytes(&env.to_bytes().unwrap()).unwrap();
173        assert_eq!(back.decode::<Ping>().unwrap(), Ping { seq: 7 });
174    }
175
176    #[test]
177    fn next_attempt_increments_and_preserves_identity() {
178        let env = Envelope::new(&Greet::new("ada")).unwrap();
179        let second = env.next_attempt();
180        assert_eq!(second.attempt, 2);
181        assert_eq!(second.job_id, env.job_id);
182        assert_eq!(second.job_type, env.job_type);
183        assert_eq!(second.queue, env.queue);
184        assert_eq!(second.enqueued_at_ms, env.enqueued_at_ms);
185        assert_eq!(second.payload, env.payload);
186        assert_eq!(second.priority, 0);
187        // The original is untouched.
188        assert_eq!(env.attempt, 1);
189        assert_eq!(second.next_attempt().attempt, 3);
190    }
191
192    #[test]
193    fn next_attempt_keeps_the_deferral_count_but_drops_the_priority() {
194        let env = Envelope::new(&Greet::new("ada")).unwrap().deferred(10);
195        let retried = env.next_attempt();
196        assert_eq!(retried.attempt, 2);
197        assert_eq!(retried.deferrals, 1, "a retry is not a second deferral");
198        assert_eq!(
199            retried.priority, 0,
200            "a retry is not a deferral: it goes behind the backlog"
201        );
202        // The deferred envelope it came from is untouched.
203        assert_eq!(env.priority, 10);
204        assert_eq!(env.attempt, 1);
205        // And a further retry stays at zero.
206        assert_eq!(retried.next_attempt().priority, 0);
207    }
208
209    #[test]
210    fn deferred_increments_deferrals_and_sets_the_priority() {
211        let env = Envelope::new(&Greet::new("ada")).unwrap();
212        let held = env.deferred(10);
213
214        assert_eq!(held.deferrals, 1);
215        assert_eq!(held.priority, 10);
216        // A deferral is not an attempt, and the identity is untouched.
217        assert_eq!(held.attempt, env.attempt);
218        assert_eq!(held.job_id, env.job_id);
219        assert_eq!(held.job_type, env.job_type);
220        assert_eq!(held.queue, env.queue);
221        assert_eq!(held.enqueued_at_ms, env.enqueued_at_ms);
222        assert_eq!(held.payload, env.payload);
223        // The original is untouched.
224        assert_eq!(env.deferrals, 0);
225        assert_eq!(env.priority, 0);
226    }
227
228    #[test]
229    fn deferrals_accumulate_and_a_zero_priority_queue_stays_at_zero() {
230        let env = Envelope::new(&Greet::new("ada")).unwrap();
231        let twice = env.deferred(10).deferred(0);
232        assert_eq!(twice.deferrals, 2);
233        assert_eq!(twice.priority, 0);
234        assert_eq!(twice.attempt, 1);
235    }
236
237    #[test]
238    fn json_without_the_deferral_fields_still_decodes() {
239        // Exactly the wire format from before deferral existed.
240        let old = serde_json::json!({
241            "job_id": "8b1a9953-4c2f-4a5b-9c2e-0d1f2a3b4c5d",
242            "job_type": "test::Greet",
243            "queue": "test.alpha",
244            "attempt": 2,
245            "enqueued_at_ms": 1_700_000_000_000u64,
246            "payload": { "name": "ada" },
247        });
248        let env = Envelope::from_bytes(&serde_json::to_vec(&old).unwrap()).unwrap();
249
250        assert_eq!(env.deferrals, 0);
251        assert_eq!(env.priority, 0);
252        assert_eq!(env.attempt, 2);
253        assert_eq!(env.decode::<Greet>().unwrap(), Greet::new("ada"));
254        // And it round-trips through the new format unchanged.
255        assert_eq!(Envelope::from_bytes(&env.to_bytes().unwrap()).unwrap(), env);
256    }
257
258    #[test]
259    fn now_ms_is_monotonic_enough() {
260        assert!(now_ms() >= 1_700_000_000_000);
261    }
262}