Skip to content

Commit ccbe0d1

Browse files
committed
Bug 2013527 - finer-grained locking for remote settings
Made it so `update_config` doesn't directly mutate the `RemoteSettingsClientInner` value, it just sets value in `pending_config` that will be applied the next time the client is locked.
1 parent 3718a06 commit ccbe0d1

File tree

2 files changed

+104
-20
lines changed

2 files changed

+104
-20
lines changed

components/remote_settings/src/client.rs

Lines changed: 103 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::signatures;
1010
use crate::storage::Storage;
1111
use crate::RemoteSettingsContext;
1212
use crate::{packaged_attachments, packaged_collections, RemoteSettingsServer};
13-
use parking_lot::Mutex;
13+
use parking_lot::{Mutex, MutexGuard};
1414
use serde::{Deserialize, Serialize};
1515
use sha2::{Digest, Sha256};
1616
use std::{
@@ -71,6 +71,9 @@ pub struct RemoteSettingsClient<C = ViaductApiClient> {
7171
// This is immutable, so it can be outside the mutex
7272
collection_name: String,
7373
inner: Mutex<RemoteSettingsClientInner<C>>,
74+
// Config that we got from `update_config`. This should be applied to
75+
// `RemoteSettingsClientInner` the next time it's used.
76+
pending_config: Mutex<Option<RemoteSettingsClientConfig>>,
7477
}
7578

7679
struct RemoteSettingsClientInner<C> {
@@ -79,6 +82,12 @@ struct RemoteSettingsClientInner<C> {
7982
jexl_filter: JexlFilter,
8083
}
8184

85+
struct RemoteSettingsClientConfig {
86+
server_url: BaseUrl,
87+
bucket_name: String,
88+
context: Option<RemoteSettingsContext>,
89+
}
90+
8291
// To initially download the dump (and attachments, if any), run:
8392
// $ cargo remote-settings dump-get --bucket main --collection-name <collection name>
8493
//
@@ -204,7 +213,27 @@ impl<C: ApiClient> RemoteSettingsClient<C> {
204213
api_client,
205214
jexl_filter,
206215
}),
216+
pending_config: Mutex::new(None),
217+
}
218+
}
219+
220+
/// Lock the `RemoteSettingsClientInner` field
221+
///
222+
/// This also applies the pending config if set.
223+
fn lock_inner(&self) -> Result<MutexGuard<'_, RemoteSettingsClientInner<C>>> {
224+
let pending_config = self.get_pending_config();
225+
let mut inner = self.inner.lock();
226+
if let Some(config) = pending_config {
227+
inner.api_client =
228+
C::create(config.server_url, config.bucket_name, &self.collection_name);
229+
inner.jexl_filter = JexlFilter::new(config.context);
230+
inner.storage.empty()?;
207231
}
232+
Ok(inner)
233+
}
234+
235+
fn get_pending_config(&self) -> Option<RemoteSettingsClientConfig> {
236+
self.pending_config.lock().take()
208237
}
209238

