Skip to content

Commit 729a20d

Browse files
committed
Remote Settings: use changes endpoint
Hit this endpoint in `RemoteSettingsService::sync` and use it to determine which collections actually need syncing.
1 parent 6c0105d commit 729a20d

File tree

5 files changed

+137
-11
lines changed

5 files changed

+137
-11
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77

88
#### BREAKING CHANGE
99
- Removed `Megazord.kt` and moved the contents to the new `RustComponentsInitializer.kt`.
10+
-
11+
## 🦊 What's Changed 🦊
12+
13+
### Remote Settings
14+
- `RemoteSettingsService::sync` is now more efficient. It checks the remote settings changes
15+
endpoint and only syncs collections that have been modified since the last sync.
1016

1117
## 🔧 What's Fixed 🔧
1218

components/remote_settings/src/client.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,12 @@ impl<C: ApiClient> RemoteSettingsClient<C> {
294294
})
295295
}
296296

297+
pub fn get_last_modified_timestamp(&self) -> Result<Option<u64>> {
298+
let mut inner = self.inner.lock();
299+
let collection_url = inner.api_client.collection_url();
300+
inner.storage.get_last_modified_timestamp(&collection_url)
301+
}
302+
297303
/// Synchronizes the local collection with the remote server by performing the following steps:
298304
/// 1. Fetches the last modified timestamp of the collection from local storage.
299305
/// 2. Fetches the changeset from the remote server based on the last modified timestamp.
@@ -950,7 +956,7 @@ impl Default for RemoteState {
950956
}
951957

