diff --git a/Cargo.lock b/Cargo.lock index b39c1c7..8ee4f93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,13 +134,24 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-priority-channel" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c21678992e1b21bebfe2bc53ab5f5f68c106eddab31b24e0bb06e9b715a86640" dependencies = [ - "event-listener", + "event-listener 2.5.3", ] [[package]] @@ -510,6 +521,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -699,6 +719,27 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + [[package]] name = "faster-hex" version = "0.9.0" @@ -1137,6 +1178,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gxhash" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a197c9b654827513cf53842c5c6d3da2b4b35a785f8e0eff78bdf8e445aba1bb" +dependencies = [ + "rustversion", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1642,6 +1692,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "moka" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" +dependencies = [ + "async-lock", + "async-trait", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "event-listener 5.3.1", + "futures-util", + "once_cell", + "parking_lot", + "quanta", + "rustc_version", + "smallvec", + "tagptr", + "thiserror", + "triomphe", + "uuid", +] + [[package]] name = "ndk-context" version = "0.1.1" @@ -1777,6 +1851,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -2038,6 +2118,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quick-xml" version = "0.31.0" @@ -2102,6 +2197,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "raw-cpuid" +version = "11.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2258,6 +2362,15 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.34" @@ -2313,6 +2426,12 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" version = "1.0.211" @@ -2507,6 +2626,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tap" version = "1.0.1" @@ -2577,6 +2702,8 @@ name = "tectonic-api" version = "0.1.0" dependencies = [ "axum", + "gxhash", + "moka", "serde", "serde_json", "tectonic", @@ -3111,6 +3238,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + [[package]] name = "typenum" version = "1.17.0" @@ -3316,6 +3449,9 @@ name = "uuid" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" +dependencies = [ + "getrandom", +] [[package]] name = "valuable" @@ -3498,6 +3634,16 @@ dependencies = [ "watchexec-signals", ] +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index a3416dd..41c1367 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,9 @@ lto = "fat" codegen-units = 1 panic = "abort" +[build] +rustflags = ["-C", "target-cpu=native"] + [package] name = "tectonic-api" version = "0.1.0" @@ -17,4 +20,11 @@ tower-http = { version = "0.6.1", features = ["util"] } utoipa = { version = "5.1.1", features = ["axum_extras"] } utoipa-swagger-ui = { version = "8.0.2", features = ["axum"] } utoipauto = "0.2.0" -tectonic = { git = "https://github.com/tectonic-typesetting/tectonic.git", default-features = false, features = ["geturl-curl"] } +tectonic = { git = "https://github.com/tectonic-typesetting/tectonic.git", default-features = false, features = [ + "geturl-curl", +] } +moka = { version = "0.12.8", features = ["future"] } +gxhash = "3.4.1" + +[features] +hybrid = ["gxhash/hybrid"] diff --git a/Dockerfile.build b/Dockerfile.build index 2a03fbe..0bacec4 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -5,7 +5,7 @@ COPY . . RUN apt update RUN apt install -y curl g++ libssl-dev libfontconfig-dev libharfbuzz-dev RUN rustup default nightly -RUN cargo install --locked --path . +RUN cargo install --locked --features hybrid --path . FROM rust:slim AS texlive-builder diff --git a/src/lib.rs b/src/lib.rs index dae1297..fb8b8e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ +mod state; mod v1; -use axum::routing::get; use axum::Router; use utoipa::OpenApi; @@ -11,11 +11,20 @@ struct ApiSpecification; pub fn app() -> Router { let root_path = "/api"; + let shared_state = std::sync::Arc::new(state::AppState { + cache: moka::future::Cache::builder() + .weigher(|_, v: &Vec| v.len().try_into().unwrap_or(u32::MAX)) + .max_capacity(12 * 1024 * 1024 * 1024) + .time_to_idle(std::time::Duration::from_secs(3600)) + .build_with_hasher(gxhash::GxBuildHasher::default()), + }); Router::new() .nest( root_path, - Router::new().route("/", get(())).merge(v1::router()), + Router::new() + .route("/", axum::routing::get(())) + .merge(v1::router(shared_state)), ) .merge( utoipa_swagger_ui::SwaggerUi::new(format!("{}/docs", root_path)) diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..b253c6e --- /dev/null +++ b/src/state.rs @@ -0,0 +1,5 @@ +use gxhash::GxBuildHasher; + +pub struct AppState { + pub cache: moka::future::Cache, GxBuildHasher>, +} diff --git a/src/v1.rs b/src/v1.rs index d433314..1b08c00 100644 --- a/src/v1.rs +++ b/src/v1.rs @@ -6,8 +6,11 @@ use axum::{ Router, }; -pub fn router() -> Router { +use crate::state::AppState; + +pub fn router(shared_state: std::sync::Arc) -> Router { Router::new() .route("/v1", get(index::index)) .route("/v1/compile", post(compile::compile)) + .with_state(shared_state) } diff --git a/src/v1/compile.rs b/src/v1/compile.rs index 0672006..c28e6e3 100644 --- a/src/v1/compile.rs +++ b/src/v1/compile.rs @@ -1,9 +1,14 @@ +use std::sync::Arc; + use axum::body::Body; +use axum::extract::State; use axum::http::header::{CONTENT_DISPOSITION, CONTENT_TYPE}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; +use crate::state::AppState; + #[derive(serde::Deserialize, utoipa::ToSchema)] pub struct CompileSchema { #[schema(example = "\\documentclass{article}\\begin{document}Hello, world!\\end{document}")] @@ -19,8 +24,21 @@ pub struct CompileSchema { (status = 400, body = String) ) )] -pub async fn compile(Json(payload): Json) -> impl IntoResponse { - match tectonic::latex_to_pdf(payload.latex) { +pub async fn compile( + State(state): State>, + Json(payload): Json, +) -> impl IntoResponse { + let result = state + .cache + .try_get_with(payload.latex.clone(), async { + match tectonic::latex_to_pdf(payload.latex) { + Ok(pdf) => Ok(pdf), + Err(error) => Err(error.to_string()), + } + }) + .await; + + match result { Ok(pdf) => Response::builder() .status(StatusCode::OK) .header(CONTENT_TYPE, "application/pdf")