ソースを参照

Introduce HTTP cache layer

The foreign cache is a mechanism to share and synchronize cache content between
nodes; since the caching strategy is quite simple and no eviction policies
other than TTL, this layer will be used to synchronize data between nodes.

For performance reasons, the cache is always local first.
Cesar Rodas 4 ヶ月 前
コミット
8d06ba714f

+ 134 - 50
Cargo.lock

@@ -72,9 +72,9 @@ dependencies = [
 
 [[package]]
 name = "allocator-api2"
-version = "0.2.20"
+version = "0.2.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
 
 [[package]]
 name = "android-tzdata"
@@ -148,9 +148,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.93"
+version = "1.0.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
+checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
 dependencies = [
  "backtrace",
 ]
@@ -247,7 +247,7 @@ dependencies = [
  "js-sys",
  "thiserror 1.0.69",
  "tokio",
- "tokio-rustls 0.26.0",
+ "tokio-rustls 0.26.1",
  "tokio-socks",
  "tokio-tungstenite 0.24.0",
  "url",
@@ -710,11 +710,14 @@ dependencies = [
  "futures",
  "moka",
  "paste",
+ "redis",
  "serde",
  "serde_json",
+ "sha2",
  "tokio",
  "tracing",
  "utoipa",
+ "uuid",
 ]
 
 [[package]]
@@ -772,7 +775,6 @@ dependencies = [
  "tokio-stream",
  "tokio-util",
  "tracing",
- "uuid",
 ]
 
 [[package]]
@@ -901,6 +903,7 @@ dependencies = [
  "serde_json",
  "thiserror 1.0.69",
  "tracing",
+ "uuid",
 ]
 
 [[package]]
@@ -931,6 +934,7 @@ dependencies = [
  "thiserror 1.0.69",
  "tokio",
  "tracing",
+ "uuid",
 ]
 
 [[package]]
@@ -1040,9 +1044,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.21"
+version = "4.5.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
+checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -1050,9 +1054,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.21"
+version = "4.5.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
+checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
 dependencies = [
  "anstream",
  "anstyle",
@@ -1074,9 +1078,9 @@ dependencies = [
 
 [[package]]
 name = "clap_lex"
-version = "0.7.3"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
 
 [[package]]
 name = "cln-rpc"
@@ -1121,6 +1125,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 
 [[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
 name = "concurrent-queue"
 version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1150,6 +1168,16 @@ dependencies = [
 
 [[package]]
 name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
 version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
@@ -1835,9 +1863,9 @@ dependencies = [
 
 [[package]]
 name = "http"
-version = "1.1.0"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
 dependencies = [
  "bytes",
  "fnv",
@@ -1862,7 +1890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
 dependencies = [
  "bytes",
- "http 1.1.0",
+ "http 1.2.0",
 ]
 
 [[package]]
@@ -1873,7 +1901,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
 dependencies = [
  "bytes",
  "futures-util",
- "http 1.1.0",
+ "http 1.2.0",
  "http-body 1.0.1",
  "pin-project-lite",
 ]
@@ -1929,7 +1957,7 @@ dependencies = [
  "bytes",
  "futures-channel",
  "futures-util",
- "http 1.1.0",
+ "http 1.2.0",
  "http-body 1.0.1",
  "httparse",
  "itoa",
@@ -1960,14 +1988,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
 dependencies = [
  "futures-util",
- "http 1.1.0",
+ "http 1.2.0",
  "hyper 1.5.1",
  "hyper-util",
  "rustls 0.23.19",
- "rustls-native-certs",
+ "rustls-native-certs 0.8.1",
  "rustls-pki-types",
  "tokio",
- "tokio-rustls 0.26.0",
+ "tokio-rustls 0.26.1",
  "tower-service",
  "webpki-roots 0.26.7",
 ]
@@ -1993,7 +2021,7 @@ dependencies = [
  "bytes",
  "futures-channel",
  "futures-util",
- "http 1.1.0",
+ "http 1.2.0",
  "http-body 1.0.1",
  "hyper 1.5.1",
  "pin-project-lite",
@@ -2522,9 +2550,9 @@ dependencies = [
 
 [[package]]
 name = "minreq"
-version = "2.12.0"
+version = "2.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "763d142cdff44aaadd9268bebddb156ef6c65a0e13486bb81673cf2d8739f9b0"
+checksum = "36a8e50e917e18a37d500d27d40b7bc7d127e71c0c94fb2d83f43b4afd308390"
 dependencies = [
  "log",
  "serde",
@@ -3202,7 +3230,7 @@ dependencies = [
  "rustc-hash",
  "rustls 0.23.19",
  "socket2 0.5.8",
- "thiserror 2.0.3",
+ "thiserror 2.0.4",
  "tokio",
  "tracing",
 ]
@@ -3221,7 +3249,7 @@ dependencies = [
  "rustls 0.23.19",
  "rustls-pki-types",
  "slab",
- "thiserror 2.0.3",
+ "thiserror 2.0.4",
  "tinyvec",
  "tracing",
  "web-time",
@@ -3319,6 +3347,30 @@ dependencies = [
 ]
 
 [[package]]
+name = "redis"
+version = "0.23.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f49cdc0bb3f412bf8e7d1bd90fe1d9eb10bc5c399ba90973c14662a27b3f8ba"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "combine",
+ "futures-util",
+ "itoa",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls 0.21.12",
+ "rustls-native-certs 0.6.3",
+ "ryu",
+ "sha1_smol",
+ "socket2 0.4.10",
+ "tokio",
+ "tokio-rustls 0.24.1",
+ "tokio-util",
+ "url",
+]
+
+[[package]]
 name = "redox_syscall"
 version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3401,7 +3453,7 @@ dependencies = [
  "bytes",
  "futures-core",
  "futures-util",
- "http 1.1.0",
+ "http 1.2.0",
  "http-body 1.0.1",
  "http-body-util",
  "hyper 1.5.1",
@@ -3416,7 +3468,7 @@ dependencies = [
  "pin-project-lite",
  "quinn",
  "rustls 0.23.19",
- "rustls-native-certs",
+ "rustls-native-certs 0.8.1",
  "rustls-pemfile 2.2.0",
  "rustls-pki-types",
  "serde",
@@ -3424,7 +3476,7 @@ dependencies = [
  "serde_urlencoded",
  "sync_wrapper 1.0.2",
  "tokio",
- "tokio-rustls 0.26.0",
+ "tokio-rustls 0.26.1",
  "tokio-socks",
  "tower-service",
  "url",
@@ -3620,6 +3672,18 @@ dependencies = [
 
 [[package]]
 name = "rustls-native-certs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile 1.0.4",
+ "schannel",
+ "security-framework 2.11.1",
+]
+
+[[package]]
+name = "rustls-native-certs"
 version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
@@ -3627,7 +3691,7 @@ dependencies = [
  "openssl-probe",
  "rustls-pki-types",
  "schannel",
- "security-framework",
+ "security-framework 3.0.1",
 ]
 
 [[package]]
@@ -3797,12 +3861,25 @@ dependencies = [
 
 [[package]]
 name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.6.0",
+ "core-foundation 0.9.4",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework"
 version = "3.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e1415a607e92bec364ea2cf9264646dcce0f91e6d65281bd6f2819cca3bf39c8"
 dependencies = [
  "bitflags 2.6.0",
- "core-foundation",
+ "core-foundation 0.10.0",
  "core-foundation-sys",
  "libc",
  "security-framework-sys",
@@ -3941,6 +4018,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "sha1_smol"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
+
+[[package]]
 name = "sha2"
 version = "0.10.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4112,6 +4195,7 @@ dependencies = [
  "thiserror 1.0.69",
  "tokio-stream",
  "url",
+ "uuid",
  "webpki-roots 0.22.6",
 ]
 
@@ -4275,11 +4359,11 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "2.0.3"
+version = "2.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa"
+checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490"
 dependencies = [
- "thiserror-impl 2.0.3",
+ "thiserror-impl 2.0.4",
 ]
 
 [[package]]
@@ -4295,9 +4379,9 @@ dependencies = [
 
 [[package]]
 name = "thiserror-impl"
-version = "2.0.3"
+version = "2.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
+checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -4316,9 +4400,9 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.36"
+version = "0.3.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
+checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
 dependencies = [
  "deranged",
  "itoa",
@@ -4337,9 +4421,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
 
 [[package]]
 name = "time-macros"
-version = "0.2.18"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
+checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
 dependencies = [
  "num-conv",
  "time-core",
@@ -4382,9 +4466,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
 [[package]]
 name = "tokio"
-version = "1.41.1"
+version = "1.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
+checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
 dependencies = [
  "backtrace",
  "bytes",
@@ -4442,12 +4526,11 @@ dependencies = [
 
 [[package]]
 name = "tokio-rustls"
-version = "0.26.0"
+version = "0.26.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
+checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
 dependencies = [
  "rustls 0.23.19",
- "rustls-pki-types",
  "tokio",
 ]
 
@@ -4465,9 +4548,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-stream"
-version = "0.1.16"
+version = "0.1.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
 dependencies = [
  "futures-core",
  "pin-project-lite",
@@ -4497,16 +4580,16 @@ dependencies = [
  "rustls 0.23.19",
  "rustls-pki-types",
  "tokio",
- "tokio-rustls 0.26.0",
+ "tokio-rustls 0.26.1",
  "tungstenite 0.24.0",
  "webpki-roots 0.26.7",
 ]
 
 [[package]]
 name = "tokio-util"
-version = "0.7.12"
+version = "0.7.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
+checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
 dependencies = [
  "bytes",
  "futures-core",
@@ -4720,7 +4803,7 @@ dependencies = [
  "byteorder",
  "bytes",
  "data-encoding",
- "http 1.1.0",
+ "http 1.2.0",
  "httparse",
  "log",
  "rand",
@@ -4893,6 +4976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
 dependencies = [
  "getrandom",
+ "serde",
 ]
 
 [[package]]

+ 2 - 0
crates/cdk-axum/Cargo.toml

@@ -30,6 +30,8 @@ serde_json = "1"
 paste = "1.0.15"
 serde = { version = "1.0.210", features = ["derive"] }
 uuid = { version = "1", features = ["v4", "serde"] }
+sha2 = "0.10.8"
+redis = { version = "0.23.3", features = ["tokio-rustls-comp"] }
 
 [features]
 swagger = ["cdk/swagger", "dep:utoipa"]

+ 38 - 0
crates/cdk-axum/src/cache/memory.rs

@@ -0,0 +1,38 @@
+use std::time::Duration;
+
+use moka::future::Cache;
+
+use super::{HttpCacheKey, HttpCacheStorage};
+
+/// In memory cache storage for the HTTP cache.
+///
+/// This is the default cache storage backend, which is used if no other storage
+/// backend is provided, or if the provided storage backend is `None`.
+///
+/// The cache is limited to 10,000 entries and it is not shared between
+/// instances nor persisted.
+pub struct InMemoryHttpCache(pub Cache<HttpCacheKey, Vec<u8>>);
+
+#[async_trait::async_trait]
+impl HttpCacheStorage for InMemoryHttpCache {
+    fn new(cache_ttl: Duration, cache_tti: Duration) -> Self
+    where
+        Self: Sized,
+    {
+        InMemoryHttpCache(
+            Cache::builder()
+                .max_capacity(10_000)
+                .time_to_live(cache_ttl)
+                .time_to_idle(cache_tti)
+                .build(),
+        )
+    }
+
+    async fn get(&self, key: &HttpCacheKey) -> Option<Vec<u8>> {
+        self.0.get(key)
+    }
+
+    async fn set(&self, key: HttpCacheKey, value: Vec<u8>) {
+        self.0.insert(key, value).await;
+    }
+}

+ 117 - 0
crates/cdk-axum/src/cache/mod.rs

@@ -0,0 +1,117 @@
+//! HTTP cache.
+//!
+//! This is mod defines a common trait to define custom backends for the HTTP cache.
+//!
+//! The HTTP cache is a layer to cache responses from HTTP requests, to avoid hitting
+//! the same endpoint multiple times, which can be expensive and slow, or to provide
+//! idempotent operations.
+//!
+//! This mod also provides common backend implementations as well, such as In
+//! Memory (default) and Redis.
+use serde::{de::DeserializeOwned, Serialize};
+use sha2::{Digest, Sha256};
+use std::{ops::Deref, sync::Arc, time::Duration};
+
+mod memory;
+mod redis;
+#[allow(unused_imports)]
+pub use self::{memory::InMemoryHttpCache, redis::HttpCacheRedis};
+
+#[async_trait::async_trait]
+/// Cache storage for the HTTP cache.
+pub trait HttpCacheStorage {
+    /// Create a new cache storage instance
+    fn new(cache_ttl: Duration, cache_tti: Duration) -> Self
+    where
+        Self: Sized;
+
+    /// Get a value from the cache.
+    async fn get(&self, key: &HttpCacheKey) -> Option<Vec<u8>>;
+
+    /// Set a value in the cache.
+    async fn set(&self, key: HttpCacheKey, value: Vec<u8>);
+}
+
+/// Http cache with a pluggable storage backend.
+pub struct HttpCache {
+    storage: Arc<dyn HttpCacheStorage + Send + Sync>,
+}
+
+/// Max payload size for the cache key.
+///
+/// This is a trade-off between security and performance. A large payload can be used to
+/// perform a CPU attack.
+const MAX_PAYLOAD_SIZE: usize = 10 * 1024 * 1024;
+
+/// Http cache key.
+///
+/// This type ensures no Vec<u8> is used as a key, which is error-prone.
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct HttpCacheKey([u8; 32]);
+
+impl Deref for HttpCacheKey {
+    type Target = [u8; 32];
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl HttpCache {
+    /// Create a new HTTP cache.
+    pub fn new(
+        cache_ttl: Duration,
+        cache_tti: Duration,
+        storage: Option<Arc<dyn HttpCacheStorage + Send + Sync + 'static>>,
+    ) -> Self {
+        Self {
+            storage: storage
+                .unwrap_or_else(|| Arc::new(InMemoryHttpCache::new(cache_ttl, cache_tti))),
+        }
+    }
+
+    /// Calculate a cache key from a serializable value.
+    ///
+    /// Usually the input is the request body or query parameters.
+    ///
+    /// The result is an optional cache key. If the key cannot be calculated, it
+    /// will be None, meaning the value cannot be cached, thefore the entire
+    /// caching mechanism should be skipped.
+    ///
+    /// Instead of using the entire serialized input as the key, the key is a
+    /// double hash to have a predictable key size, although it may open the
+    /// window for CPU attacks with large payloads, but it is a trade-off.
+    /// Perhaps upper layer have a protection against large payloads.
+    pub fn calculate_key<K: Serialize>(&self, key: &K) -> Option<HttpCacheKey> {
+        let json_value = match serde_json::to_vec(key) {
+            Ok(value) => value,
+            Err(err) => {
+                tracing::warn!("Failed to serialize key: {:?}", err);
+                return None;
+            }
+        };
+
+        if json_value.len() > MAX_PAYLOAD_SIZE {
+            tracing::warn!("Key size is too large: {}", json_value.len());
+            return None;
+        }
+
+        let first_hash = Sha256::digest(json_value);
+        let second_hash = Sha256::digest(first_hash);
+        Some(HttpCacheKey(second_hash.into()))
+    }
+
+    /// Get a value from the cache.
+    pub async fn get<V: DeserializeOwned>(self: &Arc<Self>, key: &HttpCacheKey) -> Option<V> {
+        self.storage
+            .get(key)
+            .await
+            .map(|value| serde_json::from_slice(&value).unwrap())
+    }
+
+    /// Set a value in the cache.
+    pub async fn set<V: Serialize>(self: &Arc<Self>, key: HttpCacheKey, value: &V) {
+        let value = serde_json::to_vec(value).unwrap();
+        self.storage.set(key, value).await;
+    }
+}

+ 90 - 0
crates/cdk-axum/src/cache/redis.rs

@@ -0,0 +1,90 @@
+use super::{HttpCacheKey, HttpCacheStorage};
+use redis::AsyncCommands;
+use std::time::Duration;
+
+/// Redis cache storage for the HTTP cache.
+///
+/// This cache storage backend uses Redis to store the cache.
+pub struct HttpCacheRedis {
+    cache_ttl: Duration,
+    prefix: Option<Vec<u8>>,
+    client: Option<redis::Client>,
+}
+
+impl HttpCacheRedis {
+    /// Create a new Redis cache.
+    pub fn set_client(mut self, client: redis::Client) -> Self {
+        self.client = Some(client);
+        self
+    }
+
+    /// Set a prefix for the cache keys.
+    ///
+    /// This is useful to have all the HTTP cache keys under a common prefix,
+    /// some sort of namespace, to make managment of the database easier.
+    pub fn set_prefix(mut self, prefix: Vec<u8>) -> Self {
+        self.prefix = Some(prefix);
+        self
+    }
+}
+
+#[async_trait::async_trait]
+impl HttpCacheStorage for HttpCacheRedis {
+    fn new(cache_ttl: Duration, _cache_tti: Duration) -> Self {
+        Self {
+            cache_ttl,
+            prefix: None,
+            client: None,
+        }
+    }
+
+    async fn get(&self, key: &HttpCacheKey) -> Option<Vec<u8>> {
+        let mut con = match self
+            .client
+            .as_ref()
+            .expect("A client must be set with set_client()")
+            .get_multiplexed_tokio_connection()
+            .await
+        {
+            Ok(con) => con,
+            Err(err) => {
+                tracing::error!("Failed to get redis connection: {:?}", err);
+                return None;
+            }
+        };
+
+        let mut db_key = self.prefix.clone().unwrap_or_default();
+        db_key.extend(&**key);
+
+        con.get(db_key)
+            .await
+            .map_err(|err| {
+                tracing::error!("Failed to get value from redis: {:?}", err);
+                err
+            })
+            .ok()
+    }
+
+    async fn set(&self, key: HttpCacheKey, value: Vec<u8>) {
+        let mut db_key = self.prefix.clone().unwrap_or_default();
+        db_key.extend(&*key);
+
+        let mut con = match self
+            .client
+            .as_ref()
+            .expect("A client must be set with set_client()")
+            .get_multiplexed_tokio_connection()
+            .await
+        {
+            Ok(con) => con,
+            Err(err) => {
+                tracing::error!("Failed to get redis connection: {:?}", err);
+                return;
+            }
+        };
+
+        let _: Result<(), _> = con
+            .set_ex(db_key, value, self.cache_ttl.as_secs() as usize)
+            .await;
+    }
+}

+ 14 - 8
crates/cdk-axum/src/lib.rs

@@ -10,9 +10,9 @@ use anyhow::Result;
 use axum::routing::{get, post};
 use axum::Router;
 use cdk::mint::Mint;
-use moka::future::Cache;
 use router_handlers::*;
 
+pub mod cache;
 mod router_handlers;
 mod ws;
 
@@ -52,7 +52,7 @@ use uuid::Uuid;
 #[derive(Clone)]
 pub struct MintState {
     mint: Arc<Mint>,
-    cache: Cache<String, String>,
+    cache: Arc<cache::HttpCache>,
 }
 
 #[cfg(feature = "swagger")]
@@ -132,14 +132,20 @@ pub struct MintState {
 pub struct ApiDocV1;
 
 /// Create mint [`Router`] with required endpoints for cashu mint
-pub async fn create_mint_router(mint: Arc<Mint>, cache_ttl: u64, cache_tti: u64) -> Result<Router> {
+pub async fn create_mint_router(
+    mint: Arc<Mint>,
+    cache_ttl: u64,
+    cache_tti: u64,
+    cache_storage: Option<Arc<dyn cache::HttpCacheStorage + Send + Sync>>,
+) -> Result<Router> {
     let state = MintState {
         mint,
-        cache: Cache::builder()
-            .max_capacity(10_000)
-            .time_to_live(Duration::from_secs(cache_ttl))
-            .time_to_idle(Duration::from_secs(cache_tti))
-            .build(),
+        cache: cache::HttpCache::new(
+            Duration::from_secs(cache_ttl),
+            Duration::from_secs(cache_tti),
+            cache_storage,
+        )
+        .into(),
     };
 
     let v1_router = Router::new()

+ 11 - 11
crates/cdk-axum/src/router_handlers.rs

@@ -11,7 +11,6 @@ use cdk::nuts::{
     SwapRequest, SwapResponse,
 };
 use cdk::util::unix_time;
-use cdk::Error;
 use paste::paste;
 use uuid::Uuid;
 
@@ -31,19 +30,20 @@ macro_rules! post_cache_wrapper {
 
                 let json_extracted_payload = payload.deref();
                 let State(mint_state) = state.clone();
-                let cache_key = serde_json::to_string(&json_extracted_payload).map_err(|err| {
-                    into_response(Error::from(err))
-                })?;
-
-                if let Some(cached_response) = mint_state.cache.get(&cache_key) {
-                    return Ok(Json(serde_json::from_str(&cached_response)
-                        .expect("Shouldn't panic: response is json-deserializable.")));
+                let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) {
+                    Some(key) => key,
+                    None => {
+                        // Could not calculate key, just return the handler result
+                        return $handler(state, payload).await;
+                    }
+                };
+
+                if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await {
+                    return Ok(Json(cached_response));
                 }
 
                 let response = $handler(state, payload).await?;
-                mint_state.cache.insert(cache_key, serde_json::to_string(response.deref())
-                    .expect("Shouldn't panic: response is json-serializable.")
-                ).await;
+                mint_state.cache.set(cache_key, &response.deref()).await;
                 Ok(response)
             }
         }

+ 4 - 3
crates/cdk-integration-tests/src/init_fake_wallet.rs

@@ -54,9 +54,10 @@ where
     let cache_tti = 3600;
     let mint_arc = Arc::new(mint);
 
-    let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint_arc), cache_ttl, cache_tti)
-        .await
-        .unwrap();
+    let v1_service =
+        cdk_axum::create_mint_router(Arc::clone(&mint_arc), cache_ttl, cache_tti, None)
+            .await
+            .unwrap();
 
     let mint_service = Router::new()
         .merge(v1_service)

+ 1 - 0
crates/cdk-integration-tests/src/init_regtest.rs

@@ -224,6 +224,7 @@ where
         Arc::clone(&mint_arc),
         cache_time_to_live,
         cache_time_to_idle,
+        None,
     )
     .await
     .unwrap();

+ 1 - 0
crates/cdk-integration-tests/src/lib.rs

@@ -92,6 +92,7 @@ pub async fn start_mint(
         Arc::clone(&mint_arc),
         cache_time_to_live,
         cache_time_to_idle,
+        None,
     )
     .await?;
 

+ 2 - 1
crates/cdk-mintd/src/main.rs

@@ -325,7 +325,8 @@ async fn main() -> anyhow::Result<()> {
         .seconds_to_extend_cache_by
         .unwrap_or(DEFAULT_CACHE_TTI_SECS);
 
-    let v1_service = cdk_axum::create_mint_router(Arc::clone(&mint), cache_ttl, cache_tti).await?;
+    let v1_service =
+        cdk_axum::create_mint_router(Arc::clone(&mint), cache_ttl, cache_tti, None).await?;
 
     let mut mint_service = Router::new()
         .merge(v1_service)