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
#![warn(clippy::or_fun_call)]
28
#![deny(clippy::print_stderr)]
29
#![deny(clippy::print_stdout)]
30
#![warn(clippy::rc_buffer)]
31
#![deny(clippy::ref_option_ref)]
32
#![warn(clippy::semicolon_if_nothing_returned)]
33
#![warn(clippy::trait_duplication_in_bounds)]
34
#![deny(clippy::unchecked_time_subtraction)]
35
#![deny(clippy::unnecessary_wraps)]
36
#![warn(clippy::unseparated_literal_suffix)]
37
#![deny(clippy::unwrap_used)]
38
#![deny(clippy::mod_module_files)]
39
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
40
#![allow(clippy::uninlined_format_args)]
41
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
42
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
43
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
44
#![allow(clippy::needless_lifetimes)] // See arti#1765
45
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
46
#![allow(clippy::collapsible_if)] // See arti#2342
47
#![deny(clippy::unused_async)]
48
#![deny(clippy::string_slice)] // See arti#2571
49
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
50
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
51

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

            
61
mod err;
62
mod reactor;
63

            
64
pub mod http;
65

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

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

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

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

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

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

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

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

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

            
174
4
        runtime.spawn(reactor.run())?;
175

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
466
14
        self.n_failing > 0 || self.n_pending > 0
467
16
    }
468
}
469

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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