1
#![cfg_attr(docsrs, feature(doc_cfg))]
2
#![doc = include_str!("../README.md")]
3
// @@ begin lint list maintained by maint/add_warning @@
4
#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5
#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6
#![warn(missing_docs)]
7
#![warn(noop_method_call)]
8
#![warn(unreachable_pub)]
9
#![warn(clippy::all)]
10
#![deny(clippy::await_holding_lock)]
11
#![deny(clippy::cargo_common_metadata)]
12
#![deny(clippy::cast_lossless)]
13
#![deny(clippy::checked_conversions)]
14
#![allow(clippy::cognitive_complexity)] // See arti#2556
15
#![deny(clippy::debug_assert_with_mut_call)]
16
#![deny(clippy::exhaustive_enums)]
17
#![deny(clippy::exhaustive_structs)]
18
#![deny(clippy::expl_impl_clone_on_copy)]
19
#![deny(clippy::fallible_impl_from)]
20
#![deny(clippy::implicit_clone)]
21
#![deny(clippy::large_stack_arrays)]
22
#![warn(clippy::manual_ok_or)]
23
#![deny(clippy::missing_docs_in_private_items)]
24
#![warn(clippy::needless_borrow)]
25
#![warn(clippy::needless_pass_by_value)]
26
#![warn(clippy::option_option)]
27
#![deny(clippy::print_stderr)]
28
#![deny(clippy::print_stdout)]
29
#![warn(clippy::rc_buffer)]
30
#![deny(clippy::ref_option_ref)]
31
#![warn(clippy::semicolon_if_nothing_returned)]
32
#![warn(clippy::trait_duplication_in_bounds)]
33
#![deny(clippy::unchecked_time_subtraction)]
34
#![deny(clippy::unnecessary_wraps)]
35
#![warn(clippy::unseparated_literal_suffix)]
36
#![deny(clippy::unwrap_used)]
37
#![deny(clippy::mod_module_files)]
38
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39
#![allow(clippy::uninlined_format_args)]
40
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43
#![allow(clippy::needless_lifetimes)] // See arti#1765
44
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45
#![allow(clippy::collapsible_if)] // See arti#2342
46
#![deny(clippy::unused_async)]
47
#![deny(clippy::string_slice)] // See arti#2571
48
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
49
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
50

            
51
use async_trait::async_trait;
52
use futures::{
53
    StreamExt as _,
54
    task::{Spawn, SpawnError},
55
};
56
use postage::watch;
57
use std::{collections::HashSet, fmt::Debug, hash::Hash, sync::Arc, sync::Mutex, time::Duration};
58
use tor_rtcompat::SpawnExt as _;
59

            
60
mod err;
61
mod reactor;
62

            
63
pub mod http;
64

            
65
pub use err::{Rejection, UploadError};
66

            
67
/// An object that can upload documents of a given type to targets of a given type.
68
///
69
/// See type and method documentation for details on how to implement this type correctly.
70
#[async_trait]
71
pub trait Uploader: Send + Sync + 'static {
72
    /// The type of document we are uploading.
73
    ///
74
    /// Typically, this will be `str` or `[u8]`,
75
    /// but other types are possible.
76
    ///
77
    /// We pass this around in an [`Arc`],
78
    /// so it is allowed to be quite large.
79
    type Doc: ?Sized;
80

            
81
    /// A single target to which we're uploading a document.
82
    ///
83
    /// For a simple HTTP(S) upload this could be a `Vec` of addresses.
84
    ///
85
    /// In a Tor context, it could be a ChanTarget or a CircTarget.
86
    ///
87
    /// We pass this around in an [`Arc`],
88
    /// so the size doesn't much matter.
89
    ///
90
    /// We require that this type implements Eq and Hash,
91
    /// so that we can tell when a target has changed.
92
    type Target: ?Sized;
93

            
94
    /// Try to upload `document` to `target`.
95
    ///
96
    /// Return Ok(()) on success; return an error on failure.
97
    ///
98
    /// If it is possible for the target to reject a document,
99
    /// this method must return [`UploadError::Rejected`]
100
    /// in that case.
101
    ///
102
    /// If it is possible for the target to say
103
    /// "I am overloaded, come back later",
104
    /// the implementor must return [`UploadError::Deferred`]
