use std::sync::Arc;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::{get, post};
use axum::{Json, Router};
use dashmap::DashSet;
use serde::{Deserialize, Serialize};
use crate::protocols::SharedCacheHits;
pub const SHARED_KV_CACHE_QUERY_ENDPOINT: &str = "shared_kv_cache_query";
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SharedCacheQueryRequest {
pub block_hashes: Vec<u64>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SharedCacheQueryResponse {
pub ranges: Vec<[u32; 2]>,
}
#[cfg(feature = "indexer-runtime")]
impl crate::indexer::MaybeError for SharedCacheQueryResponse {
fn from_err(err: impl std::error::Error + 'static) -> Self {
tracing::warn!("SharedCacheQueryResponse::from_err: {err}");
Self { ranges: vec![] }
}
fn err(&self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
None
}
}
#[cfg(feature = "indexer-runtime")]
impl dynamo_runtime::protocols::maybe_error::MaybeError for SharedCacheQueryResponse {
fn from_err(err: impl std::error::Error + 'static) -> Self {
tracing::warn!("SharedCacheQueryResponse::from_err: {err}");
Self { ranges: vec![] }
}
fn err(&self) -> Option<dynamo_runtime::error::DynamoError> {
None
}
}
#[derive(Deserialize)]
pub struct StoreRequest {
pub block_hashes: Vec<u64>,
}
#[derive(Deserialize)]
pub struct RemoveRequest {
pub block_hashes: Vec<u64>,
}
pub struct SharedCacheStore {
blocks: DashSet<u64>,
}
impl SharedCacheStore {
pub fn new() -> Self {
Self {
blocks: DashSet::new(),
}
}
pub fn store(&self, hashes: &[u64]) {
for &h in hashes {
self.blocks.insert(h);
}
}
pub fn remove(&self, hashes: &[u64]) {
for &h in hashes {
self.blocks.remove(&h);
}
}
pub fn check_blocks(&self, block_hashes: &[u64]) -> SharedCacheHits {
let hits: Vec<bool> = block_hashes
.iter()
.map(|h| self.blocks.contains(h))
.collect();
SharedCacheHits::from_hits(&hits)
}
pub fn len(&self) -> usize {
self.blocks.len()
}
}
pub struct AppState {
pub store: Arc<SharedCacheStore>,
}
async fn check_blocks(
State(state): State<Arc<AppState>>,
Json(req): Json<SharedCacheQueryRequest>,
) -> impl IntoResponse {
let hits = state.store.check_blocks(&req.block_hashes);
let ranges: Vec<[u32; 2]> = hits.ranges.iter().map(|r| [r.start, r.end]).collect();
(StatusCode::OK, Json(SharedCacheQueryResponse { ranges }))
}
async fn store_blocks(
State(state): State<Arc<AppState>>,
Json(req): Json<StoreRequest>,
) -> impl IntoResponse {
let count = req.block_hashes.len();
state.store.store(&req.block_hashes);
(
StatusCode::CREATED,
Json(serde_json::json!({
"status": "ok",
"stored": count,
"total": state.store.len(),
})),
)
}
async fn remove_blocks(
State(state): State<Arc<AppState>>,
Json(req): Json<RemoveRequest>,
) -> impl IntoResponse {
let count = req.block_hashes.len();
state.store.remove(&req.block_hashes);
(
StatusCode::OK,
Json(serde_json::json!({
"status": "ok",
"removed": count,
"total": state.store.len(),
})),
)
}
async fn health() -> StatusCode {
StatusCode::OK
}
async fn stats(State(state): State<Arc<AppState>>) -> impl IntoResponse {
Json(serde_json::json!({
"total_blocks": state.store.len(),
}))
}
pub fn create_router(state: Arc<AppState>) -> Router {
Router::new()
.route("/check_blocks", post(check_blocks))
.route("/store", post(store_blocks))
.route("/remove", post(remove_blocks))
.route("/health", get(health))
.route("/stats", get(stats))
.with_state(state)
}
#[cfg(feature = "indexer-runtime")]
pub struct SharedCacheQueryEngine {
pub store: Arc<SharedCacheStore>,
}
#[cfg(feature = "indexer-runtime")]
#[dynamo_runtime::pipeline::async_trait]
impl
dynamo_runtime::pipeline::AsyncEngine<
dynamo_runtime::pipeline::SingleIn<SharedCacheQueryRequest>,
dynamo_runtime::pipeline::ManyOut<SharedCacheQueryResponse>,
anyhow::Error,
> for SharedCacheQueryEngine
{
async fn generate(
&self,
request: dynamo_runtime::pipeline::SingleIn<SharedCacheQueryRequest>,
) -> anyhow::Result<dynamo_runtime::pipeline::ManyOut<SharedCacheQueryResponse>> {
use dynamo_runtime::pipeline::{AsyncEngineContextProvider, ResponseStream};
let (req, ctx) = request.into_parts();
let hits = self.store.check_blocks(&req.block_hashes);
let ranges: Vec<[u32; 2]> = hits.ranges.iter().map(|r| [r.start, r.end]).collect();
let response = SharedCacheQueryResponse { ranges };
let stream = dynamo_runtime::stream::iter(vec![response]);
Ok(ResponseStream::new(Box::pin(stream), ctx.context()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_store_and_check() {
let store = SharedCacheStore::new();
store.store(&[100, 200, 300]);
let hits = store.check_blocks(&[100, 999, 200, 300, 888]);
assert_eq!(hits.total_hits, 3);
assert_eq!(hits.ranges, vec![0..1, 2..4]);
}
#[test]
fn test_check_empty_cache() {
let store = SharedCacheStore::new();
let hits = store.check_blocks(&[1, 2, 3]);
assert_eq!(hits.total_hits, 0);
assert!(hits.ranges.is_empty());
}
#[test]
fn test_remove_blocks() {
let store = SharedCacheStore::new();
store.store(&[10, 20, 30]);
store.remove(&[20]);
let hits = store.check_blocks(&[10, 20, 30]);
assert_eq!(hits.total_hits, 2);
assert_eq!(hits.ranges, vec![0..1, 2..3]);
}
#[test]
fn test_all_hits() {
let store = SharedCacheStore::new();
store.store(&[1, 2, 3]);
let hits = store.check_blocks(&[1, 2, 3]);
assert_eq!(hits.total_hits, 3);
assert_eq!(hits.ranges, vec![0..3]);
}
#[test]
fn test_store_len() {
let store = SharedCacheStore::new();
assert_eq!(store.len(), 0);
store.store(&[1, 2, 3]);
assert_eq!(store.len(), 3);
store.store(&[1, 4]); assert_eq!(store.len(), 4);
}
#[test]
fn test_response_wire_format() {
let hits = SharedCacheHits::from_ranges(vec![0..2, 5..8]);
let ranges: Vec<[u32; 2]> = hits.ranges.iter().map(|r| [r.start, r.end]).collect();
let resp = SharedCacheQueryResponse { ranges };
let json = serde_json::to_string(&resp).unwrap();
let parsed: SharedCacheQueryResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.ranges, vec![[0, 2], [5, 8]]);
}
}