Skip to content

Commit

Permalink
Manually invoked currency refresh (#181)
Browse files Browse the repository at this point in the history
Adds `currency.fetch_on_startup` to the config, which when set to false
behaves as if the `cache_duration` was infinite. This means rink will
fetch the file once, and then keep reusing that file indefinitely.

Adds a `--fetch-currency` CLI argument, which will make rink download
the latest version of the currency data and then exit. This can be put
into a cron job and used together with the config option so that rink
will never block on a web request at startup, without giving up currency
units. Requires manual setup though.
  • Loading branch information
tiffany352 authored Jun 2, 2024
1 parent 6e674f0 commit 900b075
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 13 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
**/.rpt2_cache
**/build
/web/data
# created by unit tests
/cli/currency.json
32 changes: 32 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ path = "../sandbox"

[dev-dependencies]
similar-asserts = "1.1.0"
tiny_http = "0.12"
once_cell = "1"

[package.metadata.wasm-pack.profile.profiling]
wasm-opt = ['-g', '-O']
205 changes: 192 additions & 13 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ pub struct Rink {
#[derive(Serialize, Deserialize, Clone)]
#[serde(default, deny_unknown_fields)]
pub struct Currency {
/// Set to false to disable currency fetching entirely.
/// Set to false to disable currency loading entirely.
pub enabled: bool,
/// Set to false to only reuse the existing cached currency data.
pub fetch_on_startup: bool,
/// Which web endpoint should be used to download currency data?
pub endpoint: String,
/// How long to cache for.
Expand Down Expand Up @@ -176,6 +178,7 @@ impl Default for Currency {
fn default() -> Self {
Currency {
enabled: true,
fetch_on_startup: true,
endpoint: "https://rinkcalc.app/data/currency.json".to_owned(),
cache_duration: Duration::from_secs(60 * 60), // 1 hour
timeout: Duration::from_secs(2),
Expand Down Expand Up @@ -257,13 +260,32 @@ fn read_from_search_path(
}
}

pub(crate) fn force_refresh_currency(config: &Currency) -> Result<String> {
println!("Fetching...");
let start = std::time::Instant::now();
let mut path = dirs::cache_dir().ok_or_else(|| eyre!("Could not find cache directory"))?;
path.push("rink");
path.push("currency.json");
let file = download_to_file(&path, &config.endpoint, config.timeout)
.wrap_err("Fetching currency data failed")?;
let delta = std::time::Instant::now() - start;
let metadata = file
.metadata()
.wrap_err("Fetched currency file, but failed to read file metadata")?;
let len = metadata.len();
Ok(format!(
"Fetched {len} byte currency file after {}ms",
delta.as_millis()
))
}

fn load_live_currency(config: &Currency) -> Result<ast::Defs> {
let file = cached(
"currency.json",
&config.endpoint,
config.cache_duration,
config.timeout,
)?;
let duration = if config.fetch_on_startup {
Some(config.cache_duration)
} else {
None
};
let file = cached("currency.json", &config.endpoint, duration, config.timeout)?;
let contents = file_to_string(file)?;
serde_json::from_str(&contents).wrap_err("Invalid JSON")
}
Expand Down Expand Up @@ -341,18 +363,19 @@ pub fn load(config: &Config) -> Result<Context> {
Ok(ctx)
}

fn read_if_current(file: File, expiration: Duration) -> Result<File> {
fn read_if_current(file: File, expiration: Option<Duration>) -> Result<File> {
use std::time::SystemTime;

let stats = file.metadata()?;
let mtime = stats.modified()?;
let now = SystemTime::now();
let elapsed = now.duration_since(mtime)?;
if elapsed > expiration {
Err(eyre!("File is out of date"))
} else {
Ok(file)
if let Some(expiration) = expiration {
if elapsed > expiration {
return Err(eyre!("File is out of date"));
}
}
Ok(file)
}

fn download_to_file(path: &Path, url: &str, timeout: Duration) -> Result<File> {
Expand Down Expand Up @@ -409,7 +432,12 @@ fn download_to_file(path: &Path, url: &str, timeout: Duration) -> Result<File> {
.wrap_err("Failed to write to cache dir")
}

fn cached(filename: &str, url: &str, expiration: Duration, timeout: Duration) -> Result<File> {
fn cached(
filename: &str,
url: &str,
expiration: Option<Duration>,
timeout: Duration,
) -> Result<File> {
let mut path = dirs::cache_dir().ok_or_else(|| eyre!("Could not find cache directory"))?;
path.push("rink");
path.push(filename);
Expand Down Expand Up @@ -443,3 +471,154 @@ fn cached(filename: &str, url: &str, expiration: Duration, timeout: Duration) ->
Err(err).wrap_err_with(|| format!("Failed to fetch {}", url))
}
}

#[cfg(test)]
mod tests {
use std::{
io::Read,
path::PathBuf,
sync::{Arc, Mutex},
time::Duration,
};

use once_cell::sync::Lazy;
use tiny_http::{Response, Server, StatusCode};

static SERVER: Lazy<Mutex<Arc<Server>>> = Lazy::new(|| {
Mutex::new(Arc::new(
Server::http("127.0.0.1:3090").expect("port 3090 is needed to do http tests"),
))
});

#[test]
fn test_download_timeout() {
let server = SERVER.lock().unwrap();
let server2 = server.clone();

let thread_handle = std::thread::spawn(move || {
let request = server2.recv().expect("the request should not fail");
assert_eq!(request.url(), "/data/currency.json");
std::thread::sleep(Duration::from_millis(100));
});
let result = super::download_to_file(
&PathBuf::from("currency.json"),
"http://127.0.0.1:3090/data/currency.json",
Duration::from_millis(5),
);
let result = result.expect_err("this should always fail");
assert_eq!(result.to_string(), "[28] Timeout was reached (Operation timed out after 5 milliseconds with 0 bytes received)");
thread_handle.join().unwrap();
drop(server);
}

#[test]
fn test_download_404() {
let server = SERVER.lock().unwrap();
let server2 = server.clone();

let thread_handle = std::thread::spawn(move || {
let request = server2.recv().expect("the request should not fail");
assert_eq!(request.url(), "/data/currency.json");
let mut data = b"404 not found".to_owned();
let cursor = std::io::Cursor::new(&mut data);
request
.respond(Response::new(StatusCode(404), vec![], cursor, None, None))
.expect("the response should go through");
});
let result = super::download_to_file(
&PathBuf::from("currency.json"),
"http://127.0.0.1:3090/data/currency.json",
Duration::from_millis(2000),
);
let result = result.expect_err("this should always fail");
assert_eq!(
result.to_string(),
"Received status 404 while downloading http://127.0.0.1:3090/data/currency.json"
);
thread_handle.join().unwrap();
drop(server);
}

#[test]
fn test_download_success() {
let server = SERVER.lock().unwrap();
let server2 = server.clone();

let thread_handle = std::thread::spawn(move || {
let request = server2.recv().expect("the request should not fail");
assert_eq!(request.url(), "/data/currency.json");
let mut data = b"{}".to_owned();
let cursor = std::io::Cursor::new(&mut data);
request
.respond(Response::new(StatusCode(200), vec![], cursor, None, None))
.expect("the response should go through");
});
let result = super::download_to_file(
&PathBuf::from("currency.json"),
"http://127.0.0.1:3090/data/currency.json",
Duration::from_millis(2000),
);
let mut result = result.expect("this should succeed");
let mut string = String::new();
result
.read_to_string(&mut string)
.expect("the file should exist");
assert_eq!(string, "{}");
thread_handle.join().unwrap();
drop(server);
}

#[test]
fn test_force_refresh_success() {
let config = super::Currency {
enabled: true,
fetch_on_startup: false,
endpoint: "http://127.0.0.1:3090/data/currency.json".to_owned(),
cache_duration: Duration::ZERO,
timeout: Duration::from_millis(2000),
};

let server = SERVER.lock().unwrap();
let server2 = server.clone();

let thread_handle = std::thread::spawn(move || {
let request = server2.recv().expect("the request should not fail");
assert_eq!(request.url(), "/data/currency.json");
let mut data = b"{}".to_owned();
let cursor = std::io::Cursor::new(&mut data);
request
.respond(Response::new(StatusCode(200), vec![], cursor, None, None))
.expect("the response should go through");
});
let result = super::force_refresh_currency(&config);
let result = result.expect("this should succeed");
assert!(result.starts_with("Fetched 2 byte currency file after "));
thread_handle.join().unwrap();
drop(server);
}

#[test]
fn test_force_refresh_timeout() {
let config = super::Currency {
enabled: true,
fetch_on_startup: false,
endpoint: "http://127.0.0.1:3090/data/currency.json".to_owned(),
cache_duration: Duration::ZERO,
timeout: Duration::from_millis(5),
};

let server = SERVER.lock().unwrap();
let server2 = server.clone();

let thread_handle = std::thread::spawn(move || {
let request = server2.recv().expect("the request should not fail");
assert_eq!(request.url(), "/data/currency.json");
std::thread::sleep(Duration::from_millis(100));
});
let result = super::force_refresh_currency(&config);
let result = result.expect_err("this should timeout");
assert_eq!(result.to_string(), "Fetching currency data failed");
thread_handle.join().unwrap();
drop(server);
}
}
17 changes: 17 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ async fn main() -> Result<()> {
.help("Prints a path to the config file, then exits")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("fetch-currency")
.long("fetch-currency")
.help("Fetches latest version of currency data, then exits")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("dump")
.long("dump")
Expand Down Expand Up @@ -78,6 +84,17 @@ async fn main() -> Result<()> {
return Ok(());
}

if matches.get_flag("fetch-currency") {
let result = config::force_refresh_currency(&config.currency);
match result {
Ok(msg) => {
println!("{msg}");
return Ok(());
}
Err(err) => return Err(err),
}
}

if matches.get_flag("config-path") {
println!("{}", config::config_path("config.toml").unwrap().display());
Ok(())
Expand Down
8 changes: 8 additions & 0 deletions docs/rink.1.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ Options
**--config-path**::
Prints a path to the config file, then exits.

**--fetch-currency**:
Fetches the latest version of the currency data, then exits. Can be
used as part of a cron job, possibly together with setting
`currency.fetch_on_startup` to false.

**-f**::
**--file** <__file__>::
Reads expressions from a file, one per line, printing them to stdout
Expand Down Expand Up @@ -58,6 +63,9 @@ Rink looks for a configuration file in
Rink relies on some data files, which are found using a search path.
See rink-defs(5) and rink-dates(5).

Downloaded currency data is saved in
`$XDG_CACHE_DIR/rink/currency.json`.

Bugs
----

Expand Down
Loading

0 comments on commit 900b075

Please sign in to comment.