Skip to content

Commit ff3fe59

Browse files
committed
Add HTTP layer
1 parent 34ce5e7 commit ff3fe59

10 files changed

Lines changed: 1840 additions & 3 deletions

File tree

Cargo.lock

Lines changed: 570 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ keywords = ["storage", "distributed", "leveldb", "object-store"]
1414
categories = ["database", "network-programming"]
1515

1616
[workspace.dependencies]
17+
minikv-core = { path = "minikv-core" }
18+
1719
thiserror = "2"
1820
tracing = "0.1"
1921

@@ -26,8 +28,9 @@ dashmap = "6.1"
2628
tokio = { version = "1.49", default-features = false }
2729
reqwest = { version = "0.13", default-features = false }
2830
bytes = { version = "1.11" }
29-
30-
# Chosen over `leveldb` crate which requires C++ LevelDB via FFI.
31+
uuid = { version = "1", features = ["v4"] }
32+
rand = "0.8"
33+
futures = "0.3"
3134
rusty-leveldb = "1"
3235

3336
# Testing

minikv/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,18 @@ keywords.workspace = true
1111
categories.workspace = true
1212

1313
[dependencies]
14+
minikv-core = { workspace = true }
15+
tracing = { workspace = true }
16+
dashmap = { workspace = true }
17+
thiserror = { workspace = true }
18+
bytes = { workspace = true }
19+
serde = { workspace = true, features = ["derive"] }
20+
serde_json = { workspace = true }
21+
futures = { workspace = true }
22+
rand = { workspace = true }
23+
uuid = { workspace = true }
24+
tokio = { workspace = true, features = ["full"] }
25+
clap = { version = "4", features = ["derive", "env"] }
26+
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
27+
axum = { version = "0.7", features = [] }
28+
quick-xml = { version = "0.31", features = ["serialize"] }

