Clean up
This commit is contained in:
parent
68ae92e4bf
commit
a6b584b6d3
4 changed files with 57 additions and 139 deletions
|
|
@ -35,14 +35,8 @@ fn pick_healthy_volume<'a>(
|
|||
.map(|v| v.as_str())
|
||||
}
|
||||
|
||||
/// Compute the placement plan for a PUT: which path and which volumes.
|
||||
struct PutPlan {
|
||||
path: String,
|
||||
target_volumes: Vec<String>,
|
||||
}
|
||||
|
||||
fn plan_put(ring: &Ring, key: &str, replication: usize) -> Result<PutPlan, AppError> {
|
||||
let path = Ring::key_path(key);
|
||||
/// Select target volumes for a key, ensuring we have enough for replication.
|
||||
fn select_volumes(ring: &Ring, key: &str, replication: usize) -> Result<Vec<String>, AppError> {
|
||||
let target_volumes = ring.get_volumes(key, replication);
|
||||
if target_volumes.len() < replication {
|
||||
return Err(AppError::VolumeError(format!(
|
||||
|
|
@ -50,15 +44,15 @@ fn plan_put(ring: &Ring, key: &str, replication: usize) -> Result<PutPlan, AppEr
|
|||
target_volumes.len()
|
||||
)));
|
||||
}
|
||||
Ok(PutPlan {
|
||||
path,
|
||||
target_volumes,
|
||||
})
|
||||
Ok(target_volumes)
|
||||
}
|
||||
|
||||
/// Evaluate fan-out PUT results. Returns the list of volumes that succeeded,
|
||||
/// or an error if any failed.
|
||||
fn evaluate_fanout(results: Vec<Result<(), String>>, volumes: &[String]) -> Result<Vec<String>, Vec<String>> {
|
||||
/// Evaluate fan-out results. Returns the list of volumes that succeeded,
|
||||
/// or the error messages if any failed.
|
||||
fn evaluate_fanout(
|
||||
results: Vec<Result<(), String>>,
|
||||
volumes: &[String],
|
||||
) -> Result<Vec<String>, Vec<String>> {
|
||||
let mut succeeded = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
for (i, result) in results.into_iter().enumerate() {
|
||||
|
|
@ -91,7 +85,7 @@ pub async fn get_key(
|
|||
|
||||
let healthy = state.healthy_volumes.read().await;
|
||||
let vol = pick_healthy_volume(&record.volumes, &healthy).ok_or(AppError::NoHealthyVolume)?;
|
||||
let location = format!("{vol}{}", record.path);
|
||||
let location = format!("{vol}/{key}");
|
||||
|
||||
Ok((
|
||||
StatusCode::FOUND,
|
||||
|
|
@ -106,21 +100,20 @@ pub async fn put_key(
|
|||
Path(key): Path<String>,
|
||||
body: Bytes,
|
||||
) -> Result<Response, AppError> {
|
||||
let plan = {
|
||||
let target_volumes = {
|
||||
let ring = state.ring.read().await;
|
||||
plan_put(&ring, &key, state.config.server.replication_factor)?
|
||||
select_volumes(&ring, &key, state.config.server.replication_factor)?
|
||||
};
|
||||
|
||||
// Fan out PUTs to all target volumes concurrently
|
||||
let mut handles = Vec::with_capacity(plan.target_volumes.len());
|
||||
for vol in &plan.target_volumes {
|
||||
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 = plan.path.clone();
|
||||
let key = key.clone();
|
||||
let data = body.clone();
|
||||
handles.push(tokio::spawn(async move {
|
||||
client.put(&vol, &path, &key, data).await
|
||||
client.put(&vol, &key, data).await
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -129,13 +122,10 @@ pub async fn put_key(
|
|||
results.push(handle.await.unwrap());
|
||||
}
|
||||
|
||||
match evaluate_fanout(results, &plan.target_volumes) {
|
||||
match evaluate_fanout(results, &target_volumes) {
|
||||
Ok(succeeded_volumes) => {
|
||||
let size = Some(body.len() as i64);
|
||||
state
|
||||
.writer
|
||||
.put(key, succeeded_volumes, plan.path, size)
|
||||
.await?;
|
||||
state.writer.put(key, succeeded_volumes, size).await?;
|
||||
Ok(StatusCode::CREATED.into_response())
|
||||
}
|
||||
Err(errors) => {
|
||||
|
|
@ -143,8 +133,8 @@ pub async fn put_key(
|
|||
tracing::error!("PUT to volume failed: {e}");
|
||||
}
|
||||
// Rollback: best-effort delete from any volumes that may have succeeded
|
||||
for vol in &plan.target_volumes {
|
||||
let _ = state.volume_client.delete(vol, &plan.path).await;
|
||||
for vol in &target_volumes {
|
||||
let _ = state.volume_client.delete(vol, &key).await;
|
||||
}
|
||||
Err(AppError::VolumeError(
|
||||
"not all volume writes succeeded".into(),
|
||||
|
|
@ -171,9 +161,9 @@ pub async fn delete_key(
|
|||
for vol in &record.volumes {
|
||||
let client = state.volume_client.clone();
|
||||
let vol = vol.clone();
|
||||
let path = record.path.clone();
|
||||
let key = key.clone();
|
||||
handles.push(tokio::spawn(
|
||||
async move { client.delete(&vol, &path).await },
|
||||
async move { client.delete(&vol, &key).await },
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -257,27 +247,24 @@ mod tests {
|
|||
let volumes = vec!["http://vol1".into(), "http://vol2".into()];
|
||||
let healthy: HashSet<String> = ["http://vol1".into(), "http://vol2".into()].into();
|
||||
|
||||
// Should return the first one
|
||||
assert_eq!(pick_healthy_volume(&volumes, &healthy), Some("http://vol1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plan_put_sufficient_volumes() {
|
||||
fn test_select_volumes_sufficient() {
|
||||
let volumes: Vec<String> = (1..=3).map(|i| format!("http://vol{i}")).collect();
|
||||
let ring = Ring::new(&volumes, 50);
|
||||
|
||||
let plan = plan_put(&ring, "test-key", 2).unwrap();
|
||||
assert_eq!(plan.target_volumes.len(), 2);
|
||||
assert!(!plan.path.is_empty());
|
||||
let selected = select_volumes(&ring, "test-key", 2).unwrap();
|
||||
assert_eq!(selected.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plan_put_insufficient_volumes() {
|
||||
fn test_select_volumes_insufficient() {
|
||||
let volumes: Vec<String> = vec!["http://vol1".into()];
|
||||
let ring = Ring::new(&volumes, 50);
|
||||
|
||||
let result = plan_put(&ring, "test-key", 2);
|
||||
assert!(result.is_err());
|
||||
assert!(select_volumes(&ring, "test-key", 2).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -285,8 +272,7 @@ mod tests {
|
|||
let volumes = vec!["http://vol1".into(), "http://vol2".into()];
|
||||
let results = vec![Ok(()), Ok(())];
|
||||
|
||||
let outcome = evaluate_fanout(results, &volumes);
|
||||
assert_eq!(outcome.unwrap(), volumes);
|
||||
assert_eq!(evaluate_fanout(results, &volumes).unwrap(), volumes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -294,10 +280,8 @@ mod tests {
|
|||
let volumes = vec!["http://vol1".into(), "http://vol2".into()];
|
||||
let results = vec![Ok(()), Err("connection refused".into())];
|
||||
|
||||
let outcome = evaluate_fanout(results, &volumes);
|
||||
let errors = outcome.unwrap_err();
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert_eq!(errors[0], "connection refused");
|
||||
let errors = evaluate_fanout(results, &volumes).unwrap_err();
|
||||
assert_eq!(errors, vec!["connection refused"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -305,7 +289,6 @@ mod tests {
|
|||
let volumes = vec!["http://vol1".into(), "http://vol2".into()];
|
||||
let results = vec![Err("err1".into()), Err("err2".into())];
|
||||
|
||||
let outcome = evaluate_fanout(results, &volumes);
|
||||
assert_eq!(outcome.unwrap_err().len(), 2);
|
||||
assert_eq!(evaluate_fanout(results, &volumes).unwrap_err().len(), 2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue