This commit is contained in:
Silas Brack 2026-03-07 09:53:24 +01:00
parent 8d32777f9f
commit 2a2afa5f69
7 changed files with 534 additions and 12 deletions

197
src/server.rs Normal file
View file

@ -0,0 +1,197 @@
use axum::body::Bytes;
use axum::extract::{Path, Query, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use std::collections::HashSet;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::config::Config;
use crate::db;
use crate::error::AppError;
use crate::hasher::Ring;
use crate::volume::VolumeClient;
#[derive(Clone)]
pub struct AppState {
pub writer: db::WriterHandle,
pub reads: db::ReadPool,
pub ring: Arc<RwLock<Ring>>,
pub volume_client: VolumeClient,
pub healthy_volumes: Arc<RwLock<HashSet<String>>>,
pub config: Arc<Config>,
}
/// GET /:key — look up key, redirect to a healthy volume
pub async fn get_key(
State(state): State<AppState>,
Path(key): Path<String>,
) -> Result<Response, AppError> {
let record = state
.reads
.query({
let key = key.clone();
move |conn| db::get(conn, &key)
})
.await?;
let healthy = state.healthy_volumes.read().await;
// Pick the first healthy volume
for vol in &record.volumes {
if healthy.contains(vol) {
let location = format!("{}{}", vol, record.path);
return Ok((
StatusCode::FOUND,
[(axum::http::header::LOCATION, location)],
)
.into_response());
}
}
Err(AppError::NoHealthyVolume)
}
/// PUT /:key — store blob on volumes, record in index
pub async fn put_key(
State(state): State<AppState>,
Path(key): Path<String>,
body: Bytes,
) -> Result<Response, AppError> {
let replication = state.config.server.replication_factor;
let path = Ring::key_path(&key);
let target_volumes = {
let ring = state.ring.read().await;
ring.get_volumes(&key, replication)
};
if target_volumes.len() < replication {
return Err(AppError::VolumeError(format!(
"need {replication} volumes but only {} available",
target_volumes.len()
)));
}
// Fan out PUTs to all target volumes concurrently
let mut handles = Vec::with_capacity(target_volumes.len());
for vol in &target_volumes {
let client = state.volume_client.clone();
let vol = vol.clone();
let path = path.clone();
let key = key.clone();
let data = body.clone();
handles.push(tokio::spawn(async move {
client.put(&vol, &path, &key, data).await
}));
}
let mut succeeded = Vec::new();
let mut failed = false;
for (i, handle) in handles.into_iter().enumerate() {
match handle.await.unwrap() {
Ok(()) => succeeded.push(target_volumes[i].clone()),
Err(e) => {
tracing::error!("PUT to volume failed: {e}");
failed = true;
}
}
}
if failed {
// Rollback: delete from volumes that succeeded
for vol in &succeeded {
if let Err(e) = state.volume_client.delete(vol, &path).await {
tracing::error!("Rollback DELETE failed: {e}");
}
}
return Err(AppError::VolumeError(
"not all volume writes succeeded".into(),
));
}
let size = Some(body.len() as i64);
state
.writer
.put(key, target_volumes, path, size)
.await?;
Ok(StatusCode::CREATED.into_response())
}
/// DELETE /:key — remove from volumes and index
pub async fn delete_key(
State(state): State<AppState>,
Path(key): Path<String>,
) -> Result<Response, AppError> {
let record = state
.reads
.query({
let key = key.clone();
move |conn| db::get(conn, &key)
})
.await?;
// Fan out DELETEs concurrently
let mut handles = Vec::new();
for vol in &record.volumes {
let client = state.volume_client.clone();
let vol = vol.clone();
let path = record.path.clone();
handles.push(tokio::spawn(
async move { client.delete(&vol, &path).await },
));
}
for handle in handles {
if let Err(e) = handle.await.unwrap() {
tracing::error!("DELETE from volume failed: {e}");
}
}
// Remove from index regardless of volume DELETE results
state.writer.delete(key).await?;
Ok(StatusCode::NO_CONTENT.into_response())
}
/// HEAD /:key — check if key exists, return size
pub async fn head_key(
State(state): State<AppState>,
Path(key): Path<String>,
) -> Result<Response, AppError> {
let record = state
.reads
.query(move |conn| db::get(conn, &key))
.await?;
let mut headers = HeaderMap::new();
if let Some(size) = record.size {
headers.insert(
axum::http::header::CONTENT_LENGTH,
size.to_string().parse().unwrap(),
);
}
Ok((StatusCode::OK, headers).into_response())
}
#[derive(serde::Deserialize)]
pub struct ListQuery {
#[serde(default)]
pub prefix: String,
}
/// GET / — list keys with optional prefix filter
pub async fn list_keys(
State(state): State<AppState>,
Query(query): Query<ListQuery>,
) -> Result<Response, AppError> {
let keys = state
.reads
.query(move |conn| db::list_keys(conn, &query.prefix))
.await?;
let body = keys.join("\n");
Ok((StatusCode::OK, body).into_response())
}