Skip to content

Commit df428de

Browse files
authored
Merge pull request #39 from psnairne/pn/global-rate-limiting
Pn/global rate limiting
2 parents cc5d746 + a4c7e47 commit df428de

File tree

13 files changed

+163
-143
lines changed

13 files changed

+163
-143
lines changed

Cargo.toml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
[package]
2-
authors = ["Peter Robinson, Rouven Router, Patrick Nairne"]
3-
description = "A Rust library for shared tools between different Phenopacket extraction programs."
2+
authors = ["Patrick Nairne, Rouven Reuter"]
3+
description = "A Rust library for retrieving data from the VariantValidator and HGNC APIs for Phenopackets."
44
edition = "2024"
55
homepage = "https://robinsongroup.github.io/"
6-
license = "MIT"
7-
name = "pivot"
8-
version = "0.1.4"
6+
license-file = "LICENSE"
7+
name = "pivotal"
8+
version = "0.1.6"
9+
keywords = ["variant", "validator", "hgnc", "hgvs", "phenopacket"]
10+
readme = "README.md"
11+
repository = "https://github.com/psnairne/PIVOT"
912

1013

1114
[dependencies]
1215
phenopackets = { version = "0.2.2-post1", features = ["serde"] }
13-
rstest = "0.26.1"
1416
serde = "1.0.228"
1517
thiserror = "2.0.17"
1618
ratelimit = "0.10.0"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
## PIVOT
2-
A Rust library for getting data from VariantValidator.
2+
A Rust library for getting data from VariantValidator and HGNC.
33

44
## License
55
This project is licensed under MIT.

