blob: c923d284b4ea8a2aa07fe5b67df24785785fa235 [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 {
crate::constants::{self, CONFIG_CACHE_TIMEOUT},
crate::environment::Environment,
crate::file_backed_config::FileBacked as Config,
crate::get_config_base_path,
crate::runtime::populate_runtime_config,
anyhow::{anyhow, Context, Result},
async_std::sync::{Arc, RwLock},
serde_json::Value,
std::{
collections::HashMap,
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Mutex,
},
time::Instant,
},
};
#[cfg(test)]
use tempfile::NamedTempFile;
struct CacheItem {
created: Instant,
config: Arc<RwLock<Config>>,
}
type Cache = RwLock<HashMap<Option<String>, CacheItem>>;
lazy_static::lazy_static! {
static ref INIT: AtomicBool = AtomicBool::new(false);
static ref ENV_FILE: Mutex<Option<String>> = Mutex::new(None);
static ref RUNTIME: Mutex<Option<Value>> = Mutex::new(None);
static ref CACHE: Cache = RwLock::new(HashMap::new());
}
#[cfg(not(test))]
pub fn env_file() -> Option<String> {
ENV_FILE.lock().unwrap().as_ref().map(|v| format!("{}", v))
}
#[cfg(test)]
pub fn env_file() -> Option<String> {
lazy_static::lazy_static! {
static ref FILE: NamedTempFile = NamedTempFile::new().expect("tmp access failed");
}
Environment::init_env_file(&FILE.path().to_path_buf()).expect("initializing env file");
FILE.path().to_str().map(|s| s.to_string())
}
pub fn init_config(
runtime: &Option<String>,
runtime_overrides: &Option<String>,
env_override: &Option<String>,
) -> Result<()> {
// If it's already been initialize, just fail silently. This will allow a setup method to be
// called by unit tests over and over again without issue.
if INIT.compare_and_swap(false, true, Ordering::Release) {
Ok(())
} else {
init_config_impl(runtime, runtime_overrides, env_override)
}
}
fn init_config_impl(
runtime: &Option<String>,
runtime_overrides: &Option<String>,
env_override: &Option<String>,
) -> Result<()> {
let mut populated_runtime = Value::Null;
vec![runtime, runtime_overrides].iter().try_for_each(|r| {
if let Some(v) = populate_runtime_config(r)? {
crate::api::value::merge(&mut populated_runtime, &v)
};
Result::<()>::Ok(())
})?;
match populated_runtime {
Value::Null => {}
_ => {
RUNTIME.lock().unwrap().replace(populated_runtime);
}
}
let env_path = if let Some(f) = env_override {
PathBuf::from(f)
} else {
let mut path = get_config_base_path().context("locating environment file directory")?;
path.push(constants::ENV_FILE);
path
};
if !env_path.is_file() {
log::debug!("initializing environment {}", env_path.display());
Environment::init_env_file(&env_path)?;
}
env_path.to_str().map(String::from).context("getting environment file").and_then(|e| {
let _ = ENV_FILE.lock().unwrap().replace(e);
Ok(())
})
}
fn is_cache_item_expired(item: &CacheItem, now: Instant) -> bool {
now.checked_duration_since(item.created).map_or(false, |t| t > CONFIG_CACHE_TIMEOUT)
}
async fn read_cache(
build_dir: &Option<String>,
now: Instant,
cache: &Cache,
) -> Option<Arc<RwLock<Config>>> {
let read_guard = cache.read().await;
(*read_guard)
.get(build_dir)
.filter(|item| !is_cache_item_expired(item, now))
.map(|item| item.config.clone())
}
pub(crate) async fn load_config(build_dir: &Option<String>) -> Result<Arc<RwLock<Config>>> {
load_config_with_instant(build_dir, Instant::now(), &CACHE).await
}
async fn load_config_with_instant(
build_dir: &Option<String>,
now: Instant,
cache: &Cache,
) -> Result<Arc<RwLock<Config>>> {
let cache_hit = read_cache(build_dir, now, cache).await;
match cache_hit {
Some(h) => Ok(h),
None => {
{
let mut write_guard = cache.write().await;
// Check again if in the time it took to get the lock this config was written
let write = (*write_guard)
.get(build_dir)
.map_or(true, |item| is_cache_item_expired(item, now));
if write {
let runtime = RUNTIME.lock().unwrap().as_ref().map(|v| v.clone());
(*write_guard).insert(
build_dir.as_ref().cloned(),
CacheItem {
created: now,
config: Arc::new(RwLock::new(Config::new(
&Environment::try_load(ENV_FILE.lock().unwrap().as_ref()),
build_dir,
runtime,
)?)),
},
);
}
}
read_cache(build_dir, now, cache)
.await
.ok_or(anyhow!("reading config value from cache after initialization"))
}
}
}
////////////////////////////////////////////////////////////////////////////////
// tests
#[cfg(test)]
mod test {
use {super::*, futures::future::join_all, std::time::Duration};
async fn load(now: Instant, key: &Option<String>, cache: &Cache) {
let tests = 25;
let mut futures = Vec::new();
for _x in 0..tests {
futures.push(load_config_with_instant(key, now, cache));
}
let result = join_all(futures).await;
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<String>,
cache: &Cache,
) {
{
let read_guard = cache.read().await;
assert_eq!(expected_len_before, (*read_guard).len());
}
load(now, key, cache).await;
{
let read_guard = cache.read().await;
assert_eq!(expected_len_after, (*read_guard).len());
}
}
fn setup_build_dirs(tests: usize) -> Vec<Option<String>> {
let mut build_dirs = Vec::new();
build_dirs.push(None);
for x in 0..tests - 1 {
build_dirs.push(Some(format!("test {}", x)));
}
build_dirs
}
fn setup(tests: usize) -> (Instant, Vec<Option<String>>, Cache) {
(Instant::now(), setup_build_dirs(tests), RwLock::new(HashMap::new()))
}
#[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], &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, &cache));
let result = join_all(futures).await;
assert_eq!(tests, result.len());
let read_guard = cache.read().await;
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], &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], &cache).await;
load_and_test(after_timeout, 1, 1, &build_dirs[0], &cache).await;
}
#[test]
fn test_expiration_check_does_not_panic() -> Result<()> {
let tests = 1;
let (now, build_dirs, _cache) = setup(tests);
let later = now.checked_add(Duration::from_millis(1)).expect("timeout should not overflow");
let item = CacheItem {
created: later,
config: Arc::new(RwLock::new(Config::new(
&Environment::try_load(None),
&build_dirs[0],
None,
)?)),
};
assert!(!is_cache_item_expired(&item, now));
Ok(())
}
}