Initial work
This commit is contained in:
commit
d8206cd99b
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
/.idea
|
||||||
1809
Cargo.lock
generated
Normal file
1809
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "toggl-2"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.7.5"
|
||||||
|
chrono = "0.4.38"
|
||||||
|
governor = "0.6.3"
|
||||||
|
reqwest = "0.12.5"
|
||||||
|
reqwest-ratelimit = "0.2.0"
|
||||||
|
thiserror = "1.0.62"
|
||||||
|
tokio = { version = "1.38.0", features = ["full"] }
|
||||||
120
src/main.rs
Normal file
120
src/main.rs
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
use std::cmp::max;
|
||||||
|
use std::num::NonZero;
|
||||||
|
use chrono::Utc;
|
||||||
|
use governor::clock::DefaultClock;
|
||||||
|
use governor::state::{InMemoryState, NotKeyed};
|
||||||
|
use reqwest::{Client, Error, Method, Request, RequestBuilder, Response, StatusCode};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
enum TogglApiError {
|
||||||
|
#[error("Reqwest error: {0}")]
|
||||||
|
ReqwestError(#[from] reqwest::Error),
|
||||||
|
#[error("Retries exceeded")]
|
||||||
|
RetriesExceeded,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TogglApiRequest(
|
||||||
|
Request,
|
||||||
|
oneshot::Sender<Result<Response, TogglApiError>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
struct TogglApiWorker {
|
||||||
|
rate_limiter: governor::RateLimiter<NotKeyed, InMemoryState, DefaultClock>,
|
||||||
|
rx: tokio::sync::mpsc::Receiver<TogglApiRequest>,
|
||||||
|
client: Client,
|
||||||
|
max_retries: Option<u32>,
|
||||||
|
default_delay: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TogglApiWorker {
|
||||||
|
fn new() -> (Self, TogglApi) {
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel(100);
|
||||||
|
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let rate_limiter = governor::RateLimiter::direct(
|
||||||
|
governor::Quota::per_second(NonZero::new(1u32).unwrap())
|
||||||
|
);
|
||||||
|
|
||||||
|
(Self { rate_limiter, client, rx, max_retries: Some(3), default_delay: 3.14 }, TogglApi { tx })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(&mut self) {
|
||||||
|
loop {
|
||||||
|
// We limit ourselves to the recommended rate of 1 req/s
|
||||||
|
self.rate_limiter.until_ready().await;
|
||||||
|
|
||||||
|
let TogglApiRequest(request, tx) = self.rx.recv().await.unwrap();
|
||||||
|
let response = self.make_request(request).await;
|
||||||
|
tx.send(response).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn make_request(&self, request: RequestBuilder) -> Result<Response, TogglApiError> {
|
||||||
|
let max_retries = self.max_retries.unwrap_or(1);
|
||||||
|
|
||||||
|
for _ in 0..max_retries {
|
||||||
|
let response = self.client.execute(request.clone()).await?;
|
||||||
|
|
||||||
|
if response.status().is_server_error() || response.status() == StatusCode::TOO_MANY_REQUESTS {
|
||||||
|
let delay = self.parse_retry_after_header(&response)
|
||||||
|
.unwrap_or(self.default_delay);
|
||||||
|
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs_f64(delay)).await;
|
||||||
|
} else {
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(TogglApiError::RetriesExceeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_retry_after_header(&self, response: &Response) -> Option<f64> {
|
||||||
|
match response.headers().get("Retry-After") {
|
||||||
|
Some(retry_after) => {
|
||||||
|
let retry_after = retry_after.to_str()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let a = retry_after.parse::<f64>()
|
||||||
|
.ok()
|
||||||
|
.or_else(|_| {
|
||||||
|
let date = chrono::NaiveDateTime::parse_from_str(
|
||||||
|
retry_after,
|
||||||
|
"%a, %d %b %Y %H:%M:%S GMT",
|
||||||
|
);
|
||||||
|
|
||||||
|
date.map(|date| date.and_utc())
|
||||||
|
.map(|date| Utc::now().signed_duration_since(date))
|
||||||
|
.map(|time_delta| time_delta.num_seconds() as f64)
|
||||||
|
.ok()
|
||||||
|
});
|
||||||
|
|
||||||
|
a
|
||||||
|
}
|
||||||
|
|
||||||
|
None => Some(self.default_delay),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TogglApi {
|
||||||
|
tx: tokio::sync::mpsc::Sender<TogglApiRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TogglApi {
|
||||||
|
pub async fn request(&self, request: Request) -> Result<Response, TogglApiError> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.tx.send(TogglApiRequest(request, tx)).await.expect("send request");
|
||||||
|
rx.await.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let (mut worker, api_client) = TogglApiWorker::new();
|
||||||
|
|
||||||
|
tokio::spawn(async move { worker.start().await; });
|
||||||
|
|
||||||
|
dbg!(api_client.request(Request::new(Method::GET, "https://www.google.com".parse().unwrap())).await.unwrap());
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user