Skip to content

Commit

Permalink
feat: organize into modules and add api endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
Cyrix126 committed Jul 9, 2024
1 parent a4adde1 commit c9a4b7b
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 49 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ serde = { version = "1", features = ["derive"]}
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = "0.3"
axum = {version="0.7", default-features= false, features= ["tokio", "http2", "macros"] }
axum = {version="0.7", default-features= false, features= ["tokio", "http2", "macros", "json"] }
tokio = {version="1", default-features=false, features= ["rt-multi-thread", "sync", "macros"] }
reqwest = {version="0.12", default-features=false, features=["rustls-tls", "http2"]}
url = {version="2.5", features=["serde"]}
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ It must give a very good performance for common usages of websites, but will sac
## Features
- configuration file
- multiple backend service possible, based on HOST header to decide where to redirect.
- update rules of redirection without restart or loosing current cache.
- cache invalidation api
- well thought expiration of cache (thanks [moka](https://github.com/moka-rs/moka))
- add etag header
- return non modified status when client has a valid etag
- takes into account Vary header from server (will save different cache object for every variation of the specified header)
- let backend service decide his own caching controls.
- admin API
- update rules of redirection without restart or loosing current cache.
- cache invalidation
- update fallback
- get raw cache content
- get stats of cache
## Usage
Configure your reverse proxy to redirect requests you want to cache on Mnemosyne.
**Warning**: make sure your reverse proxy does not apply unwanted modification on HOST header of your requests.
Expand Down
68 changes: 68 additions & 0 deletions src/api/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::str::FromStr;

use crate::index_cache::IndexCache;
use crate::AppState;
use axum::extract::Path;
use axum::http::StatusCode;
use axum::{extract::State, response::IntoResponse, Json};
use serde::Serialize;
use tracing::{debug, warn};
use uuid::Uuid;

// handle get cache endpoint
pub async fn cache_stats(State(state): State<AppState>) -> impl IntoResponse {
debug!("new request to get cache stats");
let stats = CacheStats {
name: state.cache.name().unwrap_or_default().to_string(),
entries: state.cache.entry_count(),
size: state.cache.weighted_size(),
};
(StatusCode::OK, Json(stats))
}
#[derive(Serialize)]
struct CacheStats {
name: String,
entries: u64,
size: u64,
}

// handle delete endpoint
// will also delete from index by iterating over the entries to find the method/path
pub async fn delete_entry(
Path(path): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
debug!("new request to delete a cache entry");
if let Ok(uuid) = Uuid::from_str(&path) {
state.cache.invalidate(&uuid).await;
state.index_cache.lock().await.delete_uuid_from_index(&uuid);
debug!("cache entry removed");
return StatusCode::OK;
}
warn!("deletion request for invalid uuid");
StatusCode::NOT_FOUND
}
// handle raw entry endpoint
// will return the raw data of a cache entry
// it is present for debugging purposes.
pub async fn get_cache_entry(
Path(path): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
debug!("new request to return a raw cache entry");
if let Ok(uuid) = Uuid::from_str(&path) {
if let Some(entry) = state.cache.get(&uuid).await {
return entry.into_response();
}
}
warn!("deletion request for invalid uuid");
StatusCode::NOT_FOUND.into_response()
}
// handle delete_all endpoint
pub async fn delete_entries(State(state): State<AppState>) -> impl IntoResponse {
debug!("new request to delete all cache entries");
state.cache.invalidate_all();
*state.index_cache.lock().await = IndexCache::new();
debug!("all cache cleared");
StatusCode::OK
}
78 changes: 78 additions & 0 deletions src/api/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use axum::{
extract::{Path, State},
response::IntoResponse,
};
use reqwest::StatusCode;
use tracing::debug;
use url::Url;

use crate::AppState;

// handle delete endpoint
pub async fn delete_endpoint(
Path(path): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
debug!("new request to delete an endpoint in configuration");
if let Some(index) = state
.config
.lock()
.await
.endpoints
.iter()
.position(|x| *x.0 == path)
{
// delete endpoint
state.config.lock().await.endpoints.remove(index);
// write config

// return success
return StatusCode::OK;
}
// return not found
StatusCode::NOT_FOUND
}
// handle add endpoint
pub async fn add_endpoint(
Path(path): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
debug!("new request to delete an endpoint in configuration");
if let Some(index) = state
.config
.lock()
.await
.endpoints
.iter()
.position(|x| *x.0 == path)
{
// delete endpoint
state.config.lock().await.endpoints.remove(index);
// write config

// return success
return StatusCode::OK;
}
// return not found
StatusCode::NOT_FOUND
}
pub async fn set_fallback_value(State(state): State<AppState>, body: String) -> impl IntoResponse {
debug!("new request to set the fallback in configuration");
if let Ok(url) = Url::parse(&body) {
state.config.lock().await.fall_back_endpoint = url;
}
// return not found
StatusCode::NOT_FOUND
}
pub async fn get_fallback_value(State(state): State<AppState>) -> impl IntoResponse {
debug!("new request to get the fallback in configuration");
let body = &state.config.lock().await.fall_back_endpoint;
// return not found
(StatusCode::NOT_FOUND, body.to_string())
}
// handle delete all endpoints
pub async fn delete_endpoints(State(state): State<AppState>) -> impl IntoResponse {
debug!("new request to delete all endpoints in configuration");
state.config.lock().await.endpoints = Vec::new();
StatusCode::OK
}
53 changes: 12 additions & 41 deletions src/api.rs → src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,49 +1,18 @@
use std::str::FromStr;

use axum::{
body::to_bytes,
extract::{Path, Request, State},
http::{uri::PathAndQuery, HeaderMap, HeaderValue},
response::IntoResponse,
};
use crate::index_cache::headers_match_vary;
use crate::AppState;
use axum::body::to_bytes;
use axum::extract::{Request, State};
use axum::http::{uri::PathAndQuery, HeaderMap, HeaderValue};
use axum::response::IntoResponse;
use enclose::enc;
use reqwest::{
header::{ETAG, HOST, VARY},
StatusCode,
};
use reqwest::header::{ETAG, HOST, VARY};
use reqwest::StatusCode;
use tokio::spawn;
use tracing::{debug, info, trace, warn};
use uuid::Uuid;

use crate::{
index_cache::{headers_match_vary, IndexCache},
AppState,
};

// handle delete endpoint
// will also delete from index by iterating over the entries to find the method/path
pub async fn delete_entry(
Path(path): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
debug!("new request to delete a cache entry");
if let Ok(uuid) = Uuid::from_str(&path) {
state.cache.invalidate(&uuid).await;
state.index_cache.lock().await.delete_uuid_from_index(&uuid);
debug!("cache entry removed");
return StatusCode::OK;
}
warn!("deletion request for invalid uuid");
StatusCode::NOT_FOUND
}
// handle delete_all endpoint
pub async fn delete_all(State(state): State<AppState>) -> impl IntoResponse {
debug!("new request to delete all cache entries");
state.cache.invalidate_all();
*state.index_cache.lock().await = IndexCache::new();
debug!("all cache cleared");
StatusCode::OK
}
pub mod cache;
pub mod config;

// handle request
pub async fn handler(State(state): State<AppState>, request: Request) -> impl IntoResponse {
Expand Down Expand Up @@ -81,6 +50,8 @@ pub async fn handler(State(state): State<AppState>, request: Request) -> impl In
debug!("response was not cached, requesting backend service");
let url_backend = state
.config
.lock()
.await
.to_backend_uri(&req_uri, request.headers().get(HOST));
debug!("Request URI retrieved: {req_uri}");
debug!("Request URL transmitted:{url_backend}");
Expand Down
34 changes: 29 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
use anyhow::Result;
use axum::{routing::delete, Router};
use api::cache::{cache_stats, delete_entries, delete_entry, get_cache_entry};
use api::config::{
add_endpoint, delete_endpoint, delete_endpoints, get_fallback_value, set_fallback_value,
};
use axum::routing::get;
use axum::{
routing::{delete, post, put},
Router,
};
use cache::Cache;
use config::Config;
use index_cache::IndexCache;
Expand All @@ -18,7 +26,7 @@ mod config;
mod index_cache;
#[derive(Clone)]
struct AppState {
config: Config,
config: Arc<Mutex<Config>>,
// option HeaderMap is the header request that needs to be present.
// the response will contains a Vary Header in this case.
// one method and uri can contain multiple different response based on headers, so we use a Vec per entry since the id of the entry is based on uri and method.
Expand Down Expand Up @@ -46,14 +54,30 @@ async fn main() -> Result<()> {

fn router() -> Router<AppState> {
Router::new()
.route("/delete/:uuid", delete(api::delete_entry))
.route("/delete_all", delete(api::delete_all))
.nest("/api/1/cache", cache_router())
.nest("/api/1/config", config_router())
.fallback(api::handler)
}

fn cache_router() -> Router<AppState> {
Router::new()
.route("/:uuid", delete(delete_entry))
.route("/:uuid", get(get_cache_entry))
.route("/", delete(delete_entries))
.route("/", get(cache_stats))
}
fn config_router() -> Router<AppState> {
Router::new()
.route("/endpoint/:endpoint", delete(delete_endpoint))
.route("/endpoint/:endpoint", put(add_endpoint))
.route("/endpoint", delete(delete_endpoints))
.route("/fallback", get(get_fallback_value))
.route("/fallback", post(set_fallback_value))
}
fn new_state(config: Config) -> AppState {
AppState {
cache: Cache::new(&config),
config,
config: Arc::new(Mutex::new(config)),
index_cache: Arc::new(Mutex::new(IndexCache::new())),
client: Client::new(),
}
Expand Down

0 comments on commit c9a4b7b

Please sign in to comment.