remote_settings/
service.rs

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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::{
    collections::HashSet,
    sync::{Arc, Weak},
};

use camino::Utf8PathBuf;
use parking_lot::Mutex;
use url::Url;

use crate::{
    storage::Storage, RemoteSettingsClient, RemoteSettingsConfig2, RemoteSettingsContext,
    RemoteSettingsServer, Result,
};

/// Internal Remote settings service API
pub struct RemoteSettingsService {
    inner: Mutex<RemoteSettingsServiceInner>,
}

struct RemoteSettingsServiceInner {
    storage_dir: Utf8PathBuf,
    base_url: Url,
    bucket_name: String,
    /// Weakrefs for all clients that we've created.  Note: this stores the
    /// top-level/public `RemoteSettingsClient` structs rather than `client::RemoteSettingsClient`.
    /// The reason for this is that we return Arcs to the public struct to the foreign code, so we
    /// need to use the same type for our weakrefs.  The alternative would be to create 2 Arcs for
    /// each client, which is wasteful.
    clients: Vec<Weak<RemoteSettingsClient>>,
}

impl RemoteSettingsService {
    /// Construct a [RemoteSettingsService]
    ///
    /// This is typically done early in the application-startup process
    pub fn new(storage_dir: String, config: RemoteSettingsConfig2) -> Result<Self> {
        let storage_dir = storage_dir.into();
        let base_url = config
            .server
            .unwrap_or(RemoteSettingsServer::Prod)
            .get_url()?;
        let bucket_name = config.bucket_name.unwrap_or_else(|| String::from("main"));

        Ok(Self {
            inner: Mutex::new(RemoteSettingsServiceInner {
                storage_dir,
                base_url,
                bucket_name,
                clients: vec![],
            }),
        })
    }

    /// Create a new Remote Settings client
    #[cfg(feature = "jexl")]
    pub fn make_client(
        &self,
        collection_name: String,
        context: Option<RemoteSettingsContext>,
    ) -> Result<Arc<RemoteSettingsClient>> {
        let mut inner = self.inner.lock();
        let storage = Storage::new(inner.storage_dir.join(format!("{collection_name}.sql")))?;

        let client = Arc::new(RemoteSettingsClient::new(
            inner.base_url.clone(),
            inner.bucket_name.clone(),
            collection_name.clone(),
            context,
            storage,
        )?);
        inner.clients.push(Arc::downgrade(&client));
        Ok(client)
    }

    #[cfg(not(feature = "jexl"))]
    pub fn make_client(
        &self,
        collection_name: String,
        #[allow(unused_variables)] context: Option<RemoteSettingsContext>,
    ) -> Result<Arc<RemoteSettingsClient>> {
        let mut inner = self.inner.lock();
        let storage = Storage::new(inner.storage_dir.join(format!("{collection_name}.sql")))?;
        let client = Arc::new(RemoteSettingsClient::new(
            inner.base_url.clone(),
            inner.bucket_name.clone(),
            collection_name.clone(),
            storage,
        )?);
        inner.clients.push(Arc::downgrade(&client));
        Ok(client)
    }

    /// Sync collections for all active clients
    pub fn sync(&self) -> Result<Vec<String>> {
        // Make sure we only sync each collection once, even if there are multiple clients
        let mut synced_collections = HashSet::new();

        // TODO: poll the server using `/buckets/monitor/collections/changes/changeset` to fetch
        // the current timestamp for all collections.  That way we can avoid fetching collections
        // we know haven't changed and also pass the `?_expected{ts}` param to the server.

        for client in self.inner.lock().active_clients() {
            if synced_collections.insert(client.collection_name()) {
                client.internal.sync()?;
            }
        }
        Ok(synced_collections.into_iter().collect())
    }

    /// Update the remote settings config
    ///
    /// This will cause all current and future clients to use new config and will delete any stored
    /// records causing the clients to return new results from the new config.
    pub fn update_config(&self, config: RemoteSettingsConfig2) -> Result<()> {
        let base_url = config
            .server
            .unwrap_or(RemoteSettingsServer::Prod)
            .get_url()?;
        let bucket_name = config.bucket_name.unwrap_or_else(|| String::from("main"));
        let mut inner = self.inner.lock();
        for client in inner.active_clients() {
            client
                .internal
                .update_config(base_url.clone(), bucket_name.clone())?;
        }
        inner.base_url = base_url;
        inner.bucket_name = bucket_name;
        Ok(())
    }
}

impl RemoteSettingsServiceInner {
    // Find live clients in self.clients
    //
    // Also, drop dead weakrefs from the vec
    fn active_clients(&mut self) -> Vec<Arc<RemoteSettingsClient>> {
        let mut active_clients = vec![];
        self.clients.retain(|weak| {
            if let Some(client) = weak.upgrade() {
                active_clients.push(client);
                true
            } else {
                false
            }
        });
        active_clients
    }
}