dex_sdk/
pull_registration.rs1use std::collections::BTreeMap;
10use std::fmt::{Display, Formatter};
11use std::sync::Mutex;
12
13use tokio::sync::watch;
14use tonic::Status;
15
16use crate::{SdkError, SdkResult};
17
18#[derive(Clone, Debug)]
31pub struct PullRegistrationError {
32 pub queue: String,
34 pub status: Status,
36}
37
38impl Display for PullRegistrationError {
39 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
40 write!(
41 formatter,
42 "pull queue {} rejected: {}",
43 self.queue, self.status
44 )
45 }
46}
47
48impl std::error::Error for PullRegistrationError {
49 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
50 Some(&self.status)
51 }
52}
53
54#[derive(Clone, Debug)]
56pub struct PullQueueRegistrationState {
57 pub queue: String,
59 pub has_registered_stream: bool,
61 pub last_permanent_error: Option<Status>,
63}
64
65#[derive(Clone, Debug, Default)]
67pub struct PullRegistrationState {
68 pub is_ready: bool,
70 pub queues: Vec<PullQueueRegistrationState>,
72}
73
74#[derive(Default)]
75struct QueueRegistration {
76 has_registered: bool,
77 registered_streams: usize,
78 last_permanent_error: Option<Status>,
79}
80
81#[derive(Default)]
82struct RegistrationState {
83 queues: BTreeMap<String, QueueRegistration>,
84 has_registered_all_queues: bool,
85 initial_error: Option<SdkError>,
86 permanent_error: Option<PullRegistrationError>,
87 has_stopped: bool,
88}
89
90pub(crate) struct PullRegistration {
91 state: Mutex<RegistrationState>,
92 changed: watch::Sender<()>,
93}
94
95impl Default for PullRegistration {
96 fn default() -> Self {
97 Self::new(&[])
98 }
99}
100
101impl PullRegistration {
102 pub(crate) fn new(queues: &[String]) -> Self {
103 Self {
104 state: Mutex::new(RegistrationState {
105 queues: queues
106 .iter()
107 .map(|queue| (queue.clone(), QueueRegistration::default()))
108 .collect(),
109 ..RegistrationState::default()
110 }),
111 changed: watch::channel(()).0,
112 }
113 }
114
115 pub(crate) fn registered(&self, queue: &str) {
116 let mut state = self.state.lock().expect("pull registration lock");
117 let registration = state.queues.entry(queue.to_string()).or_default();
118 registration.registered_streams += 1;
119 registration.has_registered = true;
120 registration.last_permanent_error = None;
121 state.has_registered_all_queues |= state.queues.values().all(|queue| queue.has_registered);
122 self.changed.send_replace(());
123 }
124
125 pub(crate) fn ended(&self, queue: &str, was_registered: bool, failure: Option<&Status>) {
126 let mut state = self.state.lock().expect("pull registration lock");
127 let registration = state.queues.entry(queue.to_string()).or_default();
128 if was_registered {
129 registration.registered_streams -= 1;
130 }
131 if let Some(status) = failure {
132 registration.last_permanent_error = Some(status.clone());
133 if registration.registered_streams == 0 {
134 let rejection = PullRegistrationError {
135 queue: queue.to_string(),
136 status: status.clone(),
137 };
138 if !state.has_registered_all_queues && state.initial_error.is_none() {
139 state.initial_error = Some(SdkError::from_pull_registration(rejection.clone()));
140 }
141 state.permanent_error.get_or_insert(rejection);
142 }
143 }
144 self.changed.send_replace(());
145 }
146
147 pub(crate) fn finish(&self, failure: Option<SdkError>) {
148 let mut state = self.state.lock().expect("pull registration lock");
149 if state.initial_error.is_none() {
150 state.initial_error = failure;
151 }
152 state.has_stopped = true;
153 self.changed.send_replace(());
154 }
155
156 pub(crate) fn permanent_error(&self) -> Option<PullRegistrationError> {
157 self.state
158 .lock()
159 .expect("pull registration lock")
160 .permanent_error
161 .clone()
162 }
163
164 pub(crate) fn snapshot(&self) -> PullRegistrationState {
165 let state = self.state.lock().expect("pull registration lock");
166 let queues: Vec<_> = state
167 .queues
168 .iter()
169 .map(|(queue, registration)| PullQueueRegistrationState {
170 queue: queue.clone(),
171 has_registered_stream: registration.registered_streams > 0,
172 last_permanent_error: registration.last_permanent_error.clone(),
173 })
174 .collect();
175 PullRegistrationState {
176 is_ready: state.has_registered_all_queues
177 && !queues.iter().any(|queue| {
178 !queue.has_registered_stream && queue.last_permanent_error.is_some()
179 }),
180 queues,
181 }
182 }
183
184 pub(crate) async fn wait(&self) -> SdkResult<()> {
185 let mut changed = self.changed.subscribe();
186 loop {
187 {
188 let state = self.state.lock().expect("pull registration lock");
189 if state.has_registered_all_queues {
190 return Ok(());
191 }
192 if let Some(error) = &state.initial_error {
193 return Err(error.clone());
194 }
195 if state.has_stopped {
196 return Err(SdkError::InvalidArgument {
197 message: "pull Worker stopped before registration".to_string(),
198 });
199 }
200 }
201 changed
202 .changed()
203 .await
204 .expect("registration sender remains alive");
205 }
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[tokio::test]
214 async fn readiness_latches_and_recovers_from_permanent_rejections() {
215 let registration = PullRegistration::new(&["app".to_string(), "flow".to_string()]);
216 registration.registered("app");
217 assert!(!registration.snapshot().is_ready);
218 registration.registered("flow");
219 registration.wait().await.expect("all queues registered");
220 let snapshot = registration.snapshot();
221 registration.ended("flow", true, None);
222 assert!(registration.snapshot().is_ready);
223 registration.ended("flow", false, Some(&Status::permission_denied("rejected")));
224 assert!(!registration.snapshot().is_ready);
225 registration
226 .wait()
227 .await
228 .expect("completion remains latched");
229 registration.registered("flow");
230 assert!(registration.snapshot().is_ready);
231 assert!(
232 registration.snapshot().queues[1]
233 .last_permanent_error
234 .is_none()
235 );
236 registration.finish(None);
237 registration.ended("app", true, None);
238 registration.ended("flow", true, None);
239 assert!(registration.snapshot().is_ready);
240 assert!(
241 snapshot
242 .queues
243 .iter()
244 .all(|queue| queue.has_registered_stream)
245 );
246 }
247
248 #[tokio::test]
249 async fn another_registered_stream_prevents_a_fatal_rejection() {
250 let registration = PullRegistration::new(&["app".to_string(), "flow".to_string()]);
251 registration.registered("app");
252 registration.ended("app", false, Some(&Status::invalid_argument("rejected")));
253 assert!(registration.permanent_error().is_none());
254 registration.registered("flow");
255 registration.wait().await.expect("all queues registered");
256 assert!(registration.snapshot().is_ready);
257 registration.ended("app", true, None);
258 assert!(!registration.snapshot().is_ready);
259 registration.registered("app");
260 assert!(registration.snapshot().is_ready);
261 }
262
263 #[tokio::test]
264 async fn stopping_before_readiness_wakes_all_waiters() {
265 let registration = std::sync::Arc::new(PullRegistration::new(&["app".to_string()]));
266 let first = std::sync::Arc::clone(®istration);
267 let second = std::sync::Arc::clone(®istration);
268 let waiting_first = tokio::spawn(async move { first.wait().await });
269 let waiting_second = tokio::spawn(async move { second.wait().await });
270 registration.finish(None);
271 assert!(waiting_first.await.expect("first waiter").is_err());
272 assert!(waiting_second.await.expect("second waiter").is_err());
273 }
274}