mas_storage_pg/queue/
worker.rs1use 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 )]
47 async fn register(
48 &mut self,
49 rng: &mut (dyn Rng + Send),
50 clock: &dyn Clock,
51 ) -> Result<Worker, Self::Error> {
52 let now = clock.now();
53 let worker_id = Ulid::from_datetime_with_rng(now, rng);
54 tracing::Span::current().record("worker.id", tracing::field::display(worker_id));
55
56 sqlx::query!(
57 r#"
58 INSERT INTO queue_workers (queue_worker_id, registered_at, last_seen_at)
59 VALUES ($1, $2, $2)
60 "#,
61 Uuid::from(worker_id),
62 now,
63 )
64 .traced()
65 .execute(&mut *self.conn)
66 .await?;
67
68 Ok(Worker { id: worker_id })
69 }
70
71 #[tracing::instrument(
72 name = "db.queue_worker.heartbeat",
73 skip_all,
74 fields(
75 %worker.id,
76 db.query.text,
77 ),
78 )]
79 async fn heartbeat(&mut self, clock: &dyn Clock, worker: &Worker) -> Result<(), Self::Error> {
80 let now = clock.now();
81 let res = sqlx::query!(
82 r#"
83 UPDATE queue_workers
84 SET last_seen_at = $2
85 WHERE queue_worker_id = $1 AND shutdown_at IS NULL
86 "#,
87 Uuid::from(worker.id),
88 now,
89 )
90 .traced()
91 .execute(&mut *self.conn)
92 .await?;
93
94 DatabaseError::ensure_affected_rows(&res, 1)?;
96
97 Ok(())
98 }
99
100 #[tracing::instrument(
101 name = "db.queue_worker.shutdown",
102 skip_all,
103 fields(
104 %worker.id,
105 db.query.text,
106 ),
107 )]
108 async fn shutdown(&mut self, clock: &dyn Clock, worker: &Worker) -> Result<(), Self::Error> {
109 let now = clock.now();
110 let res = sqlx::query!(
111 r#"
112 UPDATE queue_workers
113 SET shutdown_at = $2
114 WHERE queue_worker_id = $1
115 "#,
116 Uuid::from(worker.id),
117 now,
118 )
119 .traced()
120 .execute(&mut *self.conn)
121 .await?;
122
123 DatabaseError::ensure_affected_rows(&res, 1)?;
124
125 let res = sqlx::query!(
127 r#"
128 DELETE FROM queue_leader
129 WHERE queue_worker_id = $1
130 "#,
131 Uuid::from(worker.id),
132 )
133 .traced()
134 .execute(&mut *self.conn)
135 .await?;
136
137 if res.rows_affected() > 0 {
139 sqlx::query!(
140 r#"
141 NOTIFY queue_leader_stepdown
142 "#,
143 )
144 .traced()
145 .execute(&mut *self.conn)
146 .await?;
147 }
148
149 Ok(())
150 }
151
152 #[tracing::instrument(
153 name = "db.queue_worker.shutdown_dead_workers",
154 skip_all,
155 fields(
156 db.query.text,
157 ),
158 )]
159 async fn shutdown_dead_workers(
160 &mut self,
161 clock: &dyn Clock,
162 threshold: Duration,
163 ) -> Result<(), Self::Error> {
164 let now = clock.now();
168 sqlx::query!(
169 r#"
170 UPDATE queue_workers
171 SET shutdown_at = $1
172 WHERE shutdown_at IS NULL
173 AND last_seen_at < $2
174 "#,
175 now,
176 now - threshold,
177 )
178 .traced()
179 .execute(&mut *self.conn)
180 .await?;
181
182 Ok(())
183 }
184
185 #[tracing::instrument(
186 name = "db.queue_worker.remove_leader_lease_if_expired",
187 skip_all,
188 fields(
189 db.query.text,
190 ),
191 )]
192 async fn remove_leader_lease_if_expired(
193 &mut self,
194 _clock: &dyn Clock,
195 ) -> Result<(), Self::Error> {
196 sqlx::query!(
199 r#"
200 DELETE FROM queue_leader
201 WHERE expires_at < NOW()
202 "#,
203 )
204 .traced()
205 .execute(&mut *self.conn)
206 .await?;
207
208 Ok(())
209 }
210
211 #[tracing::instrument(
212 name = "db.queue_worker.try_get_leader_lease",
213 skip_all,
214 fields(
215 %worker.id,
216 db.query.text,
217 ),
218 )]
219 async fn try_get_leader_lease(
220 &mut self,
221 clock: &dyn Clock,
222 worker: &Worker,
223 ) -> Result<bool, Self::Error> {
224 let now = clock.now();
225 let res = sqlx::query!(
234 r#"
235 INSERT INTO queue_leader (elected_at, expires_at, queue_worker_id)
236 VALUES ($1, NOW() + INTERVAL '5 seconds', $2)
237 ON CONFLICT (active)
238 DO UPDATE SET expires_at = EXCLUDED.expires_at
239 WHERE queue_leader.queue_worker_id = $2
240 "#,
241 now,
242 Uuid::from(worker.id)
243 )
244 .traced()
245 .execute(&mut *self.conn)
246 .await?;
247
248 let am_i_the_leader = res.rows_affected() == 1;
251
252 Ok(am_i_the_leader)
253 }
254}