Skip to content

Commit

Permalink
feat: add endpoint for OpenAPI document, add doc
Browse files Browse the repository at this point in the history
  • Loading branch information
Cyrix126 committed Jul 11, 2024
1 parent 528847f commit 8a659f9
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 47 deletions.
10 changes: 6 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,22 @@ edition = "2021"

[dependencies]
confy = "0.6"
serde = { version = "1", features = ["derive"]}
serde = { version = "1", features = ["derive", "rc"]}
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = "0.3"
axum = {version="0.7", default-features= false, features= ["tokio", "http2", "macros", "json"] }
axum = {version="0.7", default-features=false, features= ["tokio", "http2", "macros", "json", "query", "form", "matched-path", "original-uri"] }
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"]}
moka = {version="0.12", features=["future"]}
ahash = "0.8"
uuid = {version="1.8", features=["v4", "fast-rng"]}
uuid = {version="1.10", features=["v4", "fast-rng"]}
nohash = "0.2"
derive_more = {version="0.99", default-features=false, features=["deref", "deref_mut"]}
enclose = "1.2"
typesize = "0.1"
aide = {version="0.13", features=["axum"]}
tower-http = {version="0.5", features=["set-header"]}
[dev-dependencies]
axum-test = "15.2"
axum-test = "15.3"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ All features are present, but the software is very new and not tested for produc
- [x] organize code in modules
- [x] tracing
- [x] tests
- [x] documentation
- [ ] benchmarks/optimizations
- [ ] documentation
## Description
Mnemosyne is placed between your load balancer (ex: nginx) and your server applications that needs their requests to be cached. It will optimize the resources by caching responses and adding caching headers that will ask clients to re-use the cache locally. The cache will be expired based on activity and from manual invalidation.
## Objectives
Expand Down
37 changes: 37 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# DOCUMENTATION Mnemosyne
## About
Mnemosyne is a http caching proxy made to save resources on server side by caching response from backend service and on client side using etag and not modified headers.
It does not support non http connections for now.
It offers an API to manage the cache and invalidate entries, so backend service can trigger the cache to remove obsolete cache entries without waiting for a timer.
## Configuration file
The configuration file is expected to be on the path /etc/mnemosyne/config.toml It needs to have read/write permission of the user running Mnemosyne.
The configuration format is toml.
```,ignore
## which address:port Mnemosyne will listen to
listen_address = "127.0.0.1:9830"
## for a HOST header, redirect to address.
## If it's not precised enough for your scenario, you could make your reverse proxy put a custom HOST header for different path.
endpoints = [["example.net","http://127.0.0.1:9934"]]
## if the HOST of the request does not exists in the "endpoints" var, redirect to this address.
fall_back_endpoint = "http://127.0.0.1:1000/"
## cache configuration
[cache]
## Size in Megabytes before most unused entries will be deleted.
size_limit = 250
## time in seconds before unused entres will be deleted.
expiration = 2592000
```
## Integrating in your reverse-proxy
Your reverse proxy must send the request to Mnemosyne that will redirect them to their respective backend service depending on the HOST header.
### Example nginx
```,ignore
location / {
## redirect to Mnemosyne
proxy_pass http://127.0.0.1:9830;
## keep the same HOST header
proxy_set_header Host $host;
```
## Admin API
The admin API should be protected by an authentication. Mnemosyne does not have any, you must choose one yourself and protect the endpoint /api with it.
You can access the OpenAPI document file on /openapi.json and view it with a OpenAPI document viewer like Swagger.
9 changes: 5 additions & 4 deletions src/api/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::str::FromStr;

use crate::index_cache::IndexCache;
use crate::AppState;
use aide::axum::IntoApiResponse;
use axum::extract::Path;
use axum::http::StatusCode;
use axum::{extract::State, response::IntoResponse, Json};
Expand All @@ -10,7 +11,7 @@ use tracing::{debug, warn};
use uuid::Uuid;

