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

Simple plugin system #1389

Merged
merged 25 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cb09258
Create request chain with basic authentication
thomas-zahner Mar 11, 2024
3c0747e
Test chain
thomas-zahner Mar 11, 2024
bbb4792
Add quirks to request chain
thomas-zahner Mar 12, 2024
95b046a
Pass down request_chain instead of credentials & add test
thomas-zahner Mar 12, 2024
e702086
Introduce early exit in chain
thomas-zahner Mar 13, 2024
d9a3d1d
Implement Chainable directly for BasicAuthCredentials
thomas-zahner Mar 13, 2024
811a73b
Move chain into check_website function
thomas-zahner Mar 13, 2024
da39a4f
Update RequestChain & add chain to client
thomas-zahner Mar 13, 2024
b006a9f
Add doc comment
thomas-zahner Mar 14, 2024
84dfd00
Small improvements
thomas-zahner Mar 14, 2024
9b42240
Apply suggestions
thomas-zahner Mar 15, 2024
3fb34e7
Apply clippy suggestions
thomas-zahner Mar 15, 2024
9f381e3
Move Arc and Mutex inside of Chain struct
thomas-zahner Mar 15, 2024
1c6c39f
Extract checking functionality & make chain async
thomas-zahner Mar 20, 2024
7917d5d
Use `async_trait` to fix issues with `Chain` type inference
mre-trv Mar 20, 2024
31f4494
Make checker part of the request chain
thomas-zahner Mar 22, 2024
ea66ab0
Add credentials to chain
thomas-zahner Apr 3, 2024
ca55953
Create ClientRequestChain helper structure to combine multiple chains
thomas-zahner Apr 5, 2024
61df5c9
Small tweaks & extract method
thomas-zahner Apr 5, 2024
7c4834d
Extract function and add SAFETY note
thomas-zahner Apr 11, 2024
a4f57cb
Add documentation to `chain` module
mre Apr 21, 2024
2a5fbcb
Extend docs around `clone_unwrap`
mre Apr 21, 2024
d99ba5c
Adjust documentation
thomas-zahner Apr 22, 2024
c228a8b
Merge pull request #3 from lycheeverse/plugin-prototype-docs
thomas-zahner Apr 22, 2024
50bd88a
Rename Chainable to Handler
thomas-zahner Apr 22, 2024
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
5 changes: 3 additions & 2 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions lychee-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ version.workspace = true

[dependencies]
async-stream = "0.3.5"
async-trait = "0.1.78"
cached = "0.46.1"
check-if-email-exists = { version = "0.9.1", optional = true }
email_address = "0.2.4"
Expand Down
125 changes: 125 additions & 0 deletions lychee-lib/src/chain/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use crate::Status;
use async_trait::async_trait;
use core::fmt::Debug;
use std::sync::Arc;
use tokio::sync::Mutex;

#[derive(Debug, PartialEq)]
pub(crate) enum ChainResult<T, R> {
Next(T),
Done(R),
}

pub(crate) type RequestChain = Chain<reqwest::Request, Status>;

pub(crate) type InnerChain<T, R> = Vec<Box<dyn Chainable<T, R> + Send>>;

#[derive(Debug)]
pub struct Chain<T, R>(Arc<Mutex<InnerChain<T, R>>>);

impl<T, R> Default for Chain<T, R> {
fn default() -> Self {
Self(Arc::new(Mutex::new(vec![])))
}
}

impl<T, R> Clone for Chain<T, R> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}

impl<T, R> Chain<T, R> {
pub(crate) fn new(values: InnerChain<T, R>) -> Self {
Self(Arc::new(Mutex::new(values)))
}

pub(crate) async fn traverse(&self, mut input: T) -> ChainResult<T, R> {
use ChainResult::{Done, Next};
for e in self.0.lock().await.iter_mut() {
match e.chain(input).await {
Next(r) => input = r,
Done(r) => {
return Done(r);
}
}
}

Next(input)
}
}