minikv/src/cli.rs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/// CLI definition for `minikv`.
2+
///
3+
/// # Subcommands
4+
/// - `server` run the HTTP metadata coordinator
5+
/// - `rebuild` reconstruct LevelDB from volume server autoindex
6+
/// - `rebalance` move all keys to their ideal volume set
7+
/// - `print-nginx-config` emit the nginx volume server config to stdout
8+
use std::path::PathBuf;
9+
use std::time::Duration;
10+
11+
use clap::{Args, Parser, Subcommand};
12+
13+
#[derive(Parser, Debug)]
14+
#[command(
15+
name = "minikv",
16+
version,
17+
about = "Distributed object storage coordinator",
18+
long_about = None,
19+
)]
20+
pub struct Cli {
21+
#[command(subcommand)]
22+
pub command: Command,
23+
}
24+
25+
#[derive(Subcommand, Debug)]
26+
pub enum Command {
27+
/// Run the HTTP metadata coordinator server.
28+
Server(ServerArgs),
29+
30+
/// Rebuild LevelDB metadata from volume server autoindex listings.
31+
///
32+
/// WARNING: This clears the entire DB before scanning.
33+
Rebuild(CommonArgs),
34+
35+
/// Rebalance all keys to their ideal volume set.
36+
Rebalance(CommonArgs),
37+
38+
/// Print the nginx volume server configuration to stdout.
39+
PrintNginxConfig,
40+
}
41+
42+
/// Arguments shared by server, rebuild, and rebalance.
43+
#[derive(Args, Debug, Clone)]
44+
pub struct CommonArgs {
45+
/// Path to the LevelDB database directory.
46+
#[arg(long, env = "MINIKV_DB", required = true)]
47+
pub db: PathBuf,
48+
49+
/// Comma-separated list of volume server addresses (host:port).
50+
#[arg(long, env = "MINIKV_VOLUMES", required = true, value_delimiter = ',')]
51+
pub volumes: Vec<String>,
52+
53+
/// Number of replicas to maintain per object.
54+
#[arg(long, env = "MINIKV_REPLICAS", default_value = "3")]
55+
pub replicas: usize,
56+
57+
/// Number of sub-volume shards per volume server.
58+
/// Use 1 to disable sub-volume path components.
59+
#[arg(long, env = "MINIKV_SUBVOLUMES", default_value = "10")]
60+
pub subvolumes: usize,
61+
62+
/// Timeout for HEAD probes to volume servers (e.g. "1s", "500ms").
63+
#[arg(long, env = "MINIKV_VOLTIMEOUT", default_value = "1s", value_parser = parse_duration)]
64+
pub voltimeout: Duration,
65+
}
66+
67+
/// Additional arguments for the `server` subcommand.
68+
#[derive(Args, Debug)]
69+
pub struct ServerArgs {
70+
#[command(flatten)]
71+
pub common: CommonArgs,
72+
73+
/// Port to listen on.
74+
#[arg(long, env = "MINIKV_PORT", default_value = "3000")]
75+
pub port: u16,
76+
77+
/// Optional fallback server for keys missing from all volumes.
78+
#[arg(long, env = "MINIKV_FALLBACK")]
79+
pub fallback: Option<String>,
80+
81+
/// Public-facing addresses for each volume server, used in Location
82+
/// redirect headers returned to clients.
83+
///
84+
/// Must be the same length as --volumes and in the same order.
85+
/// Each entry is the externally-reachable host:port for the corresponding
86+
/// --volumes entry.
87+
///
88+
/// Example:
89+
/// --volumes=volume1:8080,volume2:8080,volume3:8080
90+
/// --public-volumes=localhost:8001,localhost:8002,localhost:8003
91+
///
92+
/// When omitted, --volumes addresses are used as-is in Location headers
93+
/// (correct for bare-metal deployments where internal == external).
94+
#[arg(
95+
long,
96+
env = "MINIKV_PUBLIC_VOLUMES",
97+
value_delimiter = ',',
98+
requires = "volumes"
99+
)]
100+
pub public_volumes: Option<Vec<String>>,
101+
102+
/// Require UNLINK (soft-delete) before a hard DELETE is allowed.
103+
#[arg(long, env = "MINIKV_PROTECT", default_value = "false")]
104+
pub protect: bool,
105+
106+
/// Compute and store a BLAKE3 checksum for every uploaded object.
107+
#[arg(long, env = "MINIKV_CHECKSUM", default_value = "true")]
108+
pub checksum: bool,
109+
110+
/// Enable X-Accel-Redirect mode for GET/HEAD responses.
111+
///
112+
/// When enabled, the coordinator returns `X-Accel-Redirect: /accel/<host>/<path>`
113+
/// instead of `302 Location`. A frontend nginx must be configured with:
114+
///
115+
/// proxy_pass http://coordinator:3000;
116+
/// location ~ ^/accel/([^/]+)/(.+)$ {
117+
/// internal;
118+
/// proxy_pass http://$1/$2;
119+
/// }
120+
///
121+
/// This allows nginx to stream the object body while the coordinator controls
122+
/// all response headers, including Content-Type from stored object metadata.
123+
///
124+
/// When disabled (default), GET/HEAD returns a standard 302 redirect.
125+
#[arg(long, env = "MINIKV_ACCEL_REDIRECT", default_value = "false")]
126+
pub accel_redirect: bool,
127+
128+
/// Enable verbose structured logging.
129+
#[arg(short, long, env = "MINIKV_VERBOSE", default_value = "false")]
130+
pub verbose: bool,
131+
}
132+
133+
/// Parse a human-friendly duration string such as "1s" or "500ms".
134+
fn parse_duration(s: &str) -> Result<Duration, String> {
135+
if let Some(ms) = s.strip_suffix("ms") {
136+
ms.parse::<u64>()
137+
.map(Duration::from_millis)
138+
.map_err(|e| e.to_string())
139+
} else if let Some(secs) = s.strip_suffix('s') {
140+
secs.parse::<u64>()
141+
.map(Duration::from_secs)
142+
.map_err(|e| e.to_string())
143+
} else {
144+
// Fall back: treat as integer milliseconds.
145+
s.parse::<u64>()
146+
.map(Duration::from_millis)
147+
.map_err(|_| format!("invalid duration '{s}'. Use '1s' or '500ms'"))
148+
}
149+
}
150+
151+
/// Validate common arguments and panic with a clear message on bad input.
152+
pub fn validate_common(args: &CommonArgs) {
153+
if args.volumes.is_empty() {
154+
eprintln!("error: --volumes must contain at least one volume server");
155+
std::process::exit(1);
156+
}
157+
if args.replicas == 0 {
158+
eprintln!("error: --replicas must be ≥ 1");
159+
std::process::exit(1);
160+
}
161+
if args.volumes.len() < args.replicas {
162+
eprintln!(
163+
"error: need at least as many volumes ({}) as replicas ({})",
164+
args.volumes.len(),
165+
args.replicas
166+
);
167+
std::process::exit(1);
168+
}
169+
if args.subvolumes == 0 {
170+
eprintln!("error: --subvolumes must be ≥ 1");
171+
std::process::exit(1);
172+
}
173+
}
174+
175+
/// Validate server-only arguments.
176+
#[allow(unused)]
177+
pub fn validate_server(args: &ServerArgs) {
178+
validate_common(&args.common);
179+
if let Some(ref pv) = args.public_volumes
180+
&& pv.len() != args.common.volumes.len()
181+
{
182+
eprintln!(
183+
"error: --public-volumes has {} entries but --volumes has {}; they must match 1:1",
184+
pv.len(),
185+
args.common.volumes.len()
186+
);
187+
std::process::exit(1);
188+
}
189+
}
190+
191+
#[cfg(test)]
192+
mod tests {
193+
use super::*;
194+
195+
#[test]
196+
fn parse_duration_seconds() {
197+
assert_eq!(parse_duration("1s").unwrap(), Duration::from_secs(1));
198+
assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
199+
}
200+
201+
#[test]
202+
fn parse_duration_millis() {
203+
assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
204+
}
205+
206+
#[test]
207+
fn parse_duration_invalid() {
208+
assert!(parse_duration("1h").is_err());
209+
}
210+
}

minikv/src/error.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// Unified error type for minikv.
2+
#[derive(Debug, thiserror::Error)]
3+
pub enum Error {
4+
/// Standard I/O errors (temp file handling for multipart uploads).
5+
#[error("I/O error: {0}")]
6+
Io(#[from] std::io::Error),
7+
8+
/// XML deserialization errors for S3-compatible API bodies.
9+
#[error("XML parse error: {0}")]
10+
XmlParse(String),
11+
12+
/// Minikv core library error
13+
#[error(transparent)]
14+
Core(#[from] minikv_core::Error),
15+
}

0 commit comments

Comments
 (0)