Files
seaweedfs/seaweed-volume/src/storage/needle_map/compact_map.rs
T
adaf3534fa rust: clippy-clean both crates and adopt the std APIs the 1.91 MSRV allows (#11312)
* rust: apply clippy --fix to both crates

The mechanical part of a clippy sweep: `cargo clippy --all-targets --fix`
on seaweed-volume and the seaweed-worker workspace, hand-reviewed. Both
manifests declare their MSRV (1.91.1 and 1.94.1), so every suggestion
clippy applied is within it: the collapsible_if sites become let chains
(1.88, edition 2024), `% n == 0` becomes is_multiple_of (1.87),
chunks_exact with a constant becomes as_chunks (1.88), repeat().take()
becomes repeat_n (1.82), and io::Error::new(Other, ..) becomes
io::Error::other (1.74). The rest is redundant clones, borrows, casts,
closures and field names.

Nothing here changes behaviour. The three let_and_return sites in
needle_map.rs and store_ec.rs deserve a note: the `let result = ..;
result` shape was a deliberate edition-2021 workaround to drop a redb
guard before the table it borrows. Edition 2024 drops tail-expression
temporaries before locals, which is why clippy now flags it, and the
two comments that described the workaround say so instead.

Manual edits on top of the tool output: the blocks clippy rewrote are
re-indented the way rustfmt lays them out (only those blocks — the
crate is not rustfmt-clean and a whole-crate fmt would bury this diff),
the blank lines let_and_return left behind are removed, and the CRC
legacy_value test compares against a literal worked out from the
original shift formula rather than restating rotate_right.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CjZY429aVU74SLDmo1wiuU

* rust: clear the clippy warnings --fix cannot apply, and say why the rest stay

Hand fixes for the lints clippy only reports. Behaviour is unchanged
throughout; each rewrite is the one clippy names.

- needless_range_loop (7): index loops over shard vectors become
  iterator loops. Where the old code indexed `v[..n]` the new loop
  iterates `v[..n]` so an undersized vector still panics the same way.
- field_reassign_with_default (6): struct literals with `..Default`.
- redundant_pattern_matching (3): `if let Err(_) = guard.check()` becomes
  `.is_err()`, which also releases the read guard at the end of the
  condition instead of at the end of the block.
- manual_strip (2), manual_checked_ops, format_in_format_args,
  redundant_locals, wrong_self_convention (to_vif takes self by value,
  so it is into_vif; CompactEntry is Copy, so to_needle_value takes self).
- type_complexity (2): `OrphanShardLoad` and `RawNeedleEntry` name two
  tuples that were spelled out inline.
- new_without_default: CompactNeedleMap gets a Default that calls new().
- suspicious_open_options: a test helper spells out `.truncate(false)`,
  which is what `.create(true).write(true)` already did.

What stays, and the attribute that says so:

- too_many_arguments (10): `#[expect]` on each function. Folding 8–15
  parameters into a struct is a design change, not a lint fix.
- await_holding_lock / readonly_write_lock: one test holds the store
  write guard across a sleep on purpose, as a barrier that parks the
  copy task at the mount block. `#[expect(.., reason = ..)]` records it.
- module_inception: needle/needle.rs mirrors the Go package layout.

Two lints become crate-wide policy in `[lints.clippy]`, with the reason
next to each: result_large_err, because every RPC path returns
tonic::Status (176 bytes) and boxing it would change every handler
signature; and needless_update, because `..Default::default()` on a
protobuf message literal is what lets a proto gain a field without
touching every constructor (all 11 sites are pb messages). The worker
workspace gets the same table and its members opt in with
`lints.workspace = true`; its generated plugin.rs also allows
large_enum_variant on prost's oneof enums.

Both crates are now clean under `cargo clippy --all-targets -- -D warnings`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CjZY429aVU74SLDmo1wiuU

* rust volume: use the std APIs the 1.91 MSRV already pays for

The crate declares rust-version 1.91.1, so a few things the code still
worked around are plain std now. All of them come from the 1.85–1.91
release notes; nothing here needs a newer toolchain than the manifest
already requires.

- std::sync::LazyLock (1.80) replaces the lazy_static! block in
  metrics.rs, and the lazy_static dependency goes. Every use site reads
  the same through Deref, so no caller changes.
- Duration::from_mins / from_hours (1.91) replace `from_secs(v * 60)`
  and `from_secs(v * 3600)` in the option parser and the shard-location
  refresh TTLs. One difference for the parser: an absurd count that
  overflows u64 seconds now panics in release builds too, where the
  multiplication used to wrap.
- Result::flatten (1.89) replaces `.and_then(|r| r)` on the replication
  join handle.
- OsStr::display (1.87) replaces `to_string_lossy()` where the name was
  only being formatted; the output is byte-identical.
- `#[allow]` becomes `#[expect]` (1.81) on the suppressions that are
  meant to be permanent, so a suppression that stops being needed
  becomes a warning rather than lingering. Doing that found four that
  already had: dead_code on ChunkManifest, base_name and last_io_error,
  and too_many_arguments on read_from_data_shards, which is down to
  seven parameters. Those attributes are deleted. The three allows that
  depend on cfg (a unix-only mutation, a linux-only field set, a
  profiling-only parameter) stay as allow, because expect would be
  unfulfilled on the other platforms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CjZY429aVU74SLDmo1wiuU

* ci: add a commented-out clippy step to both Rust workflows

Both crates are warning-free under `cargo clippy --all-targets
-D warnings` now. Whether that becomes a gate is a policy call, so the
step is present but commented out; uncommenting it is the whole change.
The comment points at the `[lints.clippy]` table where crate-wide
exceptions are recorded, so the gate does not become a reason to
sprinkle allows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CjZY429aVU74SLDmo1wiuU

* rust volume: guard parse_duration against overflow panics

Duration::from_mins/from_hours panic when the count overflows u64
seconds. Use checked_mul so an oversized CLI value falls back to the
parser default instead of crashing volume startup.

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Chris Lu <chris.lu@gmail.com>
2026-09-14 11:29:29 -07:00

376 lines
11 KiB
Rust

//! CompactMap: memory-efficient in-memory map of NeedleId -> (Offset, Size).
//!
//! Port of Go's CompactMap from weed/storage/needle_map/compact_map.go.
//! Uses segmented sorted arrays with compressed keys (u16 instead of u64)
//! to achieve ~10 bytes per entry instead of ~40-48 bytes with HashMap.
//!
//! NeedleId is split into: chunk = id / SEGMENT_CHUNK_SIZE, compact_key = id % SEGMENT_CHUNK_SIZE.
//! Each segment stores up to SEGMENT_CHUNK_SIZE entries in a sorted Vec, searched via binary search.
//! Best case (ordered inserts): O(1). Worst case: O(log n) per segment.
use std::collections::HashMap;
use super::NeedleValue;
use crate::storage::types::*;
/// Maximum entries per segment. Must be <= u16::MAX (65535).
const SEGMENT_CHUNK_SIZE: u64 = 50_000;
/// Compact key: only the low bits of NeedleId within a segment.
type CompactKey = u16;
/// Segment chunk identifier: NeedleId / SEGMENT_CHUNK_SIZE.
type Chunk = u64;
/// Compact entry: 10 bytes (2 + 4 + 4) vs 16 bytes for full NeedleId + NeedleValue.
#[derive(Clone, Copy)]
struct CompactEntry {
key: CompactKey, // 2 bytes
offset: [u8; OFFSET_SIZE], // 4 bytes
size: Size, // 4 bytes
}
impl CompactEntry {
fn to_needle_value(self) -> NeedleValue {
NeedleValue {
offset: Offset::from_bytes(&self.offset),
size: self.size,
}
}
}
/// A sorted segment of compact entries for a given chunk.
struct Segment {
list: Vec<CompactEntry>,
chunk: Chunk,
first_key: CompactKey,
last_key: CompactKey,
}
impl Segment {
fn new(chunk: Chunk) -> Self {
Segment {
list: Vec::new(),
chunk,
first_key: u16::MAX,
last_key: 0,
}
}
fn compact_key(&self, id: NeedleId) -> CompactKey {
(id.0 - SEGMENT_CHUNK_SIZE * self.chunk) as CompactKey
}
/// Binary search for a compact key. Returns (index, found).
/// If not found, index is the insertion point.
fn bsearch(&self, id: NeedleId) -> (usize, bool) {
let ck = self.compact_key(id);
if self.list.is_empty() {
return (0, false);
}
if ck == self.first_key {
return (0, true);
}
if ck < self.first_key {
return (0, false);
}
if ck == self.last_key {
return (self.list.len() - 1, true);
}
if ck > self.last_key {
return (self.list.len(), false);
}
let i = self.list.partition_point(|e| e.key < ck);
if i < self.list.len() && self.list[i].key == ck {
(i, true)
} else {
(i, false)
}
}
/// Insert or update. Returns old NeedleValue if updating.
fn set(&mut self, id: NeedleId, offset: Offset, size: Size) -> Option<NeedleValue> {
let (i, found) = self.bsearch(id);
if found {
let old = self.list[i].to_needle_value();
let mut offset_bytes = [0u8; OFFSET_SIZE];
offset.to_bytes(&mut offset_bytes);
self.list[i].offset = offset_bytes;
self.list[i].size = size;
return Some(old);
}
// Insert at sorted position
let ck = self.compact_key(id);
let mut offset_bytes = [0u8; OFFSET_SIZE];
offset.to_bytes(&mut offset_bytes);
let entry = CompactEntry {
key: ck,
offset: offset_bytes,
size,
};
// Match Go panic: don't exceed segment capacity
if self.list.len() >= SEGMENT_CHUNK_SIZE as usize {
panic!(
"attempted to write more than {} entries on CompactMapSegment",
SEGMENT_CHUNK_SIZE
);
}
if self.list.len() == SEGMENT_CHUNK_SIZE as usize - 1 {
// Pin capacity to exact size when maxing out
let mut new_list = Vec::with_capacity(SEGMENT_CHUNK_SIZE as usize);
new_list.extend_from_slice(&self.list[..i]);
new_list.push(entry);
new_list.extend_from_slice(&self.list[i..]);
self.list = new_list;
} else {
self.list.insert(i, entry);
}
if ck < self.first_key {
self.first_key = ck;
}
if ck > self.last_key {
self.last_key = ck;
}
None
}
fn get(&self, id: NeedleId) -> Option<NeedleValue> {
let (i, found) = self.bsearch(id);
if found {
Some(self.list[i].to_needle_value())
} else {
None
}
}
/// Mark as deleted by negating size. Returns previous size if not already deleted.
/// Matches Go behavior: checks !IsDeleted() (i.e., size >= 0).
fn delete(&mut self, id: NeedleId) -> Option<Size> {
let (i, found) = self.bsearch(id);
if found && !self.list[i].size.is_deleted() {
let old_size = self.list[i].size;
if self.list[i].size.0 == 0 {
self.list[i].size = TOMBSTONE_FILE_SIZE;
} else {
self.list[i].size = Size(-self.list[i].size.0);
}
Some(old_size)
} else {
None
}
}
}
/// Memory-efficient map of NeedleId -> (Offset, Size).
/// Segments NeedleIds into chunks of 50,000 and stores compact 10-byte entries
/// in sorted arrays, using only 2 bytes for the key within each segment.
pub struct CompactMap {
segments: HashMap<Chunk, Segment>,
}
impl CompactMap {
pub fn new() -> Self {
CompactMap {
segments: HashMap::new(),
}
}
fn _segment_for_key(&mut self, id: NeedleId) -> &mut Segment {
let chunk = id.0 / SEGMENT_CHUNK_SIZE;
self.segments
.entry(chunk)
.or_insert_with(|| Segment::new(chunk))
}
/// Insert or update. Returns old NeedleValue if updating.
pub fn set(&mut self, id: NeedleId, offset: Offset, size: Size) -> Option<NeedleValue> {
let chunk = id.0 / SEGMENT_CHUNK_SIZE;
let segment = self
.segments
.entry(chunk)
.or_insert_with(|| Segment::new(chunk));
segment.set(id, offset, size)
}
pub fn get(&self, id: NeedleId) -> Option<NeedleValue> {
let chunk = id.0 / SEGMENT_CHUNK_SIZE;
self.segments.get(&chunk)?.get(id)
}
/// Mark as deleted. Returns previous size if was valid.
pub fn delete(&mut self, id: NeedleId) -> Option<Size> {
let chunk = id.0 / SEGMENT_CHUNK_SIZE;
self.segments.get_mut(&chunk)?.delete(id)
}
/// Remove entry entirely (used during idx loading).
pub fn remove(&mut self, id: NeedleId) -> Option<NeedleValue> {
let chunk = id.0 / SEGMENT_CHUNK_SIZE;
let segment = self.segments.get_mut(&chunk)?;
let (i, found) = segment.bsearch(id);
if found {
let entry = segment.list.remove(i);
// Update first/last keys
if segment.list.is_empty() {
segment.first_key = u16::MAX;
segment.last_key = 0;
} else {
segment.first_key = segment.list[0].key;
segment.last_key = segment.list[segment.list.len() - 1].key;
}
Some(entry.to_needle_value())
} else {
None
}
}
/// Iterate all entries in ascending NeedleId order.
pub fn ascending_visit<F, E>(&self, mut f: F) -> Result<(), E>
where
F: FnMut(NeedleId, &NeedleValue) -> Result<(), E>,
{
let mut chunks: Vec<Chunk> = self.segments.keys().copied().collect();
chunks.sort_unstable();
for chunk in chunks {
let segment = &self.segments[&chunk];
for entry in &segment.list {
let id = NeedleId(SEGMENT_CHUNK_SIZE * segment.chunk + entry.key as u64);
let nv = entry.to_needle_value();
f(id, &nv)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn offset(v: u32) -> Offset {
Offset::from_actual_offset(v as i64 * NEEDLE_PADDING_SIZE as i64)
}
#[test]
fn test_compact_map_basic() {
let mut m = CompactMap::new();
// Insert
assert!(m.set(NeedleId(1), offset(100), Size(50)).is_none());
assert!(m.set(NeedleId(2), offset(200), Size(60)).is_none());
// Get
let nv = m.get(NeedleId(1)).unwrap();
assert_eq!(nv.size, Size(50));
// Update returns old value
let old = m.set(NeedleId(1), offset(300), Size(70)).unwrap();
assert_eq!(old.size, Size(50));
// Get updated value
let nv = m.get(NeedleId(1)).unwrap();
assert_eq!(nv.size, Size(70));
// Miss
assert!(m.get(NeedleId(999)).is_none());
}
#[test]
fn test_compact_map_delete() {
let mut m = CompactMap::new();
m.set(NeedleId(1), offset(100), Size(50));
// Delete returns old size
let old = m.delete(NeedleId(1)).unwrap();
assert_eq!(old, Size(50));
// Get returns deleted (negative size)
let nv = m.get(NeedleId(1)).unwrap();
assert!(nv.size.is_deleted());
// Delete again returns None (already deleted)
assert!(m.delete(NeedleId(1)).is_none());
}
#[test]
fn test_compact_map_zero_size_delete() {
let mut m = CompactMap::new();
m.set(NeedleId(1), offset(100), Size(0));
let old = m.delete(NeedleId(1)).unwrap();
assert_eq!(old, Size(0));
let nv = m.get(NeedleId(1)).unwrap();
assert_eq!(nv.size, TOMBSTONE_FILE_SIZE);
}
#[test]
fn test_compact_map_cross_segment() {
let mut m = CompactMap::new();
// Insert across multiple segments
m.set(NeedleId(1), offset(1), Size(1));
m.set(NeedleId(50_000), offset(2), Size(2));
m.set(NeedleId(100_000), offset(3), Size(3));
assert_eq!(m.get(NeedleId(1)).unwrap().size, Size(1));
assert_eq!(m.get(NeedleId(50_000)).unwrap().size, Size(2));
assert_eq!(m.get(NeedleId(100_000)).unwrap().size, Size(3));
}
#[test]
fn test_compact_map_ascending_visit() {
let mut m = CompactMap::new();
m.set(NeedleId(100_005), offset(3), Size(3));
m.set(NeedleId(5), offset(1), Size(1));
m.set(NeedleId(50_005), offset(2), Size(2));
let mut visited = Vec::new();
m.ascending_visit(|id, nv| {
visited.push((id, nv.size));
Ok::<_, String>(())
})
.unwrap();
assert_eq!(visited.len(), 3);
assert_eq!(visited[0].0, NeedleId(5));
assert_eq!(visited[1].0, NeedleId(50_005));
assert_eq!(visited[2].0, NeedleId(100_005));
}
#[test]
fn test_compact_map_remove() {
let mut m = CompactMap::new();
m.set(NeedleId(1), offset(100), Size(50));
m.set(NeedleId(2), offset(200), Size(60));
let removed = m.remove(NeedleId(1)).unwrap();
assert_eq!(removed.size, Size(50));
assert!(m.get(NeedleId(1)).is_none());
assert_eq!(m.get(NeedleId(2)).unwrap().size, Size(60));
}
#[test]
fn test_compact_map_reverse_insert_order() {
let mut m = CompactMap::new();
// Insert in reverse order to test sorted insert
for i in (0..100).rev() {
m.set(NeedleId(i), offset(i as u32), Size(i as i32));
}
for i in 0..100 {
assert_eq!(m.get(NeedleId(i)).unwrap().size, Size(i as i32));
}
}
}