1use async_trait::async_trait;
11use chrono::Duration;
12use mas_data_model::{Clock, UlidExt as _};
13use mas_storage::queue::{QueueWorkerRepository, Worker};
14use rand::Rng;
15use sqlx::PgConnection;
16use ulid::Ulid;
17use uuid::Uuid;
18
19use crate::{DatabaseError, ExecuteExt};
20
21pub struct PgQueueWorkerRepository<'c> {
23 conn: &'c mut PgConnection,
24}
25
26impl<'c> PgQueueWorkerRepository<'c> {
27 #[must_use]
30 pub fn new(conn: &'c mut PgConnection) -> Self {
31 Self { conn }
32 }
33}
34
35#[async_trait]
36impl QueueWorkerRepository for PgQueueWorkerRepository<'_> {
37 type Error = DatabaseError;
38
39 #[tracing::instrument(
40 name = "db.queue_worker.register",
41 skip_all,
42 fields(
43 worker.id,
44 db.query.text,
45 ),
46 err,
47 )]
48 async fn register(
49 &mut self,
50 rng: &mut (dyn Rng + Send),
51 clock: &dyn Clock,
52 ) -> Result<Worker, Self::Error> {
53 let now = clock.now();
54 let worker_id = Ulid::from_datetime_with_rng(now, rng);
55 tracing::Span::current().record("worker.id", tracing::field::display(worker_id));
56
57 sqlx::query!(
58 r#"
59 INSERT INTO queue_workers (queue_worker_id, registered_at, last_seen_at)
60 VALUES ($1, $2, $2)
61 "#,
62 Uuid::from(worker_id),
63 now,
64 )
65 .traced()
66 .execute(&mut *self.conn)
67 .await?;
68
69 Ok(Worker { id: worker_id })
70 }
71
72 #[tracing::instrument(
73 name = "db.queue_worker.heartbeat",
74 skip_all,
75 fields(
76 %worker.id,
77 db.query.text,
78 ),
79 err,
80 )]
81 async fn heartbeat(&mut self, clock: &dyn Clock, worker: &Worker) -> Result<(), Self::Error> {
82 let now = clock.now();
83 let res = sqlx::query!(
84 r#"
85 UPDATE queue_workers
86 SET last_seen_at = $2
87 WHERE queue_worker_id = $1 AND shutdown_at IS NULL
88 "#,
89 Uuid::from(worker.id),
90 now,
91 )
92 .traced()
93 .execute(&mut *self.conn)
94 .await?;
95
96 DatabaseError::ensure_affected_rows(&res, 1)?;
98
99 Ok(())
100 }
101
102 #[tracing::instrument(
103 name = "db.queue_worker.shutdown",
104 skip_all,
105 fields(
106 %worker.id,
107 db.query.text,
108 ),
109 err,
110 )]
111 async fn shutdown(&mut self, clock: &dyn Clock, worker: &Worker) -> Result<(), Self::Error> {
112 let now = clock.now();
113 let res = sqlx::query!(
114 r#"
115 UPDATE queue_workers
116 SET shutdown_at = $2
117 WHERE queue_worker_id = $1
118 "#,
119 Uuid::from(worker.id),
120 now,
121 )
122 .traced()
123 .execute(&mut *self.conn)
124 .await?;
125
126 DatabaseError::ensure_affected_rows(&res, 1)?;
127
128 let res = sqlx::query!(
130 r#"
131 DELETE FROM queue_leader
132 WHERE queue_worker_id = $1
133 "#,
134 Uuid::from(worker.id),
135 )
136 .traced()
137 .execute(&mut *self.conn)
138 .await?;
139
140 if res.rows_affected() > 0 {
142 sqlx::query!(
143 r#"
144 NOTIFY queue_leader_stepdown
145 "#,
146 )
147 .traced()
148 .execute(&mut *self.conn)
149 .await?;
150 }
151
152 Ok(())
153 }
154
155 #[tracing::instrument(
156 name = "db.queue_worker.shutdown_dead_workers",
157 skip_all,
158 fields(
159 db.query.text,
160 ),
161 err,
162 )]
163 async fn shutdown_dead_workers(
164 &mut self,
165 clock: &dyn Clock,
166 threshold: Duration,
167 ) -> Result<(), Self::Error> {
168 let now = clock.now();
172 sqlx::query!(
173 r#"
174 UPDATE queue_workers
175 SET shutdown_at = $1
176 WHERE shutdown_at IS NULL
177 AND last_seen_at < $2
178 "#,
179 now,
180 now - threshold,
181 )
182 .traced()
183 .execute(&mut *self.conn)
184 .await?;
185
186 Ok(())
187 }
188
189 #[tracing::instrument(
190 name = "db.queue_worker.remove_leader_lease_if_expired",
191 skip_all,
192 fields(
193 db.query.text,
194 ),
195 err,
196 )]
197 async fn remove_leader_lease_if_expired(
198 &mut self,
199 _clock: &dyn Clock,
200 ) -> Result<(), Self::Error> {
201 sqlx::query!(
204 r#"
205 DELETE FROM queue_leader
206 WHERE expires_at < NOW()
207 "#,
208 )
209 .traced()
210 .execute(&mut *self.conn)
211 .await?;
212
213 Ok(())
214 }
215
216 #[tracing::instrument(
217 name = "db.queue_worker.try_get_leader_lease",
218 skip_all,
219 fields(
220 %worker.id,
221 db.query.text,
222 ),
223 err,
224 )]
225 async fn try_get_leader_lease(
226 &mut self,
227 clock: &dyn Clock,
228 worker: &Worker,
229 ) -> Result<bool, Self::Error> {
230 let now = clock.now();
231 let res = sqlx::query!(
240 r#"
241 INSERT INTO queue_leader (elected_at, expires_at, queue_worker_id)
242 VALUES ($1, NOW() + INTERVAL '5 seconds', $2)
243 ON CONFLICT (active)
244 DO UPDATE SET expires_at = EXCLUDED.expires_at
245 WHERE queue_leader.queue_worker_id = $2
246 "#,
247 now,
248 Uuid::from(worker.id)
249 )
250 .traced()
251 .execute(&mut *self.conn)
252 .await?;
253
254 let am_i_the_leader = res.rows_affected() == 1;
257
258 Ok(am_i_the_leader)
259 }
260}