mkv

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

server.rs (6862B)


      1 use axum::body::Bytes;
      2 use axum::extract::{Path, Query, State};
      3 use axum::http::{HeaderMap, StatusCode};
      4 use axum::response::{IntoResponse, Response};
      5 use std::sync::Arc;
      6 use std::time::Duration;
      7 
      8 use crate::db;
      9 use crate::error::{AppError, VolumeError};
     10 
     11 #[derive(Clone)]
     12 pub struct AppState {
     13     pub db: db::Db,
     14     pub volumes: Arc<Vec<String>>,
     15     pub replicas: usize,
     16     pub voltimeout: Duration,
     17     pub http: reqwest::Client,
     18 }
     19 
     20 #[derive(Debug, Clone, PartialEq, Eq)]
     21 pub enum ProbeResult {
     22     Found(String),
     23     AllFailed,
     24 }
     25 
     26 pub fn first_healthy_volume(key: &str, volumes: &[String], results: &[bool]) -> ProbeResult {
     27     for (vol, &healthy) in volumes.iter().zip(results) {
     28         if healthy {
     29             return ProbeResult::Found(format!("{vol}/{key}"));
     30         }
     31     }
     32     ProbeResult::AllFailed
     33 }
     34 
     35 pub fn shuffle_volumes(volumes: Vec<String>, seed: u64) -> Vec<String> {
     36     use rand::SeedableRng;
     37     use rand::seq::SliceRandom;
     38     let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
     39     let mut vols = volumes;
     40     vols.shuffle(&mut rng);
     41     vols
     42 }
     43 
     44 pub async fn get_key(
     45     State(state): State<AppState>,
     46     Path(key): Path<String>,
     47 ) -> Result<Response, AppError> {
     48     let record = state.db.get(&key).await?;
     49     if record.volumes.is_empty() {
     50         return Err(AppError::CorruptRecord { key });
     51     }
     52 
     53     let seed = std::time::SystemTime::now()
     54         .duration_since(std::time::UNIX_EPOCH)
     55         .map(|d| d.as_nanos() as u64)
     56         .unwrap_or(0);
     57     let volumes = shuffle_volumes(record.volumes, seed);
     58 
     59     let mut results = Vec::with_capacity(volumes.len());
     60     for vol in &volumes {
     61         let url = format!("{vol}/{key}");
     62         let healthy = match state.http.head(&url).timeout(state.voltimeout).send().await {
     63             Ok(resp) if resp.status().is_success() => true,
     64             Ok(resp) => {
     65                 tracing::warn!("volume {vol} returned {} for {key}", resp.status());
     66                 false
     67             }
     68             Err(e) => {
     69                 tracing::warn!("volume {vol} unreachable for {key}: {e}");
     70                 false
     71             }
     72         };
     73         results.push(healthy);
     74         if healthy {
     75             break;
     76         }
     77     }
     78 
     79     match first_healthy_volume(&key, &volumes, &results) {
     80         ProbeResult::Found(url) => {
     81             Ok((StatusCode::FOUND, [(axum::http::header::LOCATION, url)]).into_response())
     82         }
     83         ProbeResult::AllFailed => Err(AppError::AllVolumesUnreachable),
     84     }
     85 }
     86 
     87 pub async fn put_key(
     88     State(state): State<AppState>,
     89     Path(key): Path<String>,
     90     body: Bytes,
     91 ) -> Result<Response, AppError> {
     92     let target_volumes = crate::hasher::volumes_for_key(&key, &state.volumes, state.replicas);
     93     if target_volumes.len() < state.replicas {
     94         return Err(AppError::InsufficientVolumes {
     95             need: state.replicas,
     96             have: target_volumes.len(),
     97         });
     98     }
     99 
    100     let mut handles = Vec::with_capacity(target_volumes.len());
    101     for vol in &target_volumes {
    102         let url = format!("{vol}/{key}");
    103         let handle =
    104             tokio::spawn({
    105                 let client = state.http.clone();
    106                 let data = body.clone();
    107                 async move {
    108                     let resp = client.put(&url).body(data).send().await.map_err(|e| {
    109                         VolumeError::Request {
    110                             url: url.clone(),
    111                             source: e,
    112                         }
    113                     })?;
    114                     if !resp.status().is_success() {
    115                         return Err(VolumeError::BadStatus {
    116                             url,
    117                             status: resp.status(),
    118                         });
    119                     }
    120                     Ok(())
    121                 }
    122             });
    123         handles.push(handle);
    124     }
    125 
    126     let mut failed = false;
    127     for handle in handles {
    128         match handle.await {
    129             Ok(Err(e)) => {
    130                 tracing::error!("{e}");
    131                 failed = true;
    132             }
    133             Err(e) => {
    134                 tracing::error!("volume write task failed: {e}");
    135                 failed = true;
    136             }
    137             Ok(Ok(())) => {}
    138         }
    139     }
    140 
    141     if failed {
    142         // Rollback: best-effort delete from volumes
    143         for vol in &target_volumes {
    144             let _ = state.http.delete(format!("{vol}/{key}")).send().await;
    145         }
    146         return Err(AppError::PartialWrite);
    147     }
    148 
    149     let size = Some(body.len() as i64);
    150     if let Err(e) = state
    151         .db
    152         .put(key.clone(), target_volumes.clone(), size)
    153         .await
    154     {
    155         for vol in &target_volumes {
    156             let _ = state.http.delete(format!("{vol}/{key}")).send().await;
    157         }
    158         return Err(e);
    159     }
    160     Ok(StatusCode::CREATED.into_response())
    161 }
    162 
    163 pub async fn delete_key(
    164     State(state): State<AppState>,
    165     Path(key): Path<String>,
    166 ) -> Result<Response, AppError> {
    167     let record = state.db.get(&key).await?;
    168 
    169     let mut handles = Vec::new();
    170     for vol in &record.volumes {
    171         let url = format!("{vol}/{key}");
    172         let handle = tokio::spawn({
    173             let client = state.http.clone();
    174             async move {
    175                 let resp = client
    176                     .delete(&url)
    177                     .send()
    178                     .await
    179                     .map_err(|e| VolumeError::Request {
    180                         url: url.clone(),
    181                         source: e,
    182                     })?;
    183                 if !resp.status().is_success() {
    184                     return Err(VolumeError::BadStatus {
    185                         url,
    186                         status: resp.status(),
    187                     });
    188                 }
    189                 Ok(())
    190             }
    191         });
    192         handles.push(handle);
    193     }
    194     for handle in handles {
    195         match handle.await {
    196             Ok(Err(e)) => tracing::error!("{e}"),
    197             Err(e) => tracing::error!("volume delete task failed: {e}"),
    198             Ok(Ok(())) => {}
    199         }
    200     }
    201 
    202     state.db.delete(key).await?;
    203     Ok(StatusCode::NO_CONTENT.into_response())
    204 }
    205 
    206 pub async fn head_key(
    207     State(state): State<AppState>,
    208     Path(key): Path<String>,
    209 ) -> Result<Response, AppError> {
    210     let record = state.db.get(&key).await?;
    211     let mut headers = HeaderMap::new();
    212     if let Some(size) = record.size {
    213         headers.insert(
    214             axum::http::header::CONTENT_LENGTH,
    215             size.to_string().parse().unwrap(),
    216         );
    217     }
    218     Ok((StatusCode::OK, headers).into_response())
    219 }
    220 
    221 #[derive(serde::Deserialize)]
    222 pub struct ListQuery {
    223     #[serde(default)]
    224     pub prefix: String,
    225 }
    226 
    227 pub async fn list_keys(
    228     State(state): State<AppState>,
    229     Query(query): Query<ListQuery>,
    230 ) -> Result<Response, AppError> {
    231     let keys = state.db.list_keys(&query.prefix).await?;
    232     Ok((StatusCode::OK, keys.join("\n")).into_response())
    233 }