Skip to main content

dex_sdk/
pull_registration.rs

1// Copyright (c) 2026 Super Durable, Inc.
2//
3// Licensed under the Sustainable Use License 1.0.
4// You may not use this file except in compliance with the License.
5// See the LICENSE file in the repository root.
6//
7// SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
9use 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/// Permanent rejection that leaves a pull queue without any registered stream.
19///
20/// Use [`SdkError::pull_registration_error`] on errors from [`crate::Worker::wait_pull_registered`] or
21/// [`crate::Worker::start`] to inspect the queue and original gRPC status.
22///
23/// ```
24/// fn report(error: dex_sdk::SdkError) {
25///     if let Some(rejection) = error.pull_registration_error() {
26///         eprintln!("queue {}: {}", rejection.queue, rejection.status.code());
27///     }
28/// }
29/// ```
30#[derive(Clone, Debug)]
31pub struct PullRegistrationError {
32    /// Rejected Flow type or app Worker ID queue.
33    pub queue: String,
34    /// Original permanent gRPC rejection, preserving its code and details.
35    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/// Independent snapshot of one queue's registration state.
55#[derive(Clone, Debug)]
56pub struct PullQueueRegistrationState {
57    /// Flow type or app Worker ID queue name.
58    pub queue: String,
59    /// Whether the queue currently has an acknowledged stream.
60    pub has_registered_stream: bool,
61    /// Last permanent rejection, cleared when a stream successfully registers.
62    pub last_permanent_error: Option<Status>,
63}
64
65/// Independent readiness snapshot returned by [`crate::Worker::pull_registration_state`].
66#[derive(Clone, Debug, Default)]
67pub struct PullRegistrationState {
68    /// True after all queues register once, unless a queue has a permanent error and no registered stream.
69    pub is_ready: bool,
70    /// Queue snapshots sorted by name; empty for a listening Worker.
71    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(&registration);
267        let second = std::sync::Arc::clone(&registration);
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}