blob: e75739e2db292607ae06a86cb07a56a204e605f3 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{anyhow, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
// Timeout for the config cache.
pub const CONFIG_CACHE_TIMEOUT: Duration = Duration::from_secs(3);
#[derive(Debug)]
struct CacheItem<T> {
created: Instant,
config: Arc<RwLock<T>>,
}
impl<T> CacheItem<T> {
fn is_cache_item_expired(&self, now: Instant) -> bool {
now.checked_duration_since(self.created).map_or(false, |t| t > CONFIG_CACHE_TIMEOUT)
}
}
#[derive(Debug)]
pub(crate) struct Cache<T>(RwLock<HashMap<Option<PathBuf>, CacheItem<T>>>);
impl<T> Default for Cache<T> {
fn default() -> Self {
Cache(RwLock::default())
}
}
/// Invalidate the cache. Call this if you do anything that might make a cached config go stale
/// in a critical way, like changing the environment.
pub(crate) async fn invalidate<T>(cache: &Cache<T>) {
cache.0.write().expect("config write guard").clear();
}
fn read_cache<T>(
guard: &impl std::ops::Deref<Target = HashMap<Option<PathBuf>, CacheItem<T>>>,
build_dir: Option<&Path>,
now: Instant,
) -> Option<Arc<RwLock<T>>> {
guard
.get(&build_dir.map(Path::to_owned)) // TODO(mgnb): get rid of this allocation when we can
.filter(|item| !item.is_cache_item_expired(now))
.map(|item| item.config.clone())
}
pub(crate) fn load_config<T>(
cache: &Cache<T>,
build_dir: Option<&Path>,
new_config: impl FnOnce() -> Result<T>,
) -> Result<Arc<RwLock<T>>> {
load_config_with_instant(build_dir, Instant::now(), cache, new_config)
}
fn load_config_with_instant<T>(
build_dir: Option<&Path>,
now: Instant,
cache: &Cache<T>,
new_config: impl FnOnce() -> Result<T>,
) -> Result<Arc<RwLock<T>>> {
let cache_hit = {
let guard = cache.0.read().map_err(|_| anyhow!("config read guard"))?;
//let build_dir = env.override_build_dir(build_dir);
read_cache(&guard, build_dir, now)
};
match cache_hit {
Some(h) => Ok(h),
None => {
let mut guard = cache.0.write().map_err(|_| anyhow!("config write guard"))?;
let config = Arc::new(RwLock::new(new_config()?));
guard.insert(
build_dir.map(Path::to_owned), // TODO(mgnb): get rid of this allocation when we can,
CacheItem { created: now, config: config.clone() },
);
Ok(config)
}
}
}
////////////////////////////////////////////////////////////////////////////////
// tests
#[cfg(test)]
mod test {
use super::*;
use futures::future::join_all;
async fn load(now: Instant, key: Option<&Path>, cache: &Cache<usize>) {
let tests = 25;
let mut result = Vec::new();
for x in 0..tests {
result.push(load_config_with_instant(key, now, cache, move || Ok(x)));
}
assert_eq!(tests, result.len());
result.iter().for_each(|x| {
assert!(x.is_ok());
});
}
async fn load_and_test(
now: Instant,
expected_len_before: usize,
expected_len_after: usize,
key: Option<&Path>,
cache: &Cache<usize>,
) {
{
let read_guard = cache.0.read().expect("config read guard");
assert_eq!(expected_len_before, (*read_guard).len());
}
load(now, key, cache).await;
{
let read_guard = cache.0.read().expect("config read guard");
assert_eq!(expected_len_after, (*read_guard).len());
}
}
fn setup_build_dirs(tests: usize) -> Vec<Option<PathBuf>> {
let mut build_dirs = Vec::new();
build_dirs.push(None);
for x in 0..tests - 1 {
build_dirs.push(Some(PathBuf::from(format!("test {}", x))));
}
build_dirs
}
fn setup(tests: usize) -> (Instant, Vec<Option<PathBuf>>, Cache<usize>) {
(Instant::now(), setup_build_dirs(tests), Cache::default())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_config_one_at_a_time() {
let tests = 10;
let (now, build_dirs, cache) = setup(tests);
for x in 0..tests {
load_and_test(now, x, x + 1, build_dirs[x].as_deref(), &cache).await;
}
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_config_many_at_a_time() {
let tests = 25;
let (now, build_dirs, cache) = setup(tests);
let futures = build_dirs.iter().map(|x| load(now, x.as_deref(), &cache));
let result = join_all(futures).await;
assert_eq!(tests, result.len());
let read_guard = cache.0.read().expect("config read guard");
assert_eq!(tests, (*read_guard).len());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_config_timeout() {
let tests = 1;
let (now, build_dirs, cache) = setup(tests);
load_and_test(now, 0, 1, build_dirs[0].as_deref(), &cache).await;
let timeout = now.checked_add(CONFIG_CACHE_TIMEOUT).expect("timeout should not overflow");
let after_timeout = timeout
.checked_add(Duration::from_millis(1))
.expect("after timeout should not overflow");
load_and_test(timeout, 1, 1, build_dirs[0].as_deref(), &cache).await;
load_and_test(after_timeout, 1, 1, build_dirs[0].as_deref(), &cache).await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_expiration_check_does_not_panic() -> Result<()> {
let now = Instant::now();
let later = now.checked_add(Duration::from_millis(1)).expect("timeout should not overflow");
let item = CacheItem { created: later, config: Arc::new(RwLock::new(1)) };
assert!(!item.is_cache_item_expired(now));
Ok(())
}
}