105
    /// in that case.
106
    ///
107
    /// It is the implementor's responsibility to provide:
108
    /// - Timeout behavior, if desired.
109
    /// - [Happy-eyeballs] address selection, if desired.
110
    ///
111
    /// [Happy-eyeballs]: https://en.wikipedia.org/wiki/Happy_Eyeballs
112
    async fn upload(
113
        &self,
114
        target: Arc<Self::Target>,
115
        document: Arc<Self::Doc>,
116
    ) -> Result<(), UploadError>;
117
}
118

            
119
/// A handle to a publisher object that manages uploading a document to a set of targets.
120
///
121
/// See the [crate documentation](crate) for more information on this type and how to use it.
122
pub struct Publisher<D, T>
123
where
124
    T: Hash + Eq + Send + Sync + Debug + 'static + ?Sized,
125
    D: Send + Sync + 'static + ?Sized,
126
{
127
    /// A sender that we use to tell the reactor what actions to take.
128
    directive: Mutex<watch::Sender<PublishDirective<D, T>>>,
129

            
130
    /// A receiver to tell us about publication progress.
131
    status: watch::Receiver<PublishStatus>,
132
}
133

            
134
impl<D, T> Publisher<D, T>
135
where
136
    T: Hash + Eq + Send + Sync + Debug + 'static + ?Sized,
137
    D: Send + Sync + 'static + ?Sized,
