operation_queue/line_token.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5//! Helpers for synchronizing operations (e.g. error handling) across futures.
6//!
7//! This module revolves around the [`Line`] struct, which is an asynchronous
8//! flow control structure that behaves a bit like a mutex, with the exception
9//! that consumers waiting for the [`Line`] to be released do not subsequently
10//! lock it.
11//!
12//! The design of a [`Line`] is inspired from the one of a [one-track railway
13//! line](https://en.wikipedia.org/wiki/Token_(railway_signalling)). To avoid
14//! collisions, conductors must acquire a token at the entrance to the line that
15//! ensures they're the only one on it. If the token is being held, traffic
16//! around this line stops until it's released again.
17//!
18//! Similarly, in a context with multiple parallel [`Future`]s, it might be
19//! necessary to ensure only one takes care of a given operation. For example,
20//! if multiple requests are being performed against the same service, and one
21//! of them hits an authentication error, it is likely the others will as well.
22//! In this case, it is preferrable to only let one future handle the error than
23//! let every request re-authenticate independently (in this example,
24//! credentials are the same across requests, and multiple simultaneous
25//! authentication attempts might cause issues with complex flows).
26//!
27//! Each future holds a shared on a [`Line`] (e.g. wrapped in an [`Rc`] or an
28//! [`Arc`]). Whenever a future needs to perform an operation that should only
29//! be performed once at a time, it attempts to acquire the line's token with
30//! [`Line::try_acquire_token`]. This function returns an enum
31//! ([`AcquireOutcome`]) describing one of two cases:
32//!
33//! * The line's token is available and has been acquired, and the future can
34//! start performing the operation immediately. It is granted the line's
35//! [`Token`], which it must hold in scope for the duration of the operation,
36//! as dropping it releases the line.
37//! * The line's token has already been acquired by another future, in which
38//! case the future must wait for the line to become available again. When the
39//! line becomes available again, the future does not need to acquire another
40//! token, as another future should have taken care of performing the
41//! operation.
42//!
43//! [`OperationQueue`]: crate::operation_queue::OperationQueue
44//! [`Future`]: std::future::Future
45//! [`Rc`]: std::rc::Rc
46//! [`Arc`]: std::sync::Arc
47
48use async_lock::Mutex;
49use futures::{FutureExt, future::Shared};
50use oneshot::{Receiver, Sender};
51
52/// A oneshot channel used internally by a [`Line`] that's been acquired to
53/// communicate that the token has been dropped and the line was released.
54///
55/// The channel's [`Receiver`] is wrapped in a [`Shared`] that can be cloned
56/// when a new consumer tries and fails to acquire a token for the line.
57struct ReleaseChannel {
58 sender: Sender<()>,
59 receiver: Shared<Receiver<()>>,
60}
61
62/// The current status of a line.
63pub enum LineStatus {
64 /// The line is free.
65 Free,
66
67 /// The line is busy, and the nested future can be used to wait till the
68 /// line becomes free again.
69 Busy(Shared<Receiver<()>>),
70}
71
72/// A [`Line`] from which a [`Token`] can be acquired.
73///
74/// # Thread safety
75///
76/// `Line` is thread-safe and can be shared across threads via [`Arc`]:
77///
78/// ```rust
79/// use std::sync::Arc;
80/// use operation_queue::line_token::Line;
81///
82/// fn assert_send_sync<T: Send + Sync>() {}
83/// assert_send_sync::<Line>();
84/// ```
85///
86/// [`Arc`]: std::sync::Arc
87#[derive(Default)]
88pub struct Line {
89 channel: Mutex<Option<ReleaseChannel>>,
90}
91
92impl Line {
93 /// Instantiates a new [`Line`].
94 pub fn new() -> Line {
95 Line {
96 channel: Default::default(),
97 }
98 }
99
100 /// Retrieve the current status of the [`Line`], i.e. whether it's currently
101 /// free or not.
102 ///
103 /// Unlike [`try_acquire_token`], this method does not attempt to lock the
104 /// line if it's free, and does not generate a [`Token`]. If the line is
105 /// busy, it returns a future the consumer can use to wait till the line
106 /// becomes free again.
107 ///
108 /// [`try_acquire_token`]: Self::try_acquire_token
109 pub async fn status(&self) -> LineStatus {
110 match self.channel.lock().await.as_ref() {
111 Some(channel) => LineStatus::Busy(channel.receiver.clone()),
112 None => LineStatus::Free,
113 }
114 }
115
116 /// Attempts to acquire a [`Token`] for this line.
117 ///
118 /// The [`Token`] automatically releases the line upon leaving the current
119 /// scope and getting dropped.
120 ///
121 /// If a [`Token`] has already been acquired for this line, a future to
122 /// `await` is returned instead. It resolves when the current token holder
123 /// has finished handling the current error and releases the line.
124 pub async fn try_acquire_token<'l>(&'l self) -> AcquireOutcome<'l> {
125 let mut channel = self.channel.lock().await;
126
127 if let Some(channel) = channel.as_ref() {
128 // Since the oneshot `Receiver` is wrapped in a `Shared`, cloning it
129 // will return a new handle on the `Shared` which will resolve at
130 // the same time as the others.
131 return AcquireOutcome::Failure(channel.receiver.clone());
132 }
133
134 // The line is currently available, create a new channel and give the
135 // consumer their token.
136 let (sender, receiver) = oneshot::channel();
137 *channel = Some(ReleaseChannel {
138 sender,
139 receiver: receiver.shared(),
140 });
141
142 AcquireOutcome::Success(Token { line: self })
143 }
144
145 /// Releases the line, and resolves the [`Shared`] future other consumers
146 /// might be awaiting.
147 pub(self) fn release(&self) {
148 // "Take" the channel out of the `Mutex`; on top of letting us access
149 // its content, we're also making sure that even if something bad
150 // happens then the line can be acquired again.
151 match self.channel.lock_blocking().take() {
152 Some(channel) => match channel.sender.send(()) {
153 Ok(_) => (),
154 Err(_) => log::error!("trying to release using a closed channel"),
155 },
156 None => log::error!("trying to release before acquiring"),
157 };
158 }
159}
160
161/// The outcome from trying to acquire a [`Token`] for a [`Line`].
162#[must_use = "if the token is unused the line will immediately release again"]
163pub enum AcquireOutcome<'ao> {
164 /// The line could be acquired and returned a token to hold on to.
165 ///
166 /// The token must remain in scope, as it will release the line when
167 /// dropped.
168 Success(Token<'ao>),
169
170 /// The line could not be acquired as another consumer is holding a token
171 /// for it.
172 ///
173 /// This variant includes a [`Shared`] future that resolves when the current
174 /// token holder drops it and releases the line.
175 Failure(Shared<Receiver<()>>),
176}
177
178impl<'ao> AcquireOutcome<'ao> {
179 /// Returns the [`AcquireOutcome`] if it's a success, otherwise returns a
180 /// success with the provided token if it's not [`None`].
181 ///
182 /// If the current [`AcquireOutcome`] is a failure, and the provided token
183 /// is [`None`], the failure is returned.
184 ///
185 /// # Design considerations
186 ///
187 /// One way to make this method more straightforward could have been to make
188 /// `token` be a [`Token`], not an [`Option`], but the current signature was
189 /// picked to simplify the consumers (which store the token, if any, in an
190 /// [`Option`]).
191 pub fn or_token(self, token: Option<Token<'ao>>) -> Self {
192 match self {
193 AcquireOutcome::Success(_) => self,
194 AcquireOutcome::Failure(_) => match token {
195 Some(token) => AcquireOutcome::Success(token),
196 None => self,
197 },
198 }
199 }
200}
201
202/// A token that symbolizes the current consumer holds exclusive access to the
203/// corresponding [`Line`].
204///
205/// The [`Line`] is automatically released when this token goes out of scope and
206/// is dropped.
207#[must_use = "if unused the line will immediately release again"]
208pub struct Token<'t> {
209 line: &'t Line,
210}
211
212impl Drop for Token<'_> {
213 fn drop(&mut self) {
214 self.line.release();
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use tokio::time::Duration;
221
222 use super::*;
223
224 async fn get_token(line: &Line) -> Token<'_> {
225 match line.try_acquire_token().await {
226 AcquireOutcome::Success(token) => token,
227 AcquireOutcome::Failure(_) => panic!("expected a token from try_acquire_token()"),
228 }
229 }
230
231 #[tokio::test(flavor = "current_thread")]
232 async fn acquire_token() {
233 let line = Line::new();
234
235 let _token = get_token(&line).await;
236
237 match line.try_acquire_token().await {
238 AcquireOutcome::Success(_) => {
239 panic!("should not be able to acquire the line while the token is in scope")
240 }
241 AcquireOutcome::Failure(_) => (),
242 }
243 }
244
245 #[tokio::test(flavor = "current_thread")]
246 async fn token_out_of_scope() {
247 let line = Line::new();
248
249 {
250 let _token = get_token(&line).await;
251
252 match line.try_acquire_token().await {
253 AcquireOutcome::Success(_) => {
254 panic!("should not be able to acquire the line while the token is in scope")
255 }
256 AcquireOutcome::Failure(_) => (),
257 }
258 }
259
260 match line.try_acquire_token().await {
261 AcquireOutcome::Success(_) => (),
262 AcquireOutcome::Failure(_) => {
263 panic!("expected a token now that the previous token has been dropped")
264 }
265 }
266 }
267
268 #[tokio::test(flavor = "current_thread")]
269 async fn or_token() {
270 let line = Line::new();
271
272 let token = get_token(&line).await;
273
274 match line.try_acquire_token().await.or_token(Some(token)) {
275 AcquireOutcome::Success(_) => (),
276 AcquireOutcome::Failure(_) => panic!("we should have kept our token"),
277 }
278 }
279
280 #[tokio::test(flavor = "current_thread")]
281 async fn line_release_on_drop() {
282 let line = Line::new();
283
284 // A mutable variable that will act as the test's success flag and will
285 // only be true if it succeeds.
286 let mut success = false;
287
288 // Acquire the line's token, sleep for a bit (10ms) and then drop it.
289 // The reason we sleep here is to give some time to `wait_for_line` to
290 // try (and fail) to acquire the line's token before we drop it.
291 async fn acquire_sleep_and_drop(line: &Line) {
292 let _token = get_token(&line).await;
293 tokio::time::sleep(Duration::from_millis(10)).await;
294 }
295
296 // Try (and fail) to acquire the token, then wait for the line to become
297 // available again. This function sets the success flag.
298 async fn wait_for_line(line: &Line, success: &mut bool) {
299 let shared = match line.try_acquire_token().await {
300 AcquireOutcome::Success(_) => {
301 panic!("should not be able to acquire the line while the token is in scope")
302 }
303 AcquireOutcome::Failure(shared) => shared,
304 };
305
306 shared.await.unwrap();
307 *success = true;
308 }
309
310 // Run both futures in parallel. `biased;` ensures the futures are
311 // polled in order (meaning `acquire_sleep_and_drop` is run first).
312 tokio::join! {
313 biased;
314 acquire_sleep_and_drop(&line),
315 wait_for_line(&line, &mut success),
316 };
317
318 assert!(success)
319 }
320
321 /// Tests the [`Line::status`] method.
322 ///
323 /// This test behaves a lot like [`line_release_on_drop`], except it waits
324 /// for the line to be freed via [`Line::status`] rather than
325 /// [`Line::try_acquire_token`].
326 #[tokio::test(flavor = "current_thread")]
327 async fn line_status() {
328 let line = Line::new();
329 assert!(matches!(line.status().await, LineStatus::Free));
330
331 // A mutable variable that will act as the test's success flag and will
332 // only be true if it succeeds.
333 let mut success = false;
334
335 // Acquire the line's token, sleep for a bit (10ms) and then drop it.
336 // The reason we sleep here is to give some time to `wait_for_line` to
337 // try (and fail) to acquire the line's token before we drop it.
338 async fn acquire_sleep_and_drop(line: &Line) {
339 let _token = get_token(&line).await;
340 tokio::time::sleep(Duration::from_millis(10)).await;
341 }
342
343 // Check that the line is busy, then wait for the line to become
344 // available again. This function sets the success flag.
345 async fn wait_for_line(line: &Line, success: &mut bool) {
346 let shared = match line.status().await {
347 LineStatus::Free => {
348 panic!("line should be busy")
349 }
350 LineStatus::Busy(shared) => shared,
351 };
352
353 shared.await.unwrap();
354 *success = true;
355 }
356
357 // Run both futures in parallel. `biased;` ensures the futures are
358 // polled in order (meaning `acquire_sleep_and_drop` is run first).
359 tokio::join! {
360 biased;
361 acquire_sleep_and_drop(&line),
362 wait_for_line(&line, &mut success),
363 };
364
365 assert!(matches!(line.status().await, LineStatus::Free));
366 assert!(success)
367 }
368}