mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-07 20:47:46 +01:00
* sw-client: the filer and volume client library The client the Enterprise repository's VFS and vector engine use, with the same API: filer gRPC for entries, listings, volume locations, assignments and remote caching; volume HTTP, plain or TLS, for needle bytes; chunk resolution for sparse, overlapping and manifest chunks; a location cache; and the write side. Enterprise-only protocol fields (license, conditional mutations, hard links, KV listing) are left out. Compiled against weed/pb/filer.proto like the other worker crates. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * weed-lance-worker: reads straight from the filers and volume servers direct_read::DirectReads wraps the object store Lance opens so that entries, listings and bytes come from the filers (gRPC) and the volume servers (HTTP, signed with the read JWT), whatever the entry holds: inline content, chunk manifests, sparse or overlapping chunks, the filer's volume encryption, the gateway's SSE-S3 (sse.rs, with the KEK from security.toml or the filer) and objects tiered to remote storage. Writes still go to the inner store. The code and its tests are those of the Enterprise repository's engine. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * weed-vector: read tables from the filers and volume servers The engine asks the masters for the filers (and follows the list), takes jwt.signing.read.key, grpc.client, https.client and the SSE-S3 keys from security.toml, and opens every table with DirectReads innermost and the read cache outside it. --masters is required: the gateway finds the engine through them and the engine finds the filers. Writes still go to the gateway. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * sse: chunks out of a manifest keep their own SSE-S3 keys A chunk expanded from a manifest carries its id as fid, so keying the per-chunk key cache by file_id gave every such chunk the first one's key and IV. Keyed by the chunk's id however it is carried. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * direct reads: a silent filer is passed over like an unreachable one Each filer attempt is bounded, so a filer that accepts the connection and never answers does not hold the read or the startup lookup. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * sw-client: carry the assignment's fsync to the volume upload The filer's storage rule may want a needle on disk before the volume answers; the assignment says so and the upload now asks for it, as the Go uploader does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * weed-vector: argument tests pass the required masters; connect timeout for filers Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * direct reads: a failed read is tried once more against fresh locations A fetch that fails forgets the volume locations it was made against; the request now looks them up again and retries instead of failing on a volume that moved. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * direct reads: the filer deadline bounds a listing page, not the listing Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * weed-vector: security.toml booleans read as viper reads them Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * weed-vector: a filer the master lists badly does not hide the others Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * weed-vector: register with the masters only once ready to serve Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * sw-client: the HTTP transport asks the volume to fsync when the assignment does write_synced fell back to the trait default, which refuses fsync, so every upload under a fsync storage rule failed before it was sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sw-client: https.client.ca alone vouches for volume servers With a CA configured, reqwest still trusted the system roots next to it; weed processes trust only the configured pool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sw-client: an empty volume range is read without a request A zero length underflowed the Range end, and a range past u64::MAX overflowed it; the first now returns no bytes, the second an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sw-client: a whole-needle answer to a ranged read is cut to the range A server or proxy that ignores Range answers 200 with the whole needle; the read returned its first bytes in place of the ones asked for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sw-client: a read size past u64::MAX reads to the end of the file offset + size overflowed before the clamp to the file size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sw-client: an SSE-encrypted manifest is refused without a decoder Only filer-encrypted manifests were; an SSE one went to the protobuf decoder as ciphertext. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sw-client: an empty write leaves the staged file size alone A zero-byte write past the end grew the size to its offset, as a zero-byte pwrite does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sw-client: the chunk cache keeps no empty chunks They add no bytes, so nothing evicted them and the map grew with every distinct empty needle, even at capacity zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sw-client: flushing with a zero chunk size is an error, not a panic Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * weed-vector: an empty master list fails at startup "" split into one empty address that passed the check, and the engine then waited two minutes for a filer before a generic error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
130 lines
3.9 KiB
Rust
130 lines
3.9 KiB
Rust
//! A small fileId-keyed whole-chunk cache for read prefetch.
|
|
//!
|
|
//! SeaweedFS needles are immutable for a given fileId (`<vid>,<key><cookie>`),
|
|
//! so caching a chunk's bytes by fileId is coherence-safe — no invalidation is
|
|
//! ever needed, only capacity-bounded LRU eviction. This lets the read path
|
|
//! prefetch upcoming chunks in parallel (independent of the kernel's small,
|
|
//! single-chunk read upcalls) so sequential reads hit already-fetched bytes.
|
|
|
|
use std::collections::{HashMap, VecDeque};
|
|
use std::sync::Arc;
|
|
|
|
/// Bounded LRU of fileId -> whole-chunk bytes. Not thread-safe on its own; wrap
|
|
/// in a `Mutex`. All ops are O(cache size) at worst (the LRU order is a deque),
|
|
/// which is fine for the few-hundred-entry caches used here.
|
|
pub struct ChunkCache {
|
|
map: HashMap<String, Arc<Vec<u8>>>,
|
|
order: VecDeque<String>, // front = least-recently-used
|
|
bytes: usize,
|
|
cap: usize,
|
|
}
|
|
|
|
impl ChunkCache {
|
|
pub fn new(cap_bytes: usize) -> Self {
|
|
Self {
|
|
map: HashMap::new(),
|
|
order: VecDeque::new(),
|
|
bytes: 0,
|
|
cap: cap_bytes,
|
|
}
|
|
}
|
|
|
|
/// Fetch a cached chunk, marking it most-recently-used.
|
|
pub fn get(&mut self, file_id: &str) -> Option<Arc<Vec<u8>>> {
|
|
let v = self.map.get(file_id)?.clone();
|
|
if let Some(pos) = self.order.iter().position(|x| x == file_id) {
|
|
let f = self.order.remove(pos).unwrap();
|
|
self.order.push_back(f);
|
|
}
|
|
Some(v)
|
|
}
|
|
|
|
pub fn contains(&self, file_id: &str) -> bool {
|
|
self.map.contains_key(file_id)
|
|
}
|
|
|
|
/// Insert a chunk (no-op if empty, already present or larger than the whole
|
|
/// cap). Evicts least-recently-used entries until within capacity.
|
|
pub fn put(&mut self, file_id: String, data: Arc<Vec<u8>>) {
|
|
if self.map.contains_key(&file_id) {
|
|
return;
|
|
}
|
|
let len = data.len();
|
|
if len == 0 || len > self.cap {
|
|
return; // a single chunk bigger than the whole cache: don't churn
|
|
}
|
|
self.map.insert(file_id.clone(), data);
|
|
self.order.push_back(file_id);
|
|
self.bytes += len;
|
|
while self.bytes > self.cap {
|
|
let Some(old) = self.order.pop_front() else {
|
|
break;
|
|
};
|
|
if let Some(o) = self.map.remove(&old) {
|
|
self.bytes -= o.len();
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn len_bytes(&self) -> usize {
|
|
self.bytes
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn chunk(n: usize) -> Arc<Vec<u8>> {
|
|
Arc::new(vec![0u8; n])
|
|
}
|
|
|
|
#[test]
|
|
fn get_miss_then_hit() {
|
|
let mut c = ChunkCache::new(1024);
|
|
assert!(c.get("a").is_none());
|
|
c.put("a".into(), chunk(100));
|
|
assert_eq!(c.get("a").unwrap().len(), 100);
|
|
}
|
|
|
|
#[test]
|
|
fn put_is_idempotent_on_bytes() {
|
|
let mut c = ChunkCache::new(1024);
|
|
c.put("a".into(), chunk(100));
|
|
c.put("a".into(), chunk(100)); // duplicate ignored
|
|
assert_eq!(c.len_bytes(), 100);
|
|
}
|
|
|
|
#[test]
|
|
fn lru_evicts_oldest_first() {
|
|
let mut c = ChunkCache::new(250);
|
|
c.put("a".into(), chunk(100));
|
|
c.put("b".into(), chunk(100));
|
|
// touch a so b is now least-recently-used
|
|
assert!(c.get("a").is_some());
|
|
c.put("c".into(), chunk(100)); // bytes would be 300 > 250 -> evict LRU (b)
|
|
assert!(c.contains("a"));
|
|
assert!(!c.contains("b"));
|
|
assert!(c.contains("c"));
|
|
assert_eq!(c.len_bytes(), 200);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_chunks_are_not_cached() {
|
|
let mut c = ChunkCache::new(0);
|
|
for i in 0..10 {
|
|
c.put(i.to_string(), chunk(0));
|
|
}
|
|
assert!(c.map.is_empty() && c.order.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn oversized_chunk_not_cached() {
|
|
let mut c = ChunkCache::new(100);
|
|
c.put("big".into(), chunk(200));
|
|
assert!(!c.contains("big"));
|
|
assert_eq!(c.len_bytes(), 0);
|
|
}
|
|
}
|