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