1use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use crate::{error::Result, job::Job, queue::QueueSet};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct Envelope {
11 pub job_id: Uuid,
13 pub job_type: String,
15 pub queue: String,
17 pub attempt: u32,
19 pub enqueued_at_ms: u64,
21 #[serde(default)]
26 pub deferrals: u32,
27 #[serde(default)]
36 pub priority: u8,
37 pub payload: serde_json::Value,
39}
40
41impl Envelope {
42 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 pub fn decode<J: Job>(&self) -> Result<J> {
58 Ok(serde_json::from_value(self.payload.clone())?)
59 }
60
61 pub fn next_attempt(&self) -> Self {
69 Self {
70 attempt: self.attempt + 1,
71 priority: 0,
72 ..self.clone()
73 }
74 }
75
76 pub fn deferred(&self, priority: u8) -> Self {
86 Self {
87 deferrals: self.deferrals + 1,
88 priority,
89 ..self.clone()
90 }
91 }
92
93 pub fn to_bytes(&self) -> Result<Vec<u8>> {
95 Ok(serde_json::to_vec(self)?)
96 }
97
98 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 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 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 assert_eq!(env.priority, 10);
204 assert_eq!(env.attempt, 1);
205 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 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 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 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 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}