952958
impl RemoteState {
953-
fn handle_backoff_hint(&mut self, response: &Response) -> Result<()> {
959+
pub fn handle_backoff_hint(&mut self, response: &Response) -> Result<()> {
954960
let extract_backoff_header = |header| -> Result<u64> {
955961
Ok(response
956962
.headers
@@ -973,7 +979,7 @@ impl RemoteState {
973979
Ok(())
974980
}
975981

976-
fn ensure_no_backoff(&mut self) -> Result<()> {
982+
pub fn ensure_no_backoff(&mut self) -> Result<()> {
977983
if let BackoffState::Backoff {
978984
observed_at,
979985
duration,

components/remote_settings/src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,8 @@ impl BaseUrl {
165165
// error for cannot-be-a-base URLs.
166166
self.url.path_segments_mut().unwrap()
167167
}
168+
169+
pub fn query_pairs_mut(&mut self) -> url::form_urlencoded::Serializer<'_, url::UrlQuery<'_>> {
170+
self.url.query_pairs_mut()
171+
}
168172
}

components/remote_settings/src/service.rs

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
use std::{
6-
collections::HashSet,
6+
collections::{HashMap, HashSet},
77
sync::{Arc, Weak},
88
};
99

1010
use camino::Utf8PathBuf;
1111
use parking_lot::Mutex;
12+
use serde::Deserialize;
13+
use viaduct::Request;
1214

1315
use crate::{
14-
config::BaseUrl, storage::Storage, RemoteSettingsClient, RemoteSettingsConfig2,
15-
RemoteSettingsContext, RemoteSettingsServer, Result,
16+
client::RemoteState, config::BaseUrl, error::Error, storage::Storage, RemoteSettingsClient,
17+
RemoteSettingsConfig2, RemoteSettingsContext, RemoteSettingsServer, Result,
1618
};
1719

1820
/// Internal Remote settings service API
@@ -25,6 +27,7 @@ struct RemoteSettingsServiceInner {
2527
base_url: BaseUrl,
2628
bucket_name: String,
2729
app_context: Option<RemoteSettingsContext>,
30+
remote_state: RemoteState,
2831
/// Weakrefs for all clients that we've created. Note: this stores the
2932
/// top-level/public `RemoteSettingsClient` structs rather than `client::RemoteSettingsClient`.
3033
/// The reason for this is that we return Arcs to the public struct to the foreign code, so we
@@ -51,6 +54,7 @@ impl RemoteSettingsService {
5154
base_url,
5255
bucket_name,
5356
app_context: config.app_context,
57+
remote_state: RemoteState::default(),
5458
clients: vec![],
5559
}),
5660
}
@@ -81,13 +85,30 @@ impl RemoteSettingsService {
8185
// Make sure we only sync each collection once, even if there are multiple clients
8286
let mut synced_collections = HashSet::new();
8387

84-
// TODO: poll the server using `/buckets/monitor/collections/changes/changeset` to fetch
85-
// the current timestamp for all collections. That way we can avoid fetching collections
86-
// we know haven't changed and also pass the `?_expected{ts}` param to the server.
88+
let mut inner = self.inner.lock();
89+
let changes = inner.fetch_changes()?;
90+
let change_map: HashMap<_, _> = changes
91+
.changes
92+
.iter()
93+
.map(|c| ((c.collection.as_str(), &c.bucket), c.last_modified))
94+
.collect();
95+
let bucket_name = inner.bucket_name.clone();
8796

88-
for client in self.inner.lock().active_clients() {
89-
if synced_collections.insert(client.collection_name()) {
90-
client.internal.sync()?;
97+
for client in inner.active_clients() {
98+
let client = &client.internal;
99+
let collection_name = client.collection_name();
100+
if let Some(client_last_modified) = client.get_last_modified_timestamp()? {
101+
if let Some(server_last_modified) = change_map.get(&(collection_name, &bucket_name))
102+
{
103+
if client_last_modified != *server_last_modified {
104+
log::trace!("skipping up-to-date collection: {collection_name}");
105+
continue;
106+
}
107+
}
108+
}
109+
if synced_collections.insert(collection_name.to_string()) {
110+
log::trace!("syncing collection: {collection_name}");
111+
client.sync()?;
91112
}
92113
}
93114
Ok(synced_collections.into_iter().collect())
@@ -134,4 +155,52 @@ impl RemoteSettingsServiceInner {
134155
});
135156
active_clients
136157
}
158+
159+
fn fetch_changes(&mut self) -> Result<Changes> {
160+
let mut url = self.base_url.clone();
161+
url.path_segments_mut()
162+
.push("buckets")
163+
.push("monitor")
164+
.push("collections")
165+
.push("changes")
166+
.push("changeset");
167+
// For now, always use `0` for the expected value. This means we'll get updates based on
168+
// the default TTL of 1 hour.
169+
//
170+
// Eventually, we should add support for push notifications and use the timestamp from the
171+
// notification.
172+
url.query_pairs_mut().append_pair("_expected", "0");
173+
let url = url.into_inner();
174+
log::trace!("make_request: {url}");
175+
self.remote_state.ensure_no_backoff()?;
176+
177+
let req = Request::get(url);
178+
let resp = req.send()?;
179+
180+
self.remote_state.handle_backoff_hint(&resp)?;
181+
182+
if resp.is_success() {
183+
Ok(resp.json()?)
184+
} else {
185+
Err(Error::ResponseError(format!(
186+
"status code: {}",
187+
resp.status
188+
)))
189+
}
190+
}
191+
}
192+
193+
/// Data from the changes endpoint
194+
///
195+
/// https://remote-settings.readthedocs.io/en/latest/client-specifications.html#endpoints
196+
#[derive(Debug, Deserialize)]
197+
struct Changes {
198+
changes: Vec<ChangesCollection>,
199+
}
200+
201+
#[derive(Debug, Deserialize)]
202+
struct ChangesCollection {
203+
collection: String,
204+
bucket: String,
205+
last_modified: u64,
137206
}

components/search/src/selector.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,6 +1919,18 @@ mod tests {
19191919
selector
19201920
}
19211921

1922+
fn mock_changes_endpoint() -> mockito::Mock {
1923+
mock(
1924+
"GET",
1925+
"/v1/buckets/monitor/collections/changes/changeset?_expected=0",
1926+
)
1927+
.with_body(response_body_changes())
1928+
.with_status(200)
1929+
.with_header("content-type", "application/json")
1930+
.with_header("etag", "\"1000\"")
1931+
.create()
1932+
}
1933+
19221934
fn response_body() -> String {
19231935
json!({
19241936
"metadata": {
@@ -2021,6 +2033,20 @@ mod tests {
20212033
.to_string()
20222034
}
20232035

2036+
fn response_body_changes() -> String {
2037+
json!({
2038+
"timestamp": 1000,
2039+
"changes": [
2040+
{
2041+
"collection": "search-config-v2",
2042+
"bucket": "main",
2043+
"last_modified": 1000,
2044+
}
2045+
],
2046+
})
2047+
.to_string()
2048+
}
2049+
20242050
fn response_body_locales() -> String {
20252051
json!({
20262052
"metadata": {
@@ -2129,6 +2155,7 @@ mod tests {
21292155

21302156
#[test]
21312157
fn test_remote_settings_empty_search_config_records_throws_error() {
2158+
let changes_mock = mock_changes_endpoint();
21322159
let m = mock(
21332160
"GET",
21342161
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2168,11 +2195,13 @@ mod tests {
21682195
.unwrap_err()
21692196
.to_string()
21702197
.contains("No search config v2 records received from remote settings"));
2198+
changes_mock.expect(1).assert();
21712199
m.expect(1).assert();
21722200
}
21732201

21742202
#[test]
21752203
fn test_remote_settings_search_config_records_is_none_throws_error() {
2204+
let changes_mock = mock_changes_endpoint();
21762205
let m1 = mock(
21772206
"GET",
21782207
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2197,11 +2226,13 @@ mod tests {
21972226
.unwrap_err()
21982227
.to_string()
21992228
.contains("No search config v2 records received from remote settings"));
2229+
changes_mock.expect(1).assert();
22002230
m1.expect(1).assert();
22012231
}
22022232

22032233
#[test]
22042234
fn test_remote_settings_empty_search_config_overrides_filtered_without_error() {
2235+
let changes_mock = mock_changes_endpoint();
22052236
let m1 = mock(
22062237
"GET",
22072238
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2248,12 +2279,14 @@ mod tests {
22482279
"Should have filtered the configuration using an empty search config overrides without causing an error. {:?}",
22492280
result
22502281
);
2282+
changes_mock.expect(1).assert();
22512283
m1.expect(1).assert();
22522284
m2.expect(1).assert();
22532285
}
22542286

22552287
#[test]
22562288
fn test_remote_settings_search_config_overrides_records_is_none_throws_error() {
2289+
let changes_mock = mock_changes_endpoint();
22572290
let m1 = mock(
22582291
"GET",
22592292
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2288,12 +2321,14 @@ mod tests {
22882321
.unwrap_err()
22892322
.to_string()
22902323
.contains("No search config overrides v2 records received from remote settings"));
2324+
changes_mock.expect(1).assert();
22912325
m1.expect(1).assert();
22922326
m2.expect(1).assert();
22932327
}
22942328

22952329
#[test]
22962330
fn test_filter_with_remote_settings_overrides() {
2331+
let changes_mock = mock_changes_endpoint();
22972332
let m1 = mock(
22982333
"GET",
22992334
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2355,12 +2390,15 @@ mod tests {
23552390
test_engine.clone(),
23562391
"Should have applied the overrides to the matching engine"
23572392
);
2393+
changes_mock.expect(1).assert();
23582394
m1.expect(1).assert();
23592395
m2.expect(1).assert();
23602396
}
23612397

23622398
#[test]
23632399
fn test_filter_with_remote_settings() {
2400+
let changes_mock = mock_changes_endpoint();
2401+
23642402
let m = mock(
23652403
"GET",
23662404
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2468,11 +2506,13 @@ mod tests {
24682506
},
24692507
"Should have selected the private default engine for the matching specific default"
24702508
);
2509+
changes_mock.expect(1).assert();
24712510
m.expect(1).assert();
24722511
}
24732512

24742513
#[test]
24752514
fn test_filter_with_remote_settings_negotiate_locales() {
2515+
let changes_mock = mock_changes_endpoint();
24762516
let m = mock(
24772517
"GET",
24782518
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2551,6 +2591,7 @@ mod tests {
25512591
},
25522592
"Should have selected the en-us engine when given another english locale we don't support"
25532593
);
2594+
changes_mock.expect(1).assert();
25542595
m.expect(1).assert();
25552596
}
25562597

0 commit comments

Comments
 (0)