138
{
139
    /// Create and launch a new [`Publisher`] to deliver `initial_document` to `initial_targets`.
140
    ///
141
    /// `description` should be a string describing what we're publishing, for the benefit of logs.
142
    ///
143
    /// (This method launches a background task.)
144
8
    pub fn launch<R, UP>(
145
8
        runtime: &R,
146
8
        description: String,
147
8
        initial_document: Option<Arc<D>>,
148
8
        initial_targets: HashSet<Arc<T>>,
149
8
        initial_retry_delay: Duration,
150
8
        uploader: Arc<UP>,
151
8
    ) -> Result<Arc<Self>, SpawnError>
152
8
    where
153
8
        UP: Uploader<Doc = D, Target = T>,
154
8
        R: tor_rtcompat::SleepProvider + Spawn,
155
    {
156
8
        let n_targets = initial_targets.len();
157
8
        let action = PublishDirective::new(initial_document, initial_targets);
158
8
        let status = PublishStatus::new(action.document.version, n_targets);
159

            
160
8
        let (action, action_rcv) = watch::channel_with(action);
161
8
        let (status_snd, status) = watch::channel_with(status);
162
8
        let action = Mutex::new(action);
163

            
164
8
        let reactor = reactor::PublishReactor::new(
165
8
            runtime.clone(),
166
8
            description,
167
8
            action_rcv,
168
8
            status_snd,
169
8
            initial_retry_delay,
170
8
            uploader,
171
        );
172

            
173
8
        runtime.spawn(reactor.run())?;
174

            
175
8
        Ok(Arc::new(Self {
176
8
            directive: action,
177
8
            status,
178
8
        }))
179
8
    }
180

            
181
    /// Change the current document and publish something else instead.
182
    ///
183
    /// - Any currently in-flight attempts to publish the old document will be allowed to finish,
184
    ///   but we will not wait for them before launching attempts to publish the new one.
185
    /// - If any target has rejected or accepted the old document,
186
    ///   we will try sending it the new one.
187
    ///
188
    /// If `reset_failing_targets` is true, then any targets that are currently waiting before they retry
189
    /// will be told to retry immediately.
190
8
    pub fn set_document(&self, new_document: Option<Arc<D>>, reset_failing_targets: bool) {
191
8
        let mut action_guard = self.directive.lock().expect("poisoned lock");
192
8
        let mut action = action_guard.borrow_mut();
193
8
        let version = action.document.version.next();
194
8
        action.document = Document {
195
8
            contents: new_document,
196
8
            version,
197
8
        };
198
8
        if reset_failing_targets {
199
            action.reset_failures_count += 1;
200
8
        }
201
8
    }
202

            
203
    /// Reset the failure counters and timeouts for all targets that are currently failing.
204
    ///
205
    /// Ordinarily, once a target has failed, we wait a while before we try it again.
206
    /// Calling this function makes the next attempt happen right away.
207
    pub fn reset_failing_targets(&self) {
208
        let mut action_guard = self.directive.lock().expect("poisoned lock");
209
        let mut action = action_guard.borrow_mut();
210
        action.reset_failures_count += 1;
211
    }
212

            
213
    /// Change the current set of targets by calling `modify` on it.
214
    ///
215
    /// If targets are added, upload attempts will be launched for them.
216
    ///
217
    /// If targets are removed, then any in-flight attempts to upload to them will be allowed to finish,
218
    /// but no further attempts will be launched.
219
    ///
220
    /// (As a consequence, if the set of targets is cleared completely,
221
    /// then all in-flight attempts will be allowed to finish, and no further attempts will be made.)
222
8
    pub fn adjust_targets<F>(&self, modify: F)
223
8
    where
224
8
        F: FnOnce(&mut HashSet<Arc<T>>),
225
    {
226
8
        let mut action_guard = self.directive.lock().expect("poisoned lock");
227
8
        let mut action = action_guard.borrow_mut();
228
8
        modify(&mut action.targets);
229
8
    }
230

            
231
    /// Tell the underlying reactor to stop.
232
    ///
233
    /// All inflight attempts to upload will be halted immediately.  This [`Publisher`] object will no
234
    /// longer be usable.
235
    ///
236
    /// This method will return right away.
237
    pub fn stop(&self) {
238
        let mut action_guard = self.directive.lock().expect("poisoned lock");
239
        let mut action = action_guard.borrow_mut();
240
        action.shutdown = true;
241
    }
242

            
243
    /// Tell the underlying reactor to stop, and wait for it to shut down.
244
    ///
245
    /// All inflight attempts to upload will be halted immediately.
246
    /// This [`Publisher`] object will no longer be usable.
247
    ///
248
    /// This method will wait for the underlying reactor task to report that it has exited.
249
    pub async fn shutdown(&self) {
250
        self.stop();
251
        let mut status = self.status.clone();
252
        while status.next().await.is_some() {}
253
    }
254

            
255
    /// Return the current document that we are trying to publish.
256
    pub fn document(&self) -> Option<Arc<D>> {
257
        self.directive
258
            .lock()
259
            .expect("poisoned lock")
260
            .borrow()
261
            .document
262
            .contents
263
            .clone()
264
    }
265

            
266
    /// Return the current targets to which we are trying to publish.
267
    pub fn targets(&self) -> HashSet<Arc<T>> {
268
        self.directive
269
            .lock()
270
            .expect("poisoned lock")
271
            .borrow()
272
            .targets
273
            .clone()
274
    }
275

            
276
    /// Return a [`Stream`](futures::Stream) of [`PublishStatus`] objects
277
    /// representing changes to this publisher's status.
278
    ///
279
    /// Intermediate states may be omitted if the state changes more frequently
280
    /// than this stream is polled.
281
    pub fn watch_status(&self) -> impl futures::Stream<Item = PublishStatus> {
282
        self.status.clone()
283
    }
284

            
285
    /// Return a [`Stream`](futures::Stream) of [`PublishStatus`] objects
286
    /// representing changes to this publisher's status with respect to the current
287
    /// document.
288
    ///
289
    /// Intermediate states may be omitted if the state changes more frequently
290
    /// than this stream is polled.
291
8
    pub fn watch_current_document_status(&self) -> impl futures::Stream<Item = PublishStatus> {
292
        use futures::future::ready;
293
8
        let cur_doc_version = self
294
8
            .directive
295
8
            .lock()
296
8
            .expect("Lock poisoned")
297
8
            .borrow()
298
8
            .document
299
8
            .version;
300

            
301
8
        self.status
302
8
            .clone()
303
            // This combination of take_while and filter is a little subtle!
304
            // The "take_while" causes the stream to be done (and return None) whenever the
305
            // status publisher is talking about a _later_ version of the document.
306
            // The "filter" discards all the values from the stream for which cur_doc_version
307
            // is _less_ than the current version.
308
            //
309
            // It might be nice to have a single tor-async-utils implementation for this
310
            // kind of thing, if we find that we're using it regularly.
311
20
            .take_while(move |s| ready(s.document_version <= cur_doc_version))
312
20
            .filter(move |s| ready(s.document_version == cur_doc_version))
313
8
    }
314

            
315
    /// Return this publisher's current [`PublishStatus`].
316
36
    pub fn status(&self) -> PublishStatus {
317
36
        self.status.borrow().clone()
318
36
    }
319
}
320

            
321
/// A description of the current operation that the [`Publisher`] is telling
322
/// the [`PublishReactor`](reactor::PublishReactor) to perform.
323
///
324
/// We use [`postage::watch`] to share changes in this object.
325
#[derive(educe::Educe, Debug)]
326
#[educe(Clone)]
327
struct PublishDirective<D: ?Sized, T: Hash + Eq + ?Sized> {
328
    /// If true, the reactor should shut down right away.
329
    shutdown: bool,
330

            
331
    /// The current document we're trying to publish, and its associated version number.
332
    document: Document<D>,
333

            
334
    /// A set of targets to which we want to publish.
335
    targets: HashSet<Arc<T>>,
336

            
337
    /// A counter that we increment whenever we want to reset
338
    /// the failure status for every target.
339
    ///
340
    /// Whenever the reactor sees that this value has changed,
341
    /// it marks every target as ready to try uploading again.
342
    reset_failures_count: usize,
343
}
344

            
345
impl<D: ?Sized, T: Hash + Eq + ?Sized> PublishDirective<D, T> {
346
    /// Construct a new [`PublishDirective`].
347
8
    fn new(document: Option<Arc<D>>, targets: HashSet<Arc<T>>) -> Self {
348
8
        Self {
349
8
            shutdown: false,
350
8
            document: Document {
351
8
                version: DocVersion(0.into()),
352
8
                contents: document,
353
8
            },
354
8
            targets,
355
8
            reset_failures_count: 0,
356
8
        }
357
8
    }
358
}
359

            
360
/// The version of a document.
361
///
362
/// (We use versions rather than Eq on documents, since they are allowed to be quite large.)
363
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
364
struct DocVersion(
365
    // This is sensitive because we may want to use it for hsdesc uploads.
366
    safelog::Sensitive<u64>,
367
);
368

            
369
impl DocVersion {
370
    /// Return the next document version in sequence.
371
8
    fn next(&self) -> Self {
372
8
        let n = (*self.0) + 1;
373
8
        Self(n.into())
374
8
    }
375
}
376

            
377
/// A document we're trying to publish.
378
#[derive(educe::Educe, Debug)]
379
#[educe(Clone)]
380
struct Document<D: ?Sized> {
381
    /// The version of this document.
382
    ///
383
    /// Versions are scoped to a single [`Publisher`].
384
    version: DocVersion,
385

            
386
    /// The document itself.
387
    ///
388
    /// This may be None to indicate that we have nothing to publish at present.
389
    contents: Option<Arc<D>>,
390
}
391

            
392
/// The current status of a [`Publisher`]'s attempt to publish the current document.
393
//
394
// This information is as reported by the [`PublishReactor`](reactor::PublishReactor)
395
// to the [`Publisher`].
396
//
397
// We use [`postage::watch`] to share changes in this object.
398
#[derive(Clone, Debug, Eq, PartialEq)]
399
pub struct PublishStatus {
400
    /// The version of the document that we're trying to upload.
401
    ///
402
    /// All the counters in this struct are with respect to _this_ version of the document.
403
    document_version: DocVersion,
404

            
405
    /// The number of targets we are configured to publish to.
406
    n_targets: usize,
407

            
408
    /// The number of targets that have acknowledged that there is no document to publish.
409
    n_inert: usize,
410

            
411
    /// The number of targets we have successfully published to.
412
    n_published: usize,
413

            
414
    /// The number of targets that rejected this document.
415
    n_rejected: usize,
416

            
417
    /// The number of targets that have failed in some non-retriable way.
418
    n_failed_permanently: usize,
419

            
420
    /// The number of targets for which we have encountered at least one retriable failure,
421
    /// and are still trying to upload to.
422
    n_failing: usize,
423

            
424
    /// The number of targets that we are trying to upload the document to for the first time.
425
    n_pending: usize,
426

            
427
    /// True if the reactor has begun running.
428
    initialized: bool,
429

            
430
    /// True if the reactor has shut down.
431
    shutdown: bool,
432
}
433

            
434
// TODO: Right now the accessors for this struct are fairly coarse.
435
// We may want to provide better ones.
436
impl PublishStatus {
437
    /// Construct a new PublishStatus.
438
8
    fn new(document_version: DocVersion, n_targets: usize) -> Self {
439
8
        Self {
440
8
            document_version,
441
8
            n_targets,
442
8
            n_inert: 0,
443
8
            n_published: 0,
444
8
            n_rejected: 0,
445
8
            n_failed_permanently: 0,
446
8
            n_failing: 0,
447
8
            n_pending: 0,
448
8
            initialized: false,
449
8
            shutdown: false,
450
8
        }
451
8
    }
452

            
453
    /// Return true if there is any activity in progress, according to this status.
454
    ///
455
    /// This function returns true if we are uploading to any target,
456
    /// or waiting to upload to any target.
457
48
    pub fn is_active(&self) -> bool {
458
48
        if self.shutdown {
459
            return false;
460
48
        }
461
48
        if !self.initialized {
462
4
            return true;
463
44
        }
464

            
465
44
        self.n_failing > 0 || self.n_pending > 0
466
48
    }
467
}
468

            
469
impl std::fmt::Display for PublishStatus {
470
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471
        let Self {
472
            document_version,
473
            n_targets,
474
            n_inert,
475
            n_published,
476
            n_rejected,
477
            n_failed_permanently,
478
            n_failing,
479
            n_pending,
480
            initialized,
481
            shutdown,
482
        } = self;
483
        let n = n_targets;
484
        let status = if !*initialized {
485
            "not initialized"
486
        } else if *shutdown {
487
            "shut down"
488
        } else if self.is_active() {
489
            "in progress"
490
        } else if self.n_inert == self.n_targets {
491
            "paused"
492
        } else if self.n_published == self.n_targets {
493
            "successful"
494
        } else if self.n_published == 0 {
495
            "failed"
496
        } else {
497
            "partially successful"
498
        };
499
        let version = document_version.0;
500

            
501
        write!(
502
            f,
503
            "Document {version} upload {status}. Of {n} upload targets",
504
        )?;
505

            
506
        let mut w = |n, s| {
507
            if n != 0 {
508
                write!(f, ", {n} {s}")
509
            } else {
510
                Ok(())
511
            }
512
        };
513

            
514
        w(*n_inert, "are paused")?;
515
        w(*n_published, "have succeeded")?;
516
        w(*n_rejected, "have rejected the document")?;
517
        w(*n_failed_permanently, "have failed non-retriably")?;
518
        w(*n_failing, "are failing")?;
519
        w(*n_pending, "are pending")?;
520
        Ok(())
521
    }