// handle get cache endpoint
pub async fn cache_stats(State(state): State<AppState>) -> impl IntoResponse {
pub async fn cache_stats(State(state): State<AppState>) -> impl IntoApiResponse {
debug!("new request to get cache stats");
let stats = CacheStats {
name: state.cache.name().unwrap_or_default().to_string(),
Expand All @@ -31,7 +32,7 @@ struct CacheStats {
pub async fn delete_entry(
Path(path): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
) -> impl IntoApiResponse {
debug!("new request to delete a cache entry");
if let Ok(uuid) = Uuid::from_str(&path) {
state.cache.invalidate(&uuid).await;
Expand All @@ -48,7 +49,7 @@ pub async fn delete_entry(
pub async fn get_cache_entry(
Path(path): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
) -> impl IntoApiResponse {
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 {
Expand All @@ -59,7 +60,7 @@ pub async fn get_cache_entry(
StatusCode::NOT_FOUND.into_response()
}
// handle delete_all endpoint
pub async fn delete_entries(State(state): State<AppState>) -> impl IntoResponse {
pub async fn delete_entries(State(state): State<AppState>) -> impl IntoApiResponse {
debug!("new request to delete all cache entries");
state.cache.invalidate_all();
*state.index_cache.lock().await = IndexCache::new();
Expand Down
19 changes: 10 additions & 9 deletions src/api/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use axum::{
extract::{Path, State},
response::IntoResponse,
};
use aide::axum::IntoApiResponse;
use axum::extract::{Path, State};
use reqwest::StatusCode;
use tracing::debug;
use url::Url;
Expand All @@ -12,7 +10,7 @@ use crate::AppState;
pub async fn delete_endpoint(
Path(path): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
) -> impl IntoApiResponse {
debug!("new request to delete an endpoint in configuration");
if let Some(index) = state
.config
Expand All @@ -36,7 +34,7 @@ pub async fn delete_endpoint(
pub async fn add_endpoint(
Path(path): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
) -> impl IntoApiResponse {
debug!("new request to delete an endpoint in configuration");
if let Some(index) = state
.config
Expand All @@ -56,22 +54,25 @@ pub async fn add_endpoint(
// return not found
StatusCode::NOT_FOUND
}
pub async fn set_fallback_value(State(state): State<AppState>, body: String) -> impl IntoResponse {
pub async fn set_fallback_value(
State(state): State<AppState>,
body: String,
) -> impl IntoApiResponse {
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 {
pub async fn get_fallback_value(State(state): State<AppState>) -> impl IntoApiResponse {
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 {
pub async fn delete_endpoints(State(state): State<AppState>) -> impl IntoApiResponse {
debug!("new request to delete all endpoints in configuration");
state.config.lock().await.endpoints = Vec::new();
StatusCode::OK
Expand Down
2 changes: 1 addition & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ pub struct CacheConfig {
impl Default for CacheConfig {
fn default() -> Self {
Self {
expiration: 2592000,
expiration: 300,
size_limit: 250,
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/doc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use aide::{axum::IntoApiResponse, openapi::OpenApi, transform::TransformOpenApi};
use axum::Extension;
use std::sync::Arc;

/// serve document as json
pub async fn serve_docs(Extension(api): Extension<Arc<OpenApi>>) -> impl IntoApiResponse {
axum::Json(api)
}

/// description OpenAPI document
pub fn description_docs(api: TransformOpenApi) -> TransformOpenApi {
api.title("Mnemosyne Open API")
.summary("Caching proxy server OpenAPI")
.description(include_str!("../README.md"))
}
75 changes: 47 additions & 28 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
use aide::axum::routing::{delete, get, post, put};
use aide::axum::ApiRouter;
use aide::openapi::OpenApi;
use anyhow::Result;
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 axum::http::HeaderValue;
use axum::{Extension, Router};
use cache::Cache;
use config::Config;
use index_cache::IndexCache;
use reqwest::header::ACCESS_CONTROL_ALLOW_ORIGIN;
use reqwest::Client;
use std::sync::Arc;
use tokio::sync::Mutex;
use tower_http::set_header::SetResponseHeaderLayer;
use tracing::info;

use crate::doc::{description_docs, serve_docs};

/// Handlers
mod api;
/// impl for Moka Cache wrapper
mod cache;
/// configuration from file
mod config;
/// OpenAPI
mod doc;
/// IndexCache
mod index_cache;
#[derive(Clone)]
Expand All @@ -44,35 +50,47 @@ async fn main() -> Result<()> {
info!("creating the cache and index...");
let state = new_state(config);
info!("Done.");
// create route for cache API
let route = router().with_state(state);
let app = app_main(state, OpenApi::default());
info!("starting to listen on {listen}");
let listener = tokio::net::TcpListener::bind(listen).await?;
axum::serve(listener, route.into_make_service()).await?;
axum::serve(listener, app).await?;
Ok(())
}

fn router() -> Router<AppState> {
Router::new()
.nest("/api/1/cache", cache_router())
.nest("/api/1/config", config_router())
fn app_main(state: AppState, mut api: OpenApi) -> Router {
ApiRouter::new()
.route("/openapi.json", get(serve_docs))
.nest("/api/1", router())
.fallback(api::handler)
.finish_api_with(&mut api, description_docs)
.layer(Extension(Arc::new(api)))
.layer(SetResponseHeaderLayer::if_not_present(
ACCESS_CONTROL_ALLOW_ORIGIN,
HeaderValue::from_static("*"),
))
.with_state(state)
}

fn router() -> ApiRouter<AppState> {
ApiRouter::new()
.nest("/cache", cache_router())
.nest("/config", config_router())
}

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 cache_router() -> ApiRouter<AppState> {
ApiRouter::new()
.api_route("/:uuid", delete(delete_entry))
.api_route("/:uuid", get(get_cache_entry))
.api_route("/", delete(delete_entries))
.api_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 config_router() -> ApiRouter<AppState> {
ApiRouter::new()
.api_route("/endpoint/:endpoint", delete(delete_endpoint))
.api_route("/endpoint/:endpoint", put(add_endpoint))
.api_route("/endpoint", delete(delete_endpoints))
.api_route("/fallback", get(get_fallback_value))
.api_route("/fallback", post(set_fallback_value))
}
fn new_state(config: Config) -> AppState {
AppState {
Expand All @@ -89,6 +107,7 @@ fn new_state(config: Config) -> AppState {
mod test {
use std::time::Duration;

use aide::openapi::OpenApi;
use anyhow::Result;
use axum::{http::HeaderValue, routing::get, Router};
use axum_test::TestServer;
Expand All @@ -100,7 +119,7 @@ mod test {
use url::Url;
use uuid::Uuid;

use crate::{config::Config, new_state, router};
use crate::{app_main, config::Config, new_state};

async fn backend_handler() -> &'static str {
"Hello, World!"
Expand Down Expand Up @@ -129,9 +148,9 @@ mod test {
// state of Mnemosyne
let state = new_state(config);
// router
let router = router().with_state(state);
// start Mnemosyne
Ok(TestServer::new(router).unwrap())
let app = app_main(state, OpenApi::default());
Ok(TestServer::new(app).unwrap())
}
#[tokio::test]
async fn first_request() -> Result<()> {
Expand Down

0 comments on commit 8a659f9

Please sign in to comment.