use std::collections::HashMap;
use std::time::{Duration, Instant, SystemTime};
use nfs3_server::nfs3_types::nfs3::nfsstat3;
pub const MAX_IMAGE_LEN: usize = 64 * 1024 * 1024;
const MAX_IMAGES: usize = 1024;
const MAX_TOTAL_LEN: usize = 128 * 1024 * 1024;
pub const CLEAN_TTL: Duration = Duration::from_secs(5);
pub const PENDING_TTL: Duration = Duration::from_secs(600);
#[derive(Debug, Clone)]
pub struct Image {
pub bytes: Vec<u8>,
pub mode: Option<u32>,
pub modified: SystemTime,
pub dirty: bool,
pub generation: u64,
pub tried: Option<u64>,
pub merge: bool,
last_used: Instant,
}
#[derive(Debug, Clone, Copy)]
pub struct Facts {
pub len: u64,
pub modified: SystemTime,
pub mode: Option<u32>,
}
impl Image {
pub fn synthesized(bytes: Vec<u8>, modified: SystemTime, now: Instant) -> Self {
Self {
bytes,
mode: None,
modified,
dirty: false,
generation: 0,
tried: None,
merge: false,
last_used: now,
}
}
pub fn pending(bytes: Vec<u8>, now: Instant) -> Self {
Self {
bytes,
mode: None,
modified: SystemTime::now(),
dirty: true,
generation: 0,
tried: None,
merge: true,
last_used: now,
}
}
pub fn fresh(now: Instant) -> Self {
Self::pending(Vec::new(), now)
}
pub fn facts(&self) -> Facts {
Facts {
len: self.bytes.len() as u64,
modified: self.modified,
mode: self.mode,
}
}
fn expired(&self, now: Instant) -> bool {
let ttl = if self.dirty { PENDING_TTL } else { CLEAN_TTL };
now.saturating_duration_since(self.last_used) >= ttl
}
}
#[derive(Debug)]
pub struct Images {
by_id: HashMap<u64, Image>,
total_len: usize,
max_total_len: usize,
}
impl Default for Images {
fn default() -> Self {
Self {
by_id: HashMap::new(),
total_len: 0,
max_total_len: MAX_TOTAL_LEN,
}
}
}
impl Images {
pub fn contains(&self, id: u64) -> bool {
self.by_id.contains_key(&id)
}
pub fn get(&mut self, id: u64, now: Instant) -> Option<&Image> {
let image = self.by_id.get_mut(&id)?;
image.last_used = now;
Some(image)
}
pub fn get_mut(&mut self, id: u64, now: Instant) -> Option<&mut Image> {
let image = self.by_id.get_mut(&id)?;
image.last_used = now;
Some(image)
}
pub fn peek(&self, id: u64) -> Option<&Image> {
self.by_id.get(&id)
}
pub fn peek_mut(&mut self, id: u64) -> Option<&mut Image> {
self.by_id.get_mut(&id)
}
pub fn insert(&mut self, id: u64, image: Image, now: Instant) -> Vec<(u64, Image)> {
self.remove(id);
self.total_len += image.bytes.len();
self.by_id.insert(id, image);
self.evict(now, Some(id))
}
pub fn remove(&mut self, id: u64) -> Option<Image> {
let image = self.by_id.remove(&id)?;
self.total_len -= image.bytes.len();
Some(image)
}
pub fn write(
&mut self,
id: u64,
offset: u64,
data: &[u8],
now: Instant,
) -> Result<&Image, nfsstat3> {
let end = usize::try_from(offset)
.ok()
.and_then(|start| start.checked_add(data.len()))
.filter(|&end| end <= MAX_IMAGE_LEN)
.ok_or(nfsstat3::NFS3ERR_FBIG)?;
let image = self.by_id.get_mut(&id).ok_or(nfsstat3::NFS3ERR_STALE)?;
let start = end - data.len();
if image.bytes.len() < end {
self.total_len += end - image.bytes.len();
image.bytes.resize(end, 0);
}
image.bytes[start..end].copy_from_slice(data);
image.modified = SystemTime::now();
image.dirty = true;
image.generation += 1;
image.last_used = now;
Ok(image)
}
pub fn truncate(&mut self, id: u64, size: u64, now: Instant) -> Result<&Image, nfsstat3> {
let size = usize::try_from(size)
.ok()
.filter(|&size| size <= MAX_IMAGE_LEN)
.ok_or(nfsstat3::NFS3ERR_FBIG)?;
let image = self.by_id.get_mut(&id).ok_or(nfsstat3::NFS3ERR_STALE)?;
if size != image.bytes.len() {
self.total_len = self.total_len - image.bytes.len() + size;
image.bytes.resize(size, 0);
image.modified = SystemTime::now();
image.dirty = true;
image.generation += 1;
}
image.last_used = now;
Ok(image)
}
pub fn due(&self, now: Instant, idle: Duration) -> Vec<u64> {
self.by_id
.iter()
.filter(|(_, image)| {
image.dirty
&& image.tried != Some(image.generation)
&& now.saturating_duration_since(image.last_used) >= idle
})
.map(|(&id, _)| id)
.collect()
}
pub fn retire(&mut self, now: Instant) -> Vec<(u64, Image)> {
let due: Vec<u64> = self
.by_id
.iter()
.filter(|(_, image)| image.expired(now))
.map(|(&id, _)| id)
.collect();
let mut retired = Vec::with_capacity(due.len());
for id in due {
if let Some(image) = self.remove(id) {
retired.push((id, image));
}
}
retired.extend(self.evict(now, None));
retired
}
fn evict(&mut self, now: Instant, keep: Option<u64>) -> Vec<(u64, Image)> {
let mut dirty = Vec::new();
while self.by_id.len() > MAX_IMAGES || self.total_len > self.max_total_len {
let victim = self
.by_id
.iter()
.filter(|(id, _)| Some(**id) != keep)
.max_by_key(|(_, image)| {
(!image.dirty, now.saturating_duration_since(image.last_used))
})
.map(|(&id, _)| id);
let Some(id) = victim else {
break;
};
if let Some(image) = self.remove(id)
&& image.dirty
{
dirty.push((id, image));
}
}
dirty
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.by_id.len()
}
#[cfg(test)]
fn with_byte_budget(max_total_len: usize) -> Self {
Self {
max_total_len,
..Self::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn writes_grow_the_image_and_mark_it_dirty() {
let mut images = Images::default();
let now = Instant::now();
images.insert(
7,
Image::synthesized(b"abc".to_vec(), SystemTime::now(), now),
now,
);
assert!(!images.get(7, now).unwrap().dirty);
let image = images.write(7, 5, b"xy", now).unwrap();
assert_eq!(image.bytes, b"abc\0\0xy");
assert!(image.dirty);
assert_eq!(images.total_len, 7);
images.truncate(7, 2, now).unwrap();
assert_eq!(images.get(7, now).unwrap().bytes, b"ab");
assert_eq!(images.total_len, 2);
assert!(matches!(
images.write(7, MAX_IMAGE_LEN as u64, b"!", now),
Err(nfsstat3::NFS3ERR_FBIG)
));
assert!(matches!(
images.write(8, 0, b"!", now),
Err(nfsstat3::NFS3ERR_STALE)
));
}
#[test]
fn clean_images_go_soon_and_dirty_ones_wait() {
let mut images = Images::default();
let start = Instant::now();
images.insert(
1,
Image::synthesized(Vec::new(), SystemTime::now(), start),
start,
);
images.insert(2, Image::fresh(start), start);
let second = Duration::from_secs(1);
assert_eq!(images.due(start, second), Vec::<u64>::new());
assert_eq!(images.due(start + second, second), [2]);
images.get_mut(2, start).unwrap().tried = Some(0);
assert_eq!(
images.due(start + second, second),
Vec::<u64>::new(),
"tried at this generation"
);
images.write(2, 0, b"x", start).unwrap();
assert_eq!(images.due(start + second, second), [2]);
assert!(
images
.retire(start + CLEAN_TTL.checked_sub(Duration::from_millis(1)).unwrap())
.is_empty()
);
let retired = images.retire(start + CLEAN_TTL);
assert_eq!(retired.len(), 1);
assert_eq!(retired[0].0, 1);
assert!(images.contains(2));
let retired = images.retire(start + PENDING_TTL);
assert_eq!(retired.len(), 1);
assert!(
retired[0].1.dirty,
"a dirty image comes back for one last try"
);
assert_eq!(images.len(), 0);
}
#[test]
fn the_table_is_bounded_and_clean_images_go_first() {
let mut images = Images::default();
let now = Instant::now();
for id in 0..MAX_IMAGES as u64 {
let used = now + Duration::from_millis(id);
let image = if id % 2 == 0 {
Image::fresh(used)
} else {
Image::synthesized(Vec::new(), SystemTime::now(), used)
};
images.insert(id, image, used);
}
let later = now + Duration::from_secs(1);
let evicted = images.insert(5000, Image::fresh(later), later);
assert_eq!(images.len(), MAX_IMAGES);
assert!(evicted.is_empty(), "a clean image went, not a dirty one");
assert!(!images.contains(1), "the least recently used clean one");
assert!(images.contains(5000));
let mut images = Images::with_byte_budget(1000);
let mut at = now;
for id in 0..4u64 {
at += Duration::from_secs(1);
let mut image = if id == 1 {
Image::synthesized(Vec::new(), SystemTime::now(), at)
} else {
Image::fresh(at)
};
image.bytes = vec![0; 300];
images.insert(id, image, at);
}
assert_eq!(images.len(), 3, "the clean one made room");
let mut big = Image::fresh(at);
big.bytes = vec![0; 900];
let evicted = images.insert(9, big, at);
assert_eq!(images.len(), 1, "only the newcomer fits");
assert!(images.contains(9));
let mut evicted_ids: Vec<u64> = evicted.iter().map(|(id, _)| *id).collect();
evicted_ids.sort_unstable();
assert_eq!(evicted_ids, [0, 2, 3]);
assert!(evicted.iter().all(|(_, image)| image.dirty));
}
}