src/caching/redb_cacher.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,16 @@ impl<T: Cacheable> Default for RedbCacher<T> {
7979
fn default() -> Self {
8080
let pkg_name = env!("CARGO_PKG_NAME");
8181

82-
let pivot_cache_dir = ProjectDirs::from("", "", pkg_name)
82+
let pivotal = ProjectDirs::from("", "", pkg_name)
8383
.map(|project_dir| project_dir.cache_dir().to_path_buf())
8484
.or_else(|| home_dir().map(|home| home.join(pkg_name)))
8585
.unwrap_or_else(|| panic!("Could not find cache directory or home directory."));
8686

87-
if !pivot_cache_dir.exists() {
88-
fs::create_dir_all(&pivot_cache_dir)
89-
.expect("Failed to create default cache directory.");
87+
if !pivotal.exists() {
88+
fs::create_dir_all(&pivotal).expect("Failed to create default cache directory.");
9089
}
9190

92-
RedbCacher::new(pivot_cache_dir.join(type_name::<T>()))
91+
RedbCacher::new(pivotal.join(type_name::<T>()))
9392
}
9493
}
9594

src/hgnc/cached_hgnc_client.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ use crate::hgnc::json_schema::GeneDoc;
66
use crate::hgnc::traits::HGNCData;
77
use std::fmt::{Debug, Formatter};
88
use std::path::PathBuf;
9+
use std::sync::{Mutex, MutexGuard, OnceLock};
10+
11+
static HGNC_CACHE_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
12+
13+
fn hgnc_cache_mutex() -> &'static Mutex<()> {
14+
HGNC_CACHE_LOCK.get_or_init(|| Mutex::new(()))
15+
}
16+
17+
fn lock_mutex(mutex: &'_ Mutex<()>) -> Result<MutexGuard<'_, ()>, HGNCError> {
18+
mutex
19+
.lock()
20+
.map_err(|e| HGNCError::MutexError(e.to_string()))
21+
}
922

1023
pub struct CachedHGNCClient {
1124
cacher: RedbCacher<GeneDoc>,
@@ -14,13 +27,22 @@ pub struct CachedHGNCClient {
1427

1528
impl HGNCData for CachedHGNCClient {
1629
fn request_gene_data(&self, query: GeneQuery) -> Result<GeneDoc, HGNCError> {
17-
let cache = self.cacher.open_cache()?;
18-
if let Some(gene_doc) = self.cacher.find_cache_entry(query.inner(), &cache) {
19-
return Ok(gene_doc);
30+
{
31+
let _guard = lock_mutex(hgnc_cache_mutex())?;
32+
let cache = self.cacher.open_cache()?;
33+
if let Some(gene_doc) = self.cacher.find_cache_entry(query.inner(), &cache) {
34+
return Ok(gene_doc);
35+
}
2036
}
2137

2238
let doc = self.hgnc_client.request_gene_data(query)?;
23-
self.cacher.cache_object(doc.clone(), &cache)?;
39+
40+
{
41+
let _guard = lock_mutex(hgnc_cache_mutex())?;
42+
let cache = self.cacher.open_cache()?;
43+
self.cacher.cache_object(doc.clone(), &cache)?;
44+
}
45+
2446
Ok(doc)
2547
}
2648
}

src/hgnc/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,8 @@ pub enum HGNCError {
3030
CacheTable(#[from] TableError),
3131
#[error(transparent)]
3232
Request(#[from] reqwest::Error),
33+
#[error("Something went wrong when using Mutex: {0}")]
34+
MutexError(String),
35+
#[error("HgncAPI returned an error on {attempts} attempts to retrieve data about gene {gene}")]
36+
HgncAPI { gene: String, attempts: usize },
3337
}

src/hgnc/hgnc_client.rs

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,60 @@ use crate::hgnc::traits::HGNCData;
55
use ratelimit::Ratelimiter;
66
use reqwest::blocking::Client;
77
use std::fmt::{Debug, Formatter};
8+
use std::sync::OnceLock;
89
use std::thread::sleep;
910
use std::time::Duration;
1011

12+
static HGNC_RATE_LIMITER: OnceLock<Ratelimiter> = OnceLock::new();
13+
14+
fn hgnc_rate_limiter() -> &'static Ratelimiter {
15+
HGNC_RATE_LIMITER.get_or_init(|| {
16+
Ratelimiter::builder(10, Duration::from_millis(1100))
17+
.max_tokens(10)
18+
.build()
19+
.expect("Building rate limiter failed")
20+
})
21+
}
22+
1123
pub struct HGNCClient {
12-
rate_limiter: Ratelimiter,
24+
attempts: usize,
1325
api_url: String,
1426
client: Client,
1527
}
1628

1729
impl HGNCClient {
18-
pub fn new(rate_limiter: Ratelimiter, api_url: String) -> Self {
30+
pub fn new(attempts: usize, api_url: String) -> Self {
1931
HGNCClient {
20-
rate_limiter,
32+
attempts,
2133
api_url,
2234
client: Client::new(),
2335
}
2436
}
2537

26-
fn fetch_request(&self, url: String) -> Result<Vec<GeneDoc>, HGNCError> {
27-
if let Err(duration) = self.rate_limiter.try_wait() {
28-
sleep(duration);
38+
fn fetch_request(&self, url: &str, query: &GeneQuery) -> Result<Vec<GeneDoc>, HGNCError> {
39+
for _ in 0..self.attempts {
40+
if let Err(duration) = hgnc_rate_limiter().try_wait() {
41+
sleep(duration);
42+
}
43+
let response = self
44+
.client
45+
.get(url)
46+
.header("User-Agent", "PIVOT")
47+
.header("Accept", "application/json")
48+
.send();
49+
50+
if let Ok(response) = response
51+
&& response.status().is_success()
52+
{
53+
let gene_response = response.json::<GeneResponse>()?;
54+
return Ok(gene_response.response.docs);
55+
}
2956
}
30-
let response = self
31-
.client
32-
.get(url.clone())
33-
.header("User-Agent", "PIVOT")
34-
.header("Accept", "application/json")
35-
.send()?;
3657

37-
let gene_response = response.json::<GeneResponse>()?;
38-
39-
Ok(gene_response.response.docs)
58+
Err(HGNCError::HgncAPI {
59+
gene: query.inner().to_string(),
60+
attempts: self.attempts,
61+
})
4062
}
4163
}
4264

@@ -46,7 +68,7 @@ impl HGNCData for HGNCClient {
4668
GeneQuery::Symbol(symbol) => format!("{}fetch/symbol/{}", self.api_url, symbol),
4769
GeneQuery::HgncId(id) => format!("{}fetch/hgnc_id/{}", self.api_url, id),
4870
};
49-
let docs = self.fetch_request(fetch_url)?;
71+
let docs = self.fetch_request(&fetch_url, &query)?;
5072

5173
if docs.len() == 1 {
5274
Ok(docs.first().unwrap().clone())
@@ -62,12 +84,7 @@ impl HGNCData for HGNCClient {
6284

6385
impl Default for HGNCClient {
6486
fn default() -> Self {
65-
let rate_limiter = Ratelimiter::builder(10, Duration::from_secs(1))
66-
.max_tokens(10)
67-
.build()
68-
.expect("Building rate limiter failed");
69-
70-
HGNCClient::new(rate_limiter, "https://rest.genenames.org/".to_string())
87+
HGNCClient::new(3, "https://rest.genenames.org/".to_string())
7188
}
7289
}
7390

src/hgnc/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
//! ## HGNCClient
4444
//!
4545
//! ```rust
46-
//! use pivot::hgnc::{HGNCClient, HGNCData, GeneQuery};
46+
//! use pivotal::hgnc::{HGNCClient, HGNCData, GeneQuery};
4747
//!
4848
//! let client = HGNCClient::default();
4949
//! let gene_symbol = client.request_gene_symbol(GeneQuery::from("HGNC:13089")).unwrap();
@@ -54,7 +54,7 @@
5454
//! ## CachedHGNCClient
5555
//!
5656
//! ```rust
57-
//! use pivot::hgnc::{HGNCClient, HGNCData, GeneQuery, CachedHGNCClient};
57+
//! use pivotal::hgnc::{HGNCClient, HGNCData, GeneQuery, CachedHGNCClient};
5858
//!
5959
//! let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
6060
//! let cache_file_path = temp_dir.path().join("cache.hgnc");

src/hgvs/cached_hgvs_client.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ use crate::hgvs::hgvs_client::HGVSClient;
77
use crate::hgvs::hgvs_variant::HgvsVariant;
88
use crate::hgvs::traits::HGVSData;
99
use std::path::PathBuf;
10+
use std::sync::{Mutex, MutexGuard, OnceLock};
11+
12+
static HGVS_CACHE_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
13+
14+
fn hgvs_cache_mutex() -> &'static Mutex<()> {
15+
HGVS_CACHE_LOCK.get_or_init(|| Mutex::new(()))
16+
}
17+
18+
fn lock_mutex(mutex: &'_ Mutex<()>) -> Result<MutexGuard<'_, ()>, HGVSError> {
19+
mutex
20+
.lock()
21+
.map_err(|e| HGVSError::MutexError(e.to_string()))
22+
}
1023

1124
#[derive(Debug)]
1225
pub struct CachedHGVSClient {
@@ -37,16 +50,27 @@ impl CachedHGVSClient {
3750

3851
impl HGVSData for CachedHGVSClient {
3952
fn request_and_validate_hgvs(&self, unvalidated_hgvs: &str) -> Result<HgvsVariant, HGVSError> {
40-
let cache = self.cacher.open_cache()?;
41-
if let Some(hgvs_variant) = self.cacher.find_cache_entry(unvalidated_hgvs, &cache) {
42-
return Ok(hgvs_variant);
53+
{
54+
let _guard = lock_mutex(hgvs_cache_mutex())?;
55+
56+
let cache = self.cacher.open_cache()?;
57+
if let Some(hgvs_variant) = self.cacher.find_cache_entry(unvalidated_hgvs, &cache) {
58+
return Ok(hgvs_variant);
59+
}
4360
}
4461

4562
let hgvs_variant = self
4663
.hgvs_client
4764
.request_and_validate_hgvs(unvalidated_hgvs)?;
48-
self.cacher.cache_object(hgvs_variant.clone(), &cache)?;
49-
Ok(hgvs_variant.clone())
65+
66+
{
67+
let _guard = lock_mutex(hgvs_cache_mutex())?;
68+
69+
let cache = self.cacher.open_cache()?;
70+
self.cacher.cache_object(hgvs_variant.clone(), &cache)?;
71+
}
72+
73+
Ok(hgvs_variant)
5074
}
5175
}
5276

src/hgvs/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ pub enum HGVSError {
6565
VariantValidatorAPI { hgvs: String, attempts: usize },
6666
#[error("VariantValidator response for {hgvs} had an unexpected format: {format_issue}")]
6767
VariantValidatorResponseUnexpectedFormat { hgvs: String, format_issue: String },
68-
#[error("VariantValidator fetch request for {hgvs} failed. Error: {err}.")]
69-
FetchRequest { hgvs: String, err: String },
7068
#[error(transparent)]
7169
CacheDatabase(#[from] DatabaseError),
7270
#[error(transparent)]
@@ -79,4 +77,6 @@ pub enum HGVSError {
7977
CacheStorage(#[from] StorageError),
8078
#[error(transparent)]
8179
CacherError(#[from] CacherError),
80+
#[error("Something went wrong when using Mutex: {0}")]
81+
MutexError(String),
8282
}

0 commit comments

Comments
 (0)