Skip to content

Commit 5f3350a

Browse files
authored
fix: implement cache for authentication filestorage backend (#573)
We've seen issues in pixi where downloading becomes really slow because the `~/.rattler/auth.json` file is on a network file system. We also wanted to give better error messages if we cannot parse the JSON file. With this change, we cache the authentication information for the lifetime of the middleware. I think this should fix the two issues mentioned above.
1 parent 0e2a5f9 commit 5f3350a

File tree

2 files changed

+93
-54
lines changed

2 files changed

+93
-54
lines changed

crates/rattler_networking/src/authentication_middleware.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ mod tests {
176176
let mut storage = AuthenticationStorage::new();
177177
storage.add_backend(Arc::from(FileStorage::new(
178178
tdir.path().to_path_buf().join("auth.json"),
179-
)));
179+
)?));
180180

181181
let host = "test.example.com";
182182
let authentication = Authentication::CondaToken("testtoken".to_string());
@@ -191,7 +191,7 @@ mod tests {
191191
let mut storage = AuthenticationStorage::new();
192192
storage.add_backend(Arc::from(FileStorage::new(
193193
tdir.path().to_path_buf().join("auth.json"),
194-
)));
194+
)?));
195195

196196
let host = "conda.example.com";
197197

@@ -245,7 +245,7 @@ mod tests {
245245
let mut storage = AuthenticationStorage::new();
246246
storage.add_backend(Arc::from(FileStorage::new(
247247
tdir.path().to_path_buf().join("auth.json"),
248-
)));
248+
)?));
249249
let host = "bearer.example.com";
250250

251251
// Make sure the keyring is empty
@@ -305,7 +305,7 @@ mod tests {
305305
let mut storage = AuthenticationStorage::new();
306306
storage.add_backend(Arc::from(FileStorage::new(
307307
tdir.path().to_path_buf().join("auth.json"),
308-
)));
308+
)?));
309309
let host = "basic.example.com";
310310

311311
// Make sure the keyring is empty
@@ -383,7 +383,7 @@ mod tests {
383383
let mut storage = AuthenticationStorage::new();
384384
storage.add_backend(Arc::from(FileStorage::new(
385385
tdir.path().to_path_buf().join("auth.json"),
386-
)));
386+
)?));
387387

388388
let authentication = Authentication::BearerToken("testtoken".to_string());
389389

Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
//! file storage for passwords.
22
use anyhow::Result;
33
use fslock::LockFile;
4-
use once_cell::sync::Lazy;
5-
use std::collections::{BTreeMap, HashSet};
4+
use std::collections::BTreeMap;
5+
use std::path::Path;
6+
use std::sync::Arc;
67
use std::{path::PathBuf, sync::Mutex};
78

89
use crate::authentication_storage::StorageBackend;
910
use crate::Authentication;
1011

12+
#[derive(Clone, Debug)]
13+
struct FileStorageCache {
14+
cache: BTreeMap<String, Authentication>,
15+
file_exists: bool,
16+
}
17+
1118
/// A struct that implements storage and access of authentication
1219
/// information backed by a on-disk JSON file
1320
#[derive(Clone, Debug)]
1421
pub struct FileStorage {
1522
/// The path to the JSON file
1623
pub path: PathBuf,
24+
25+
/// The cache of the file storage
26+
/// This is used to avoid reading the file from disk every time
27+
/// a credential is accessed
28+
cache: Arc<Mutex<FileStorageCache>>,
1729
}
1830