210239
pub fn collection_name(&self) -> &str {
@@ -273,7 +302,7 @@ impl<C: ApiClient> RemoteSettingsClient<C> {
273302
/// If records are not present in storage this will normally return None. Use `sync_if_empty =
274303
/// true` to change this behavior and perform a network request in this case.
275304
pub fn get_records(&self, sync_if_empty: bool) -> Result<Option<Vec<RemoteSettingsRecord>>> {
276-
let mut inner = self.inner.lock();
305+
let mut inner = self.lock_inner()?;
277306
let collection_url = inner.api_client.collection_url();
278307

279308
// Case 1: The packaged data is more recent than the cache
@@ -322,7 +351,7 @@ impl<C: ApiClient> RemoteSettingsClient<C> {
322351
}
323352

324353
pub fn get_last_modified_timestamp(&self) -> Result<Option<u64>> {
325-
let mut inner = self.inner.lock();
354+
let mut inner = self.lock_inner()?;
326355
let collection_url = inner.api_client.collection_url();
327356
inner.storage.get_last_modified_timestamp(&collection_url)
328357
}
@@ -332,7 +361,7 @@ impl<C: ApiClient> RemoteSettingsClient<C> {
332361
/// 2. Fetches the changeset from the remote server based on the last modified timestamp.
333362
/// 3. Inserts the fetched changeset into local storage.
334363
fn perform_sync_operation(&self) -> Result<()> {
335-
let mut inner = self.inner.lock();
364+
let mut inner = self.lock_inner()?;
336365
let collection_url = inner.api_client.collection_url();
337366
let timestamp = inner.storage.get_last_modified_timestamp(&collection_url)?;
338367
let changeset = inner.api_client.fetch_changeset(timestamp)?;
@@ -374,7 +403,7 @@ impl<C: ApiClient> RemoteSettingsClient<C> {
374403

375404
fn reset_storage(&self) -> Result<()> {
376405
trace!("{0}: reset local storage.", self.collection_name);
377-
let mut inner = self.inner.lock();
406+
let mut inner = self.lock_inner()?;
378407
let collection_url = inner.api_client.collection_url();
379408
// Clear existing storage
380409
inner.storage.empty()?;
@@ -405,7 +434,7 @@ impl<C: ApiClient> RemoteSettingsClient<C> {
405434

406435
#[cfg(feature = "signatures")]
407436
fn verify_signature(&self) -> Result<()> {
408-
let mut inner = self.inner.lock();
437+
let mut inner = self.lock_inner()?;
409438
let collection_url = inner.api_client.collection_url();
410439
let timestamp = inner.storage.get_last_modified_timestamp(&collection_url)?;
411440
let records = inner.storage.get_records(&collection_url)?;
@@ -471,7 +500,7 @@ impl<C: ApiClient> RemoteSettingsClient<C> {
471500
.as_ref()
472501
.ok_or_else(|| Error::RecordAttachmentMismatchError("No attachment metadata".into()))?;
473502

474-
let mut inner = self.inner.lock();
503+
let mut inner = self.lock_inner()?;
475504
let collection_url = inner.api_client.collection_url();
476505

477506
// First try storage - it will only return data that matches our metadata
@@ -521,6 +550,20 @@ impl<C: ApiClient> RemoteSettingsClient<C> {
521550
.set_attachment(&collection_url, &metadata.location, &attachment)?;
522551
Ok(attachment)
523552
}
553+
554+
pub fn update_config(
555+
&self,
556+
server_url: BaseUrl,
557+
bucket_name: String,
558+
context: Option<RemoteSettingsContext>,
559+
) {
560+
let mut pending_config = self.pending_config.lock();
561+
*pending_config = Some(RemoteSettingsClientConfig {
562+
server_url,
563+
bucket_name,
564+
context,
565+
})
566+
}
524567
}
525568

526569
impl RemoteSettingsClient<ViaductApiClient> {
@@ -536,22 +579,13 @@ impl RemoteSettingsClient<ViaductApiClient> {
536579

537580
Self::new_from_parts(collection_name, storage, jexl_filter, api_client)
538581
}
539-
540-
pub fn update_config(
541-
&self,
542-
server_url: BaseUrl,
543-
bucket_name: String,
544-
context: Option<RemoteSettingsContext>,
545-
) -> Result<()> {
546-
let mut inner = self.inner.lock();
547-
inner.api_client = ViaductApiClient::new(server_url, &bucket_name, &self.collection_name);
548-
inner.jexl_filter = JexlFilter::new(context);
549-
inner.storage.empty()
550-
}
551582
}
552583

553584
#[cfg_attr(test, mockall::automock)]
554585
pub trait ApiClient {
586+
/// Create a new instance of the client
587+
fn create(server_url: BaseUrl, bucket_name: String, collection_name: &str) -> Self;
588+
555589
/// Get the Bucket URL for this client.
556590
///
557591
/// This is a URL that includes the server URL, bucket name, and collection name. This is used
@@ -609,6 +643,10 @@ impl ViaductApiClient {
609643
}
610644

611645
impl ApiClient for ViaductApiClient {
646+
fn create(server_url: BaseUrl, bucket_name: String, collection_name: &str) -> Self {
647+
Self::new(server_url, &bucket_name, collection_name)
648+
}
649+
612650
fn collection_url(&self) -> String {
613651
self.endpoints.collection_url.to_string()
614652
}
@@ -1882,6 +1920,7 @@ mod test_new_client {
18821920
#[cfg(test)]
18831921
mod jexl_tests {
18841922
use super::*;
1923+
use std::sync::{Arc, Weak};
18851924

18861925
#[test]
18871926
fn test_get_records_filtered_app_version_pass() {
@@ -2068,6 +2107,51 @@ mod jexl_tests {
20682107
Some(vec![])
20692108
);
20702109
}
2110+
2111+
// Test that we can't hit the deadlock described in
2112+
// https://bugzilla.mozilla.org/show_bug.cgi?id=2012955
2113+
#[test]
2114+
fn test_update_config_deadlock() {
2115+
let mut api_client = MockApiClient::new();
2116+
let rs_client_ref: Arc<Mutex<Weak<RemoteSettingsClient<MockApiClient>>>> =
2117+
Arc::new(Mutex::new(Weak::new()));
2118+
let rs_client_ref2 = rs_client_ref.clone();
2119+
2120+
api_client.expect_collection_url().returning(move || {
2121+
// While we're in the middle of `get_request` and have the `RemoteSettingsClientInner`
2122+
// locked, call `update_config` to try to trigger the deadlock.
2123+
rs_client_ref2
2124+
.lock()
2125+
.upgrade()
2126+
.expect("rs_client_ref not set")
2127+
.update_config(
2128+
BaseUrl::parse("https://example.com/").unwrap(),
2129+
"test-collection".to_string(),
2130+
None,
2131+
);
2132+
"http://rs.example.com/v1/buckets/main/collections/test-collection".into()
2133+
});
2134+
api_client.expect_is_prod_server().returning(|| Ok(false));
2135+
2136+
let context = RemoteSettingsContext {
2137+
app_version: Some("129.0.0".to_string()),
2138+
..Default::default()
2139+
};
2140+
let storage = Storage::new(":memory:".into());
2141+
2142+
let rs_client = Arc::new(RemoteSettingsClient::new_from_parts(
2143+
"test-collection".into(),
2144+
storage,
2145+
JexlFilter::new(Some(context)),
2146+
api_client,
2147+
));
2148+
*rs_client_ref.lock() = Arc::downgrade(&rs_client);
2149+
2150+
assert_eq!(
2151+
rs_client.get_records(false).expect("Error getting records"),
2152+
None,
2153+
);
2154+
}
20712155
}
20722156

20732157
#[cfg(feature = "signatures")]

components/remote_settings/src/service.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ impl RemoteSettingsService {
132132
base_url.clone(),
133133
bucket_name.clone(),
134134
config.app_context.clone(),
135-
)?;
135+
);
136136
}
137137
inner.base_url = base_url;
138138
inner.bucket_name = bucket_name;

0 commit comments

Comments
 (0)