522
}
523

            
524
#[cfg(test)]
525
mod test {
526
    // @@ begin test lint list maintained by maint/add_warning @@
527
    #![allow(clippy::bool_assert_comparison)]
528
    #![allow(clippy::clone_on_copy)]
529
    #![allow(clippy::dbg_macro)]
530
    #![allow(clippy::mixed_attributes_style)]
531
    #![allow(clippy::print_stderr)]
532
    #![allow(clippy::print_stdout)]
533
    #![allow(clippy::single_char_pattern)]
534
    #![allow(clippy::unwrap_used)]
535
    #![allow(clippy::unchecked_time_subtraction)]
536
    #![allow(clippy::useless_vec)]
537
    #![allow(clippy::needless_pass_by_value)]
538
    #![allow(clippy::string_slice)] // See arti#2571
539
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
540
    use super::*;
541
    use std::collections::HashMap;
542
    use tor_rtmock::MockRuntime;
543

            
544
    /// State for a single target in our tests.
545
    #[derive(Clone, Debug, Default)]
546
    struct TState {
547
        should_reject: bool,
548
        should_fail: u8,
549
        #[allow(clippy::rc_buffer)]
550
        document: Option<Arc<String>>,
551
    }
552

            
553
    struct TestUploader {
554
        state: Arc<Mutex<HashMap<u32, TState>>>,
555
    }
556

            
557
    #[async_trait]
558
    impl Uploader for TestUploader {
559
        type Doc = String;
560
        type Target = u32;
561
        async fn upload(&self, target: Arc<u32>, document: Arc<String>) -> Result<(), UploadError> {
562
            let mut map = self.state.lock().unwrap();
563
            let entry: &mut TState = map.entry(*target).or_default();
564
            if entry.should_reject {
565
                Err(UploadError::Rejected(Rejection::from_message(
566
                    "document refused".into(),
567
                )))
568
            } else if entry.should_fail > 0 {
569
                entry.should_fail -= 1;
570
                Err(UploadError::Timeout) // This is a pretend error, but it'll work fine.
571
            } else {
572
                entry.document = Some(document);
573
                Ok(())
574
            }
575
        }
576
    }
577

            
578
    #[test]
579
    fn successful_upload() {
580
        MockRuntime::test_with_various(|rt| async move {
581
            let state = Arc::new(Mutex::new(HashMap::new()));
582
            let uploader = TestUploader {
583
                state: Arc::clone(&state),
584
            };
585

            
586
            let targets = [1, 2, 3].into_iter().map(Arc::new).collect();
587

            
588
            let publisher = Publisher::launch(
589
                &rt,
590
                "Testing".into(),
591
                None,
592
                targets,
593
                Duration::new(1, 0),
594
                Arc::new(uploader),
595
            )
596
            .unwrap();
597

            
598
            // Kick off an initial upload.
599
            publisher.set_document(Some(Arc::new("hello world".into())), false);
600
            let mut status = publisher.watch_current_document_status();
601
            while let Some(s) = status.next().await {
602
                if !s.is_active() {
603
                    break;
604
                }
605
            }
606

            
607
            assert_eq!(state.lock().unwrap().len(), 3);
608
            for n in 1..=3 {
609
                let map = state.lock().unwrap();
610
                assert_eq!(
611
                    map.get(&n).unwrap().document,
612
                    Some(Arc::new("hello world".into()))
613
                );
614
            }
615

            
616
            // Add a target 4.
617
            publisher.adjust_targets(|targets| {
618
                targets.insert(Arc::new(4));
619
            });
620
            while let Some(s) = status.next().await {
621
                if !s.is_active() {
622
                    break;
623
                }
624
            }
625
            assert_eq!(
626
                state.lock().unwrap().get(&4).unwrap().document,
627
                Some(Arc::new("hello world".into()))
628
            );
629

            
630
            // Drop target 1, then replace the document.
631
            publisher.adjust_targets(|targets| {
632
                targets.remove(&1);
633
            });
634
            publisher.set_document(Some(Arc::new("HELLO WORLD".into())), false);
635

            
636
            let mut status = publisher.watch_current_document_status();
637
            while let Some(s) = status.next().await {
638
                if !s.is_active() {
639
                    break;
640
                }
641
            }
642

            
643
            for n in 1..=4 {
644
                let map = state.lock().unwrap();
645
                let s = if n == 1 { "hello world" } else { "HELLO WORLD" };
646
                assert_eq!(map.get(&n).unwrap().document, Some(Arc::new(s.into())));
647
            }
648
        });
649
    }
650

            
651
    #[test]
652
    fn test_with_retries() {
653
        MockRuntime::test_with_various(|rt| async move {
654
            let state = Arc::new(Mutex::new(HashMap::new()));
655
            let uploader = TestUploader {
656
                state: Arc::clone(&state),
657
            };
658

            
659
            let targets = [1, 2, 3].into_iter().map(Arc::new).collect();
660
            for t in 1..=3 {
661
                state.lock().unwrap().insert(
662
                    t,
663
                    TState {
664
                        should_reject: false,
665
                        should_fail: t as u8,
666
                        document: None,
667
                    },
668
                );
669
            }
670

            
671
            let publisher = Publisher::launch(
672
                &rt,
673
                "Testing".into(),
674
                Some(Arc::new("hello world".into())),
675
                targets,
676
                Duration::new(1, 0),
677
                Arc::new(uploader),
678
            )
679
            .unwrap();
680

            
681
            while publisher.status().is_active() {
682
                rt.advance_by(Duration::new(1, 0)).await;
683
            }
684

            
685
            for n in 1..=3 {
686
                let map = state.lock().unwrap();
687
                let e = map.get(&n).unwrap();
688
                assert_eq!(e.document, Some(Arc::new("hello world".into())));
689
                assert_eq!(e.should_fail, 0);
690
            }
691
        });
692
    }
693
}