#[async_trait]
pub(crate) trait Chainable<T, R>: Debug {
async fn chain(&mut self, input: T) -> ChainResult<T, R>;
}

#[derive(Debug)]
pub(crate) struct ClientRequestChain<'a> {
chains: Vec<&'a RequestChain>,
}

impl<'a> ClientRequestChain<'a> {
pub(crate) fn new(chains: Vec<&'a RequestChain>) -> Self {
Self { chains }
}

pub(crate) async fn traverse(&self, mut input: reqwest::Request) -> Status {
use ChainResult::{Done, Next};
for e in &self.chains {
match e.traverse(input).await {
Next(r) => input = r,
Done(r) => {
return r;
}
}
}

// consider as excluded if no chain element has converted it to a done
Status::Excluded
}
}

mod test {
use super::{
ChainResult,
ChainResult::{Done, Next},
Chainable,
};
use async_trait::async_trait;

#[derive(Debug)]
struct Add(usize);

#[derive(Debug, PartialEq, Eq)]
struct Result(usize);

#[async_trait]
impl Chainable<Result, Result> for Add {
async fn chain(&mut self, req: Result) -> ChainResult<Result, Result> {
let added = req.0 + self.0;
if added > 100 {
Done(Result(req.0))
} else {
Next(Result(added))
}
}
}

#[tokio::test]
async fn simple_chain() {
use super::Chain;
let chain: Chain<Result, Result> = Chain::new(vec![Box::new(Add(7)), Box::new(Add(3))]);
let result = chain.traverse(Result(0)).await;
assert_eq!(result, Next(Result(10)));
}

#[tokio::test]
async fn early_exit_chain() {
use super::Chain;
let chain: Chain<Result, Result> =
Chain::new(vec![Box::new(Add(80)), Box::new(Add(30)), Box::new(Add(1))]);
let result = chain.traverse(Result(0)).await;
assert_eq!(result, Done(Result(80)));
}
}
67 changes: 67 additions & 0 deletions lychee-lib/src/checker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use crate::{
chain::{ChainResult, Chainable},
retry::RetryExt,
Status,
};
use async_trait::async_trait;
use http::StatusCode;
use reqwest::Request;
use std::{collections::HashSet, time::Duration};

#[derive(Debug, Clone)]
pub(crate) struct Checker {
retry_wait_time: Duration,
max_retries: u64,
reqwest_client: reqwest::Client,
accepted: Option<HashSet<StatusCode>>,
}

impl Checker {
pub(crate) const fn new(
retry_wait_time: Duration,
max_retries: u64,
reqwest_client: reqwest::Client,
accepted: Option<HashSet<StatusCode>>,
) -> Self {
Self {
retry_wait_time,
max_retries,
reqwest_client,
accepted,
}
}

/// Retry requests up to `max_retries` times
/// with an exponential backoff.
pub(crate) async fn retry_request(&self, request: Request) -> Status {
let mut retries: u64 = 0;
let mut wait_time = self.retry_wait_time;

let mut status = self.check_default(request.try_clone().unwrap()).await; // TODO: try_clone
while retries < self.max_retries {
if status.is_success() || !status.should_retry() {
return status;
}
retries += 1;
tokio::time::sleep(wait_time).await;
wait_time = wait_time.saturating_mul(2);
status = self.check_default(request.try_clone().unwrap()).await; // TODO: try_clone
thomas-zahner marked this conversation as resolved.
Show resolved Hide resolved
}
status
}

/// Check a URI using [reqwest](https://github.com/seanmonstar/reqwest).
async fn check_default(&self, request: Request) -> Status {
match self.reqwest_client.execute(request).await {
Ok(ref response) => Status::new(response, self.accepted.clone()),
Err(e) => e.into(),
}
}
}

#[async_trait]
impl Chainable<Request, Status> for Checker {
async fn chain(&mut self, input: Request) -> ChainResult<Request, Status> {
ChainResult::Done(self.retry_request(input).await)
}
}
Loading
Loading