1931
/// An error that can occur when accessing the file storage
@@ -32,83 +44,103 @@ pub enum FileStorageError {
3244
JSONError(#[from] serde_json::Error),
3345
}
3446

35-
impl FileStorage {
36-
/// Create a new file storage with the given path
37-
pub fn new(path: PathBuf) -> Self {
38-
Self { path }
47+
/// Lock the file storage file for reading and writing. This will block until the lock is
48+
/// acquired.
49+
fn lock_file_storage(path: &Path, write: bool) -> Result<Option<LockFile>, FileStorageError> {
50+
if !write && !path.exists() {
51+
return Ok(None);
3952
}
4053

41-
/// Lock the file storage file for reading and writing. This will block until the lock is
42-
/// acquired.
43-
fn lock(&self) -> Result<LockFile, FileStorageError> {
44-
std::fs::create_dir_all(self.path.parent().unwrap())?;
45-
let path = self.path.with_extension("lock");
46-
let mut lock = fslock::LockFile::open(&path)
54+
std::fs::create_dir_all(path.parent().unwrap())?;
55+
let path = path.with_extension("lock");
56+
let mut lock = fslock::LockFile::open(&path)
57+
.map_err(|e| FileStorageError::FailedToLock(path.to_string_lossy().into_owned(), e))?;
58+
59+
// First try to lock the file without block. If we can't immediately get the lock we block and issue a debug message.
60+
if !lock
61+
.try_lock_with_pid()
62+
.map_err(|e| FileStorageError::FailedToLock(path.to_string_lossy().into_owned(), e))?
63+
{
64+
tracing::debug!("waiting for lock on {}", path.to_string_lossy());
65+
lock.lock_with_pid()
4766
.map_err(|e| FileStorageError::FailedToLock(path.to_string_lossy().into_owned(), e))?;
67+
}
4868

49-
// First try to lock the file without block. If we can't immediately get the lock we block and issue a debug message.
50-
if !lock
51-
.try_lock_with_pid()
52-
.map_err(|e| FileStorageError::FailedToLock(path.to_string_lossy().into_owned(), e))?
53-
{
54-
tracing::debug!("waiting for lock on {}", path.to_string_lossy());
55-
lock.lock_with_pid().map_err(|e| {
56-
FileStorageError::FailedToLock(path.to_string_lossy().into_owned(), e)
57-
})?;
58-
}
69+
Ok(Some(lock))
70+
}
5971

60-
Ok(lock)
72+
impl FileStorageCache {
73+
pub fn from_path(path: &Path) -> Result<Self, FileStorageError> {
74+
let file_exists = path.exists();
75+
let cache = if file_exists {
76+
lock_file_storage(path, false)?;
77+
let file = std::fs::File::open(path)?;
78+
let reader = std::io::BufReader::new(file);
79+
serde_json::from_reader(reader)?
80+
} else {
81+
BTreeMap::new()
82+
};
83+
84+
Ok(Self { cache, file_exists })
85+
}
86+
}
87+
88+
impl FileStorage {
89+
/// Create a new file storage with the given path
90+
pub fn new(path: PathBuf) -> Result<Self, FileStorageError> {
91+
// read the JSON file if it exists, and store it in the cache
92+
let cache = Arc::new(Mutex::new(FileStorageCache::from_path(&path)?));
93+
94+
Ok(Self { path, cache })
6195
}
6296

6397
/// Read the JSON file and deserialize it into a `BTreeMap`, or return an empty `BTreeMap` if the
6498
/// file does not exist
6599
fn read_json(&self) -> Result<BTreeMap<String, Authentication>, FileStorageError> {
66-
if !self.path.exists() {
67-
static WARN_GUARD: Lazy<Mutex<HashSet<PathBuf>>> =
68-
Lazy::new(|| Mutex::new(HashSet::new()));
69-
let mut guard = WARN_GUARD.lock().unwrap();
70-
if !guard.insert(self.path.clone()) {
71-
tracing::warn!(
72-
"Can't find path for file storage on {}",
73-
self.path.to_string_lossy()
74-
);
75-
}
76-
return Ok(BTreeMap::new());
77-
}
78-
let file = std::fs::File::open(&self.path)?;
79-
let reader = std::io::BufReader::new(file);
80-
let dict = serde_json::from_reader(reader)?;
81-
Ok(dict)
100+
let new_cache = FileStorageCache::from_path(&self.path)?;
101+
let mut cache = self.cache.lock().unwrap();
102+
cache.cache = new_cache.cache;
103+
cache.file_exists = new_cache.file_exists;
104+
105+
Ok(cache.cache.clone())
82106
}
83107

84108
/// Serialize the given `BTreeMap` and write it to the JSON file
85109
fn write_json(&self, dict: &BTreeMap<String, Authentication>) -> Result<(), FileStorageError> {
110+
let _lock = lock_file_storage(&self.path, true)?;
111+
86112
let file = std::fs::File::create(&self.path)?;
87113
let writer = std::io::BufWriter::new(file);
88114
serde_json::to_writer(writer, dict)?;
115+
116+
// Store the new data in the cache
117+
let mut cache = self.cache.lock().unwrap();
118+
cache.cache = dict.clone();
119+
cache.file_exists = true;
120+
89121
Ok(())
90122
}
91123
}
92124

93125
impl StorageBackend for FileStorage {
94126
fn store(&self, host: &str, authentication: &crate::Authentication) -> Result<()> {
95-
let _lock = self.lock()?;
96127
let mut dict = self.read_json()?;
97128
dict.insert(host.to_string(), authentication.clone());
98129
Ok(self.write_json(&dict)?)
99130
}
100131

101132
fn get(&self, host: &str) -> Result<Option<crate::Authentication>> {
102-
let _lock = self.lock()?;
103-
let dict = self.read_json()?;
104-
Ok(dict.get(host).cloned())
133+
let cache = self.cache.lock().unwrap();
134+
Ok(cache.cache.get(host).cloned())
105135
}
106136

107137
fn delete(&self, host: &str) -> Result<()> {
108-
let _lock = self.lock()?;
109138
let mut dict = self.read_json()?;
110-
dict.remove(host);
111-
Ok(self.write_json(&dict)?)
139+
if dict.remove(host).is_some() {
140+
Ok(self.write_json(&dict)?)
141+
} else {
142+
Ok(())
143+
}
112144
}
113145
}
114146

@@ -117,7 +149,13 @@ impl Default for FileStorage {
117149
let mut path = dirs::home_dir().unwrap();
118150
path.push(".rattler");
119151
path.push("credentials.json");
120-
Self { path }
152+
Self::new(path.clone()).unwrap_or(Self {
153+
path,
154+
cache: Arc::new(Mutex::new(FileStorageCache {
155+
cache: BTreeMap::new(),
156+
file_exists: false,
157+
})),
158+
})
121159
}
122160
}
123161

@@ -133,7 +171,7 @@ mod tests {
133171
let file = tempdir().unwrap();
134172
let path = file.path().join("test.json");
135173

136-
let storage = FileStorage::new(path.clone());
174+
let storage = FileStorage::new(path.clone()).unwrap();
137175

138176
assert_eq!(storage.get("test").unwrap(), None);
139177

@@ -168,6 +206,7 @@ mod tests {
168206

169207
let mut file = std::fs::File::create(&path).unwrap();
170208
file.write_all(b"invalid json").unwrap();
171-
assert!(storage.get("test").is_err());
209+
210+
assert!(FileStorage::new(path.clone()).is_err());
172211
}
173212
}

0 commit comments

Comments
 (0)