Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manually invoked currency refresh #181

Merged
merged 7 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
#[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 @@
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 @@
}
}

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)

Check warning on line 284 in cli/src/config.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/config.rs#L283-L284

Added lines #L283 - L284 were not covered by tests
} else {
None

Check warning on line 286 in cli/src/config.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/config.rs#L286

Added line #L286 was not covered by tests
};
let file = cached("currency.json", &config.endpoint, duration, config.timeout)?;

Check warning on line 288 in cli/src/config.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/config.rs#L288

Added line #L288 was not covered by tests
let contents = file_to_string(file)?;
serde_json::from_str(&contents).wrap_err("Invalid JSON")
}
Expand Down Expand Up @@ -341,18 +363,19 @@
Ok(ctx)
}

fn read_if_current(file: File, expiration: Duration) -> Result<File> {
fn read_if_current(file: File, expiration: Option<Duration>) -> Result<File> {

Check warning on line 366 in cli/src/config.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/config.rs#L366

Added line #L366 was not covered by tests
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"));
}

Check warning on line 376 in cli/src/config.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/config.rs#L373-L376

Added lines #L373 - L376 were not covered by tests
}
Ok(file)

Check warning on line 378 in cli/src/config.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/config.rs#L378

Added line #L378 was not covered by tests
}

fn download_to_file(path: &Path, url: &str, timeout: Duration) -> Result<File> {
Expand Down Expand Up @@ -409,7 +432,12 @@
.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> {

Check warning on line 440 in cli/src/config.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/config.rs#L435-L440

Added lines #L435 - L440 were not covered by tests
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 @@
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
Loading