1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use std::{collections::HashMap, error::Error, io, sync::Arc, time::Duration};

use actix_web::rt;
use cadence::{CountedExt, StatsdClient};
use serde_derive::Deserialize;
use tokio::sync::RwLock;

use crate::broadcast::{Broadcast, BroadcastChangeTracker};

/// The payload provided by the Megaphone service
#[derive(Deserialize)]
pub struct MegaphoneResponse {
    pub broadcasts: HashMap<String, String>,
}

/// Initialize the `BroadcastChangeTracker`
///
/// Immediately populates it with the current Broadcasts polled from the
/// Megaphone service, then spawns a background task to periodically refresh
/// it
pub async fn init_and_spawn_megaphone_updater(
    broadcaster: &Arc<RwLock<BroadcastChangeTracker>>,
    http: &reqwest::Client,
    metrics: &Arc<StatsdClient>,
    url: &str,
    token: &str,
    poll_interval: Duration,
) -> reqwest::Result<()> {
    updater(broadcaster, http, url, token).await?;

    let broadcaster = Arc::clone(broadcaster);
    let http = http.clone();
    let metrics = Arc::clone(metrics);
    let url = url.to_owned();
    let token = token.to_owned();
    rt::spawn(async move {
        loop {
            rt::time::sleep(poll_interval).await;
            if let Err(e) = updater(&broadcaster, &http, &url, &token).await {
                report_updater_error(&metrics, e);
            } else {
                metrics.incr_with_tags("megaphone.updater.ok").send();
            }
        }
    });

    Ok(())
}

/// Emits a log, metric and Sentry event depending on the type of Error
fn report_updater_error(metrics: &Arc<StatsdClient>, err: reqwest::Error) {
    let reason = if err.is_timeout() {
        "timeout"
    } else if err.is_connect() {
        "connect"
    } else if is_io(&err) {
        "io"
    } else {
        "unknown"
    };
    metrics
        .incr_with_tags("megaphone.updater.error")
        .with_tag("reason", reason)
        .send();
    if reason == "unknown" {
        error!("📢megaphone::updater failed: {}", err);
        sentry::capture_event(sentry::event_from_error(&err));
    } else {
        trace!("📢megaphone::updater failed (reason: {}): {}", reason, err);
    }
}

/// Refresh the `BroadcastChangeTracker`'s Broadcasts from the Megaphone service
async fn updater(
    broadcaster: &Arc<RwLock<BroadcastChangeTracker>>,
    http: &reqwest::Client,
    url: &str,
    token: &str,
) -> reqwest::Result<()> {
    trace!("📢megaphone::updater");
    let MegaphoneResponse { broadcasts } = http
        .get(url)
        .header("Authorization", token)
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;
    let broadcasts = Broadcast::from_hashmap(broadcasts);
    if !broadcasts.is_empty() {
        let change_count = broadcaster.write().await.add_broadcasts(broadcasts);
        trace!("📢 add_broadcast change_count: {:?}", change_count);
    }
    Ok(())
}

/// Determine if a source of [reqwest::Error] was a [hyper::Error] Io Error
fn is_io(err: &reqwest::Error) -> bool {
    let mut source = err.source();
    while let Some(err) = source {
        if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
            if is_hyper_io(hyper_err) {
                return true;
            }
        }
        source = err.source();
    }
    false
}

/// Determine if a source of [hyper::Error] was an [io::Error]
fn is_hyper_io(err: &hyper::Error) -> bool {
    let mut source = err.source();
    while let Some(err) = source {
        if err.downcast_ref::<io::Error>().is_some() {
            return true;
        }
        source = err.source();
    }
    false
}