use yo_common::num::{self, parse_i64};
use crate::elem::Elements;
use crate::frozen::{self, Broken};
use crate::listpack::{self, Listpack};
use crate::scan::Cursor;
use crate::ttl::{Applied, Ask, Cond, Deadlines, decide};
const NONE: u64 = u64::MAX;
const CHECK_MAX: usize = Limits::DEFAULT.max_listpack_entries;
const FORM_PACKED: u8 = 1;
const FORM_PACKED_EX: u8 = 2;
const FORM_FIELDS: u8 = 3;
const HAS_TTL: u8 = 0x80;
pub type Text<'a> = listpack::Entry<'a>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limits {
pub max_listpack_entries: usize,
pub max_listpack_value: usize,
}
impl Limits {
pub const DEFAULT: Limits = Limits {
max_listpack_entries: 512,
max_listpack_value: 64,
};
}
impl Default for Limits {
fn default() -> Limits {
Limits::DEFAULT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Encoding {
Listpack,
ListpackEx,
Hashtable,
}
impl Encoding {
#[inline]
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Encoding::Listpack => "listpack",
Encoding::ListpackEx => "listpackex",
Encoding::Hashtable => "hashtable",
}
}
}
#[derive(Debug, Clone)]
struct Packed {
lp: Listpack,
ex: bool,
soonest: u64,
}
impl Packed {
fn new() -> Packed {
Packed {
lp: Listpack::new(),
ex: false,
soonest: NONE,
}
}
#[inline]
const fn step(&self) -> usize {
if self.ex { 3 } else { 2 }
}
#[inline]
fn len(&self) -> usize {
self.lp.len() / self.step()
}
#[inline]
fn find(&self, field: &[u8]) -> Option<usize> {
self.lp.find(field, self.step())
}
fn deadline(&self, at: usize) -> Option<u64> {
if !self.ex {
return None;
}
match self.lp.get(at + 2) {
Some(Text::Int(n)) => u64::try_from(n).ok().filter(|&at| at != 0),
_ => None,
}
}
fn write_deadline(&mut self, at: usize, deadline: u64) {
debug_assert!(self.ex, "widen before writing a deadline");
let mut buf = [0u8; num::DIGITS_MAX];
self.lp.replace(at + 2, num::u64_digits(&mut buf, deadline));
}
fn set(&mut self, field: &[u8], value: &[u8]) -> bool {
match self.find(field) {
Some(at) => {
self.lp.replace(at + 1, value);
if self.ex {
self.write_deadline(at, 0);
}
false
}
None => {
self.lp.push(field);
self.lp.push(value);
if self.ex {
self.lp.push(b"0");
}
true
}
}
}
#[inline]
fn remove_at(&mut self, at: usize) -> bool {
self.lp.delete(at, self.step())
}
fn widen(&mut self) {
if self.ex {
return;
}
let mut fresh = Listpack::new();
let mut pair = self.lp.iter();
while let (Some(field), Some(value)) = (pair.next(), pair.next()) {
push_text(&mut fresh, field);
push_text(&mut fresh, value);
fresh.push(b"0");
}
self.lp = fresh;
self.ex = true;
}
fn earliest(&self) -> u64 {
let mut soonest = NONE;
let mut at = 0;
while at < self.lp.len() {
if let Some(deadline) = self.deadline(at) {
soonest = soonest.min(deadline);
}
at += self.step();
}
soonest
}
fn reap(&mut self, now: u64) -> usize {
let mut gone = 0;
let mut at = 0;
while at < self.lp.len() {
match self.deadline(at) {
Some(deadline) if deadline <= now => {
self.remove_at(at);
gone += 1;
}
_ => at += self.step(),
}
}
gone
}
}
fn bytes_of<'a>(t: Text<'a>, digits: &'a mut [u8; num::DIGITS_MAX]) -> &'a [u8] {
match t {
Text::Str(s) => s,
Text::Int(n) => num::i64_digits(digits, n),
}
}
fn blob_bytes_for(p: &Packed, n: usize) -> usize {
if p.len() == 0 {
return 0;
}
let mut seen = 0usize;
let mut digits = [0u8; num::DIGITS_MAX];
for i in 0..p.len() {
let at = i * p.step();
let (Some(f), Some(v)) = (p.lp.get(at), p.lp.get(at + 1)) else {
break;
};
seen += text_len(&mut digits, f) + text_len(&mut digits, v) + 1;
}
seen.saturating_mul(n) / p.len()
}
fn text_len(digits: &mut [u8; num::DIGITS_MAX], t: Text<'_>) -> usize {
match t {
Text::Str(s) => s.len(),
Text::Int(x) => num::i64_digits(digits, x).len(),
}
}
fn push_text(lp: &mut Listpack, t: Text<'_>) {
match t {
Text::Str(s) => lp.push(s),
Text::Int(n) => {
let mut buf = [0u8; num::DIGITS_MAX];
lp.push(num::i64_digits(&mut buf, n));
}
}
}
#[derive(Debug, Clone)]
struct Table {
fields: Elements<()>,
ttl: Deadlines,
}
impl Table {
fn new(hint: usize, value_bytes: usize) -> Table {
Table {
fields: Elements::tailed(hint, value_bytes),
ttl: Deadlines::new(),
}
}
#[inline]
fn get(&self, field: &[u8]) -> Option<&[u8]> {
self.fields.tail(field)
}
fn set(&mut self, field: &[u8], value: &[u8]) -> bool {
match self.fields.set_tailed(field, value, ()) {
Ok((_, true)) => {
self.ttl.inserted();
true
}
Ok((row, false)) => {
self.ttl.clear(row);
false
}
Err(_) => false,
}
}
fn remove(&mut self, field: &[u8]) -> bool {
match self.fields.index_of(field) {
Some(row) => {
self.remove_at(row);
true
}
None => false,
}
}
fn remove_at(&mut self, row: usize) {
self.fields
.remove_at(row)
.expect("the caller found the row");
self.ttl.removed(row);
}
}
#[derive(Debug, Clone)]
enum Body {
Packed(Packed),
Table(Table),
}
#[derive(Debug, Clone)]
pub struct Hash {
body: Body,
}
impl Default for Hash {
fn default() -> Hash {
Hash::new()
}
}
impl Hash {
#[must_use]
pub fn new() -> Hash {
Hash {
body: Body::Packed(Packed::new()),
}
}
#[must_use]
pub fn with_hint(hint: usize, limits: &Limits) -> Hash {
if hint <= limits.max_listpack_entries {
Hash::new()
} else {
Hash {
body: Body::Table(Table::new(hint, hint.saturating_mul(16))),
}
}
}
pub(crate) fn from_packed(lp: Listpack, limits: &Limits) -> Result<Hash, Listpack> {
let n = lp.len();
if n == 0 || !n.is_multiple_of(2) {
return Err(lp);
}
let fields = n / 2;
if fields > limits.max_listpack_entries || fields > CHECK_MAX {
return Err(lp);
}
let mut marks = [0u64; CHECK_MAX];
let ok = {
let mut field_digits = [0u8; num::DIGITS_MAX];
let mut value_digits = [0u8; num::DIGITS_MAX];
let mut walk = lp.iter();
let mut i = 0;
loop {
let Some(field) = walk.next() else { break true };
let Some(value) = walk.next() else {
break false;
};
let name = bytes_of(field, &mut field_digits);
if name.len() > limits.max_listpack_value {
break false;
}
marks[i] = Elements::<u32>::hash_of(name);
i += 1;
if bytes_of(value, &mut value_digits).len() > limits.max_listpack_value {
break false;
}
}
};
if !ok {
return Err(lp);
}
let marks = &mut marks[..fields];
marks.sort_unstable();
if marks.windows(2).any(|pair| pair[0] == pair[1]) {
return Err(lp);
}
Ok(Hash {
body: Body::Packed(Packed {
lp,
ex: false,
soonest: NONE,
}),
})
}
#[inline]
#[must_use]
pub const fn encoding(&self) -> Encoding {
match &self.body {
Body::Packed(p) if p.ex => Encoding::ListpackEx,
Body::Packed(_) => Encoding::Listpack,
Body::Table(_) => Encoding::Hashtable,
}
}
#[inline]
pub(crate) fn packed_bytes(&self) -> Option<&[u8]> {
match &self.body {
Body::Packed(p) if !p.ex => Some(p.lp.as_bytes()),
_ => None,
}
}
pub fn freeze(&self, out: &mut Vec<u8>) {
match &self.body {
Body::Packed(p) if !p.ex => {
out.push(FORM_PACKED);
out.extend_from_slice(p.lp.as_bytes());
}
Body::Packed(p) => {
out.push(FORM_PACKED_EX);
frozen::put_uint(out, p.soonest);
out.extend_from_slice(p.lp.as_bytes());
}
Body::Table(t) => {
let with_ttl = !t.ttl.is_empty();
out.push(if with_ttl {
FORM_FIELDS | HAS_TTL
} else {
FORM_FIELDS
});
let n = t.fields.len();
frozen::put_uint(out, n as u64);
let mut tail = 0usize;
for i in 0..n {
let (f, v) = t.fields.pair_at(i).expect("index is under the length");
tail += f.len() + v.len() + 1;
}
frozen::put_uint(out, tail as u64);
for i in 0..n {
let (f, v) = t.fields.pair_at(i).expect("index is under the length");
frozen::put_bytes(out, f);
frozen::put_bytes(out, v);
if with_ttl {
frozen::put_uint(out, t.ttl.get(i).unwrap_or(0));
}
}
}
}
}
pub fn thaw(bytes: &[u8]) -> Result<Hash, Broken> {
let mut cut = frozen::Cut::new(bytes);
let tag = cut.byte()?;
match tag {
FORM_PACKED => Ok(Hash {
body: Body::Packed(Packed {
lp: Listpack::from_bytes(cut.rest()).map_err(|_| Broken::Body)?,
ex: false,
soonest: NONE,
}),
}),
FORM_PACKED_EX => {
let soonest = cut.uint()?;
Ok(Hash {
body: Body::Packed(Packed {
lp: Listpack::from_bytes(cut.rest()).map_err(|_| Broken::Body)?,
ex: true,
soonest,
}),
})
}
_ if tag & !HAS_TTL == FORM_FIELDS => {
let with_ttl = tag & HAS_TTL != 0;
let n = usize::try_from(cut.uint()?).map_err(|_| Broken::Short)?;
let tail = usize::try_from(cut.uint()?).map_err(|_| Broken::Short)?;
if n > cut.rest().len() || tail > cut.rest().len() {
return Err(Broken::Body);
}
let mut t = Table::new(n, tail);
for _ in 0..n {
let field = cut.bytes()?;
let value = cut.bytes()?;
let deadline = if with_ttl { cut.uint()? } else { 0 };
if !t.set(field, value) {
return Err(Broken::Body);
}
if deadline != 0 {
let row = t.fields.len() - 1;
let applied = t.ttl.set(row, deadline, Cond::Always, 0);
debug_assert_eq!(applied, Applied::Ok, "a deadline that was stored");
}
}
Ok(Hash {
body: Body::Table(t),
})
}
_ => Err(Broken::Form),
}
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
match &self.body {
Body::Packed(p) => p.len(),
Body::Table(t) => t.fields.len(),
}
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn get(&self, field: &[u8]) -> Option<Text<'_>> {
match &self.body {
Body::Packed(p) => {
let at = p.find(field)?;
p.lp.get(at + 1)
}
Body::Table(t) => t.get(field).map(Text::Str),
}
}
#[must_use]
pub fn contains(&self, field: &[u8]) -> bool {
match &self.body {
Body::Packed(p) => p.find(field).is_some(),
Body::Table(t) => t.fields.contains(field),
}
}
#[must_use]
pub fn value_len(&self, field: &[u8]) -> Option<usize> {
match &self.body {
Body::Packed(_) => self.get(field).map(|v| v.byte_len()),
Body::Table(t) => t.fields.tail_len(field),
}
}
#[must_use]
pub fn at(&self, index: usize) -> Option<(Text<'_>, Text<'_>)> {
match &self.body {
Body::Packed(p) => {
let at = index * p.step();
let field = p.lp.get(at)?;
let value = p.lp.get(at + 1)?;
Some((field, value))
}
Body::Table(t) => {
let (name, value) = t.fields.pair_at(index)?;
Some((Text::Str(name), Text::Str(value)))
}
}
}
#[must_use]
pub fn deadline_at(&self, index: usize) -> Option<u64> {
match &self.body {
Body::Packed(p) => p.deadline(index * p.step()),
Body::Table(t) => t.ttl.get(index),
}
}
pub fn iter(&self) -> impl Iterator<Item = (Text<'_>, Text<'_>)> {
(0..self.len()).map(|i| self.at(i).expect("index is under the length"))
}
pub fn scan<F>(&self, cursor: Cursor, count: usize, mut f: F) -> Cursor
where
F: FnMut(Text<'_>, Text<'_>),
{
match &self.body {
Body::Table(t) => t.fields.scan_pairs(cursor, count, |name, value| {
f(Text::Str(name), Text::Str(value));
}),
Body::Packed(_) => {
for (field, value) in self.iter() {
f(field, value);
}
Cursor::END
}
}
}
pub fn set(&mut self, field: &[u8], value: &[u8], limits: &Limits) -> bool {
if let Body::Packed(p) = &mut self.body {
if field.len() > limits.max_listpack_value || value.len() > limits.max_listpack_value {
self.become_table(1);
} else {
let fresh = p.set(field, value);
if fresh && p.len() > limits.max_listpack_entries {
self.become_table(0);
}
return fresh;
}
}
match &mut self.body {
Body::Table(t) => t.set(field, value),
Body::Packed(_) => unreachable!("the conversion above left a table"),
}
}
pub fn remove(&mut self, field: &[u8]) -> bool {
match &mut self.body {
Body::Packed(p) => match p.find(field) {
Some(at) => p.remove_at(at),
None => false,
},
Body::Table(t) => t.remove(field),
}
}
#[inline]
#[must_use]
pub fn soonest_deadline(&self) -> Option<u64> {
match &self.body {
Body::Packed(p) if p.soonest == NONE => None,
Body::Packed(p) => Some(p.soonest),
Body::Table(t) => t.ttl.soonest(),
}
}
pub fn reap(&mut self, now: u64) -> usize {
match self.soonest_deadline() {
Some(soonest) if soonest <= now => {}
_ => return 0,
}
match &mut self.body {
Body::Packed(p) => {
let gone = p.reap(now);
p.soonest = p.earliest();
gone
}
Body::Table(t) => {
let mut gone = 0;
let mut row = 0;
while row < t.fields.len() {
if t.ttl.is_expired(row, now) {
t.remove_at(row);
gone += 1;
} else {
row += 1;
}
}
t.ttl.refresh_soonest();
gone
}
}
}
pub fn expire(&mut self, field: &[u8], at: u64, cond: Cond, now: u64) -> Applied {
match &mut self.body {
Body::Packed(p) => {
let Some(row) = p.find(field) else {
return Applied::Missing;
};
let applied = decide(p.deadline(row), at, cond, now);
match applied {
Applied::Ok => {
if !p.ex {
p.widen();
}
let row = p.find(field).expect("widening kept every field");
p.write_deadline(row, at);
p.soonest = p.soonest.min(at);
}
Applied::Deleted => {
p.remove_at(row);
}
Applied::Missing | Applied::NotMet => {}
}
applied
}
Body::Table(t) => {
let Some(row) = t.fields.index_of(field) else {
return Applied::Missing;
};
let applied = t.ttl.set(row, at, cond, now);
if applied == Applied::Deleted {
t.remove_at(row);
}
applied
}
}
}
#[must_use]
pub fn deadline(&self, field: &[u8]) -> Ask {
match &self.body {
Body::Packed(p) => match p.find(field) {
None => Ask::Missing,
Some(at) => match p.deadline(at) {
Some(at) => Ask::At(at),
None => Ask::NoDeadline,
},
},
Body::Table(t) => match t.fields.index_of(field) {
None => Ask::Missing,
Some(row) => t.ttl.ask(row),
},
}
}
pub fn persist(&mut self, field: &[u8]) -> Ask {
match &mut self.body {
Body::Packed(p) => {
let Some(at) = p.find(field) else {
return Ask::Missing;
};
match p.deadline(at) {
Some(was) => {
p.write_deadline(at, 0);
Ask::At(was)
}
None => Ask::NoDeadline,
}
}
Body::Table(t) => match t.fields.index_of(field) {
None => Ask::Missing,
Some(row) => t.ttl.clear(row),
},
}
}
#[must_use]
pub fn deadline_count(&self) -> usize {
match &self.body {
Body::Packed(p) if !p.ex => 0,
Body::Packed(p) => (0..p.len())
.filter(|i| p.deadline(i * p.step()).is_some())
.count(),
Body::Table(t) => t.ttl.len(),
}
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
match &self.body {
Body::Packed(p) => p.lp.byte_len(),
Body::Table(t) => t.fields.memory_bytes() + t.ttl.memory_bytes(),
}
}
#[must_use]
pub fn dead_value_bytes(&self) -> usize {
match &self.body {
Body::Packed(_) => 0,
Body::Table(t) => t.fields.dead_name_bytes(),
}
}
fn become_table(&mut self, extra: usize) {
let Body::Packed(p) = &self.body else {
return;
};
let n = p.len() + extra;
let mut t = Table::new(n, blob_bytes_for(p, n));
for i in 0..p.len() {
let at = i * p.step();
let (Some(field), Some(value)) = (p.lp.get(at), p.lp.get(at + 1)) else {
break;
};
let f = field.to_vec();
let v = value.to_vec();
t.set(&f, &v);
if let Some(deadline) = p.deadline(at) {
let row = t.fields.index_of(&f).expect("just inserted");
t.ttl.set(row, deadline, Cond::Always, 0);
}
}
self.body = Body::Table(t);
}
}
#[must_use]
#[inline]
pub fn stores_as_int(bytes: &[u8]) -> bool {
parse_i64(bytes).is_some()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::many;
#[test]
#[ignore = "a measurement, run it by name"]
fn measure_bytes_per_field() {
let limits = Limits::DEFAULT;
for n in [512usize, 1_000, 100_000, 1_000_000] {
let mut h = Hash::new();
let mut payload = 0usize;
for i in 0..n {
let f = format!("f{i:07}");
let v = format!("v{i:07}");
payload += f.len() + v.len();
h.set(f.as_bytes(), v.as_bytes(), &limits);
}
let total = h.memory_bytes();
let per = |b: usize| b as f64 / n as f64;
match &h.body {
Body::Table(t) => println!(
"table n={n:<9} total={total:<10} payload={payload:<9} per_field={:.2} over_per_field={:.2} slots={:.2} rows={:.2} blob={:.2}",
per(total),
per(total - payload),
per(t.fields.slot_bytes()),
per(t.fields.row_bytes()),
per(t.fields.name_bytes()),
),
Body::Packed(_) => println!(
"listpack n={n:<9} total={total:<10} payload={payload:<9} per_field={:.2} over_per_field={:.2}",
per(total),
per(total - payload),
),
}
}
let hashes = 1_000;
let each = 1_000;
let mut all = Vec::with_capacity(hashes);
let mut payload = 0usize;
for h in 0..hashes {
let mut one = Hash::new();
for i in 0..each {
let f = format!("f{i:07}");
let v = format!("v{h:03}{i:04}");
payload += f.len() + v.len();
one.set(f.as_bytes(), v.as_bytes(), &limits);
}
all.push(one);
}
let n = hashes * each;
let per = |b: usize| b as f64 / n as f64;
let sum = |f: fn(&Table) -> usize| -> usize {
all.iter()
.map(|h| match &h.body {
Body::Table(t) => f(t),
Body::Packed(_) => 0,
})
.sum()
};
let total: usize = all.iter().map(Hash::memory_bytes).sum();
println!(
"gate n={n:<9} total={total:<10} payload={payload:<9} per_field={:.2} over_per_field={:.2} slots={:.2} rows={:.2} blob={:.2}",
per(total),
per(total - payload),
per(sum(|t| t.fields.slot_bytes())),
per(sum(|t| t.fields.row_bytes())),
per(sum(|t| t.fields.name_bytes())),
);
}
#[test]
#[ignore = "a measurement, run it by name"]
fn measure_field_access() {
use std::time::Instant;
let limits = Limits::DEFAULT;
let n = 100_000usize;
let fields: Vec<String> = (0..n).map(|i| format!("f{i:07}")).collect();
let mut h = Hash::with_hint(n, &limits);
for f in &fields {
h.set(f.as_bytes(), b"v0000000", &limits);
}
let time = |label: &str, reps: usize, f: &mut dyn FnMut(usize)| {
let start = Instant::now();
for i in 0..reps {
f(i);
}
let ns = start.elapsed().as_nanos() as f64 / reps as f64;
println!("{label:<12} {ns:.2} ns");
};
let mut sink = 0usize;
time("get hit", n, &mut |i| {
sink += h.get(fields[i % n].as_bytes()).map_or(0, |v| v.byte_len());
});
let absent: Vec<String> = (0..n).map(|i| format!("g{i:07}")).collect();
time("get miss", n, &mut |i| {
sink += usize::from(h.get(absent[i].as_bytes()).is_none());
});
assert!(sink > 0, "the reads are not optimised away");
let mut w = h.clone();
time("set old", n, &mut |i| {
w.set(fields[i % n].as_bytes(), b"v1111111", &limits);
});
let mut fresh = Hash::with_hint(n, &limits);
time("set new", n, &mut |i| {
fresh.set(fields[i].as_bytes(), b"v0000000", &limits);
});
}
#[test]
fn a_promoted_hash_does_not_size_its_values_by_guesswork() {
let (limits, n) = if cfg!(miri) {
(&AS_TABLE, 250)
} else {
(&Limits::DEFAULT, 1000)
};
let mut h = Hash::new();
for i in 0..n {
h.set(
format!("f{i:07}").as_bytes(),
format!("v{i:07}").as_bytes(),
limits,
);
}
let Body::Table(t) = &h.body else {
panic!("this many fields is the table band");
};
let held = n * 17;
assert!(
t.fields.name_bytes() < held + held / 4,
"the blob is {} bytes to hold {held}",
t.fields.name_bytes()
);
}
const SMALL: Limits = Limits::DEFAULT;
const AT_128: Limits = Limits {
max_listpack_entries: 128,
max_listpack_value: 64,
};
const AS_TABLE: Limits = Limits {
max_listpack_entries: 1,
max_listpack_value: 64,
};
fn text(t: Text<'_>) -> Vec<u8> {
t.to_vec()
}
fn packed(rows: &[(&[u8], &[u8])]) -> Listpack {
let mut lp = Listpack::new();
for (f, v) in rows {
lp.push(f);
lp.push(v);
}
lp
}
#[test]
fn a_payload_in_this_layout_is_taken_whole() {
let rows: &[(&[u8], &[u8])] = &[
(b"a", b"1"),
(b"b", b"two"),
(b"10", b"ten"),
(b"9", b""),
(b"", b"empty field name"),
];
let h = Hash::from_packed(packed(rows), &SMALL).expect("this band can hold it");
assert_eq!(h.encoding(), Encoding::Listpack);
assert_eq!(h.len(), rows.len());
for (f, v) in rows {
assert_eq!(h.get(f).map(text).as_deref(), Some(*v), "field {f:?}");
}
assert_eq!(h.soonest_deadline(), None);
let mut h = h;
assert!(h.remove(b"10"));
assert_eq!(h.len(), rows.len() - 1);
assert_eq!(h.get(b"10"), None);
assert!(!h.set(b"a", b"other", &SMALL));
assert_eq!(h.get(b"a").map(text).as_deref(), Some(&b"other"[..]));
}
#[test]
fn a_blob_this_band_cannot_hold_is_handed_back() {
let long = vec![b'x'; SMALL.max_listpack_value + 1];
let cases: Vec<(&str, Listpack)> = vec![
(
"the same field twice",
packed(&[(b"a", b"1"), (b"a", b"2")]),
),
(
"the same field twice as a number",
packed(&[(b"7", b"1"), (b"7", b"2")]),
),
("a field past the value limit", packed(&[(&long, b"1")])),
("a value past the value limit", packed(&[(b"a", &long)])),
("empty", Listpack::new()),
("an odd count", {
let mut lp = packed(&[(b"a", b"1")]);
lp.push(b"b");
lp
}),
];
for (why, lp) in cases {
assert!(
Hash::from_packed(lp, &SMALL).is_err(),
"{why} should have been handed back"
);
}
let rows: Vec<(Vec<u8>, Vec<u8>)> = (0..3)
.map(|i| (format!("f{i}").into_bytes(), b"v".to_vec()))
.collect();
let borrowed: Vec<(&[u8], &[u8])> = rows
.iter()
.map(|(f, v)| (f.as_slice(), v.as_slice()))
.collect();
assert!(Hash::from_packed(packed(&borrowed), &AS_TABLE).is_err());
assert!(Hash::from_packed(packed(&borrowed), &SMALL).is_ok());
}
#[test]
fn a_blob_with_more_fields_than_the_check_array_is_handed_back() {
let wide = Limits {
max_listpack_entries: CHECK_MAX * 2,
max_listpack_value: 64,
};
let rows: Vec<(Vec<u8>, Vec<u8>)> = (0..CHECK_MAX + 1)
.map(|i| (format!("f{i:05}").into_bytes(), b"v".to_vec()))
.collect();
let borrowed: Vec<(&[u8], &[u8])> = rows
.iter()
.map(|(f, v)| (f.as_slice(), v.as_slice()))
.collect();
assert!(Hash::from_packed(packed(&borrowed), &wide).is_err());
}
fn pairs(h: &Hash) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = h
.iter()
.map(|(f, v)| {
(
String::from_utf8(text(f)).expect("utf8"),
String::from_utf8(text(v)).expect("utf8"),
)
})
.collect();
out.sort();
out
}
#[test]
fn a_field_written_comes_back() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = Hash::new();
assert!(h.set(b"a", b"1", limits), "the field is new");
assert!(h.set(b"b", b"2", limits));
assert!(!h.set(b"a", b"3", limits), "and now it is not");
assert_eq!(h.len(), 2);
assert_eq!(h.get(b"a").map(text), Some(b"3".to_vec()));
assert_eq!(h.get(b"b").map(text), Some(b"2".to_vec()));
assert_eq!(h.get(b"c"), None);
assert!(h.contains(b"a") && !h.contains(b"c"));
}
}
#[test]
fn a_value_is_never_mistaken_for_a_field() {
let mut h = Hash::new();
h.set(b"a", b"b", &SMALL);
assert_eq!(h.get(b"b"), None, "b is a value, not a field");
assert!(!h.contains(b"b"));
assert!(!h.remove(b"b"), "and it cannot be deleted as one");
assert_eq!(h.len(), 1);
assert!(h.set(b"b", b"c", &SMALL), "so writing b is a new field");
assert_eq!(h.get(b"a").map(text), Some(b"b".to_vec()));
assert_eq!(h.get(b"b").map(text), Some(b"c".to_vec()));
}
#[test]
fn deleting_takes_the_value_with_the_field() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = Hash::new();
for (f, v) in [("a", "1"), ("b", "2"), ("c", "3")] {
h.set(f.as_bytes(), v.as_bytes(), limits);
}
assert!(h.remove(b"b"));
assert!(!h.remove(b"b"), "twice is once");
assert_eq!(h.len(), 2);
assert_eq!(
pairs(&h),
[
("a".to_owned(), "1".to_owned()),
("c".to_owned(), "3".to_owned())
],
"and nothing shifted into the wrong pairing"
);
}
}
#[test]
fn it_promotes_on_the_count_and_on_the_length() {
let mut h = Hash::new();
for i in 0..128u32 {
h.set(format!("f{i}").as_bytes(), b"v", &AT_128);
}
assert_eq!(h.encoding(), Encoding::Listpack, "128 is still a listpack");
h.set(b"one more", b"v", &AT_128);
assert_eq!(h.encoding(), Encoding::Hashtable, "and 129 is not");
assert_eq!(h.len(), 129);
let long = vec![b'x'; 65];
let mut by_value = Hash::new();
by_value.set(b"f", &long, &AT_128);
assert_eq!(by_value.encoding(), Encoding::Hashtable);
assert_eq!(by_value.get(b"f").map(text), Some(long.clone()));
let mut by_field = Hash::new();
by_field.set(&long, b"v", &AT_128);
assert_eq!(by_field.encoding(), Encoding::Hashtable);
assert_eq!(by_field.get(&long).map(text), Some(b"v".to_vec()));
}
#[test]
fn promotion_carries_every_pair_over_intact() {
let mut h = Hash::new();
for i in 0..128u32 {
h.set(
format!("{i}").as_bytes(),
format!("{}", i * 2).as_bytes(),
&AT_128,
);
}
assert_eq!(h.encoding(), Encoding::Listpack);
let before = pairs(&h);
h.set(b"last", b"one", &AT_128);
assert_eq!(h.encoding(), Encoding::Hashtable);
let mut after = pairs(&h);
after.retain(|(f, _)| f != "last");
assert_eq!(after, before, "the pairs survived the conversion");
for i in 0..128u32 {
assert_eq!(
h.get(format!("{i}").as_bytes()).map(text),
Some(format!("{}", i * 2).into_bytes()),
"field {i} is findable by its digits"
);
}
}
#[test]
fn it_never_demotes() {
let mut h = Hash::new();
for i in 0..200u32 {
h.set(format!("f{i}").as_bytes(), b"v", &AT_128);
}
assert_eq!(h.encoding(), Encoding::Hashtable);
for i in 0..199u32 {
h.remove(format!("f{i}").as_bytes());
}
assert_eq!(h.len(), 1);
assert_eq!(
h.encoding(),
Encoding::Hashtable,
"one field left and still a table"
);
}
#[test]
fn a_length_is_answered_without_writing_the_digits() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = Hash::new();
h.set(b"n", b"1234567", limits);
h.set(b"s", b"hello", limits);
h.set(b"e", b"", limits);
assert_eq!(h.value_len(b"n"), Some(7));
assert_eq!(h.value_len(b"s"), Some(5));
assert_eq!(h.value_len(b"e"), Some(0));
assert_eq!(h.value_len(b"missing"), None);
}
}
#[test]
fn a_rewritten_value_gives_its_bytes_back_eventually() {
let mut h = Hash::with_hint(1000, &SMALL);
assert_eq!(h.encoding(), Encoding::Hashtable);
let big = vec![b'z'; 200];
for _ in 0..200 {
h.set(b"one", &big, &SMALL);
}
assert_eq!(h.len(), 1);
assert_eq!(h.get(b"one").map(text), Some(big.clone()));
assert!(
h.dead_value_bytes() < 4096,
"{} bytes left dead",
h.dead_value_bytes()
);
}
#[test]
fn compacting_the_values_moves_every_field_to_the_right_bytes() {
let mut h = Hash::with_hint(1000, &SMALL);
let want: Vec<(Vec<u8>, Vec<u8>)> = (0..300u32)
.map(|i| {
let f = format!("field{i}").into_bytes();
let v = f.repeat(20);
(f, v)
})
.collect();
for (f, v) in &want {
h.set(f, v, &SMALL);
}
for (f, v) in &want {
h.set(f, v, &SMALL);
}
for (f, v) in &want {
assert_eq!(
h.get(f).map(text).as_deref(),
Some(&v[..]),
"field moved wrongly"
);
}
assert_eq!(h.len(), 300);
}
#[test]
fn a_scan_walks_a_hash_of_any_size_exactly_once() {
for hint in [0usize, many(2000)] {
let mut h = Hash::with_hint(hint, &SMALL);
for i in 0..100u32 {
h.set(
format!("f{i}").as_bytes(),
format!("v{i}").as_bytes(),
&SMALL,
);
}
let mut seen: Vec<(String, String)> = Vec::new();
let mut cursor = Cursor::START;
loop {
cursor = h.scan(cursor, 7, |f, v| {
seen.push((
String::from_utf8(text(f)).expect("utf8"),
String::from_utf8(text(v)).expect("utf8"),
));
});
if cursor.is_end() {
break;
}
}
seen.sort();
assert_eq!(seen.len(), 100, "at hint {hint}");
assert_eq!(seen, pairs(&h), "at hint {hint}");
}
}
#[test]
fn a_draw_reaches_every_pair_and_pairs_them_right() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = Hash::new();
for i in 0..50u32 {
h.set(
format!("f{i}").as_bytes(),
format!("v{i}").as_bytes(),
limits,
);
}
for i in 0..h.len() {
let (f, v) = h.at(i).expect("under the length");
let f = String::from_utf8(text(f)).expect("utf8");
let v = String::from_utf8(text(v)).expect("utf8");
assert_eq!(v, f.replace('f', "v"), "row {i} paired wrongly");
}
assert_eq!(h.at(h.len()), None, "and there is nothing past the end");
}
}
#[test]
fn a_hint_that_is_wrong_costs_a_conversion_and_no_answers() {
let mut big = Hash::with_hint(5000, &SMALL);
big.set(b"a", b"1", &SMALL);
assert_eq!(big.encoding(), Encoding::Hashtable);
assert_eq!(big.get(b"a").map(text), Some(b"1".to_vec()));
let mut small = Hash::with_hint(2, &AT_128);
for i in 0..200u32 {
small.set(format!("f{i}").as_bytes(), b"v", &AT_128);
}
assert_eq!(small.encoding(), Encoding::Hashtable);
assert_eq!(small.len(), 200);
}
fn filled(n: u32, limits: &Limits) -> Hash {
let mut h = Hash::new();
for i in 0..n {
h.set(
format!("f{i}").as_bytes(),
format!("v{i}").as_bytes(),
limits,
);
}
h
}
#[test]
fn the_packed_band_widens_the_first_time_a_field_is_given_a_deadline() {
let mut h = filled(3, &SMALL);
assert_eq!(h.encoding(), Encoding::Listpack);
assert_eq!(h.deadline(b"f1"), Ask::NoDeadline);
assert_eq!(h.expire(b"f1", 5000, Cond::Always, 0), Applied::Ok);
assert_eq!(h.encoding(), Encoding::ListpackEx, "three wide now");
assert_eq!(h.len(), 3);
assert_eq!(
pairs(&h),
[
("f0".to_owned(), "v0".to_owned()),
("f1".to_owned(), "v1".to_owned()),
("f2".to_owned(), "v2".to_owned()),
]
);
assert_eq!(h.deadline(b"f1"), Ask::At(5000));
assert_eq!(h.deadline(b"f0"), Ask::NoDeadline, "and only that one");
assert_eq!(h.deadline(b"nope"), Ask::Missing);
assert_eq!(h.deadline_count(), 1);
assert_eq!(h.soonest_deadline(), Some(5000));
}
#[test]
fn the_table_band_keeps_deadlines_beside_the_rows() {
let mut h = filled(3, &AS_TABLE);
assert_eq!(h.encoding(), Encoding::Hashtable);
assert_eq!(h.expire(b"f1", 5000, Cond::Always, 0), Applied::Ok);
assert_eq!(
h.encoding(),
Encoding::Hashtable,
"the table has nothing to widen"
);
assert_eq!(h.deadline(b"f1"), Ask::At(5000));
assert_eq!(h.deadline(b"f0"), Ask::NoDeadline);
assert_eq!(h.deadline(b"nope"), Ask::Missing);
assert_eq!(h.deadline_count(), 1);
assert_eq!(h.soonest_deadline(), Some(5000));
}
#[test]
fn a_field_is_reaped_only_once_its_moment_has_passed() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = filled(3, limits);
h.expire(b"f1", 1000, Cond::Always, 0);
assert_eq!(h.reap(999), 0, "not yet");
assert_eq!(h.len(), 3);
assert!(h.contains(b"f1"), "and it is still readable until then");
assert_eq!(h.reap(1000), 1, "the deadline itself has passed");
assert_eq!(h.len(), 2);
assert!(!h.contains(b"f1"));
assert!(h.contains(b"f0") && h.contains(b"f2"), "and only that one");
assert_eq!(h.reap(1000), 0, "twice takes nothing");
assert_eq!(h.soonest_deadline(), None, "the bound is exact again");
}
}
#[test]
fn a_hash_with_no_deadlines_is_reaped_without_a_walk() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = filled(50, limits);
assert_eq!(h.soonest_deadline(), None);
assert_eq!(h.reap(u64::MAX), 0);
assert_eq!(h.len(), 50);
}
}
#[test]
fn a_write_clears_the_deadline_it_wrote_over() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = filled(3, limits);
h.expire(b"f1", 1000, Cond::Always, 0);
assert_eq!(h.deadline(b"f1"), Ask::At(1000));
assert!(!h.set(b"f1", b"fresh", limits), "not a new field");
assert_eq!(
h.deadline(b"f1"),
Ask::NoDeadline,
"and HSET took the deadline off"
);
assert_eq!(h.reap(u64::MAX), 0, "so nothing expires it");
assert_eq!(h.get(b"f1").map(text), Some(b"fresh".to_vec()));
}
}
#[test]
fn a_deadline_already_past_deletes_the_field_instead_of_being_stored() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = filled(3, limits);
assert_eq!(h.expire(b"f1", 500, Cond::Always, 500), Applied::Deleted);
assert!(!h.contains(b"f1"));
assert_eq!(h.len(), 2);
assert_eq!(h.deadline_count(), 0);
assert_eq!(h.expire(b"gone", 9000, Cond::Always, 0), Applied::Missing);
}
}
#[test]
fn the_conditions_reach_both_bands_the_same_way() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = filled(2, limits);
assert_eq!(h.expire(b"f0", 1000, Cond::AlreadySet, 0), Applied::NotMet);
assert_eq!(h.deadline(b"f0"), Ask::NoDeadline);
assert_eq!(h.expire(b"f0", 1000, Cond::NotSet, 0), Applied::Ok);
assert_eq!(h.expire(b"f0", 2000, Cond::NotSet, 0), Applied::NotMet);
assert_eq!(h.expire(b"f0", 500, Cond::Greater, 0), Applied::NotMet);
assert_eq!(h.expire(b"f0", 2000, Cond::Greater, 0), Applied::Ok);
assert_eq!(h.deadline(b"f0"), Ask::At(2000));
assert_eq!(h.expire(b"f0", 0, Cond::NotSet, 5), Applied::NotMet);
assert!(h.contains(b"f0"));
}
}
#[test]
fn persisting_takes_the_deadline_off_and_says_what_was_there() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = filled(3, limits);
h.expire(b"f1", 1000, Cond::Always, 0);
assert_eq!(h.persist(b"f1"), Ask::At(1000));
assert_eq!(
h.persist(b"f1"),
Ask::NoDeadline,
"twice is -1, not an error"
);
assert_eq!(h.persist(b"gone"), Ask::Missing);
assert_eq!(h.deadline_count(), 0);
assert_eq!(h.reap(u64::MAX), 0, "and it does not expire any more");
assert_eq!(h.len(), 3);
}
}
#[test]
fn deadlines_follow_their_fields_through_a_removal() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = filled(5, limits);
for i in 0..5u32 {
assert_eq!(
h.expire(
format!("f{i}").as_bytes(),
1000 + u64::from(i),
Cond::Always,
0
),
Applied::Ok
);
}
assert!(h.remove(b"f1"));
assert_eq!(h.len(), 4);
for i in [0u32, 2, 3, 4] {
assert_eq!(
h.deadline(format!("f{i}").as_bytes()),
Ask::At(1000 + u64::from(i)),
"f{i} kept someone else's deadline"
);
}
assert_eq!(h.deadline_count(), 4);
}
}
#[test]
fn a_deadline_comes_over_with_its_field_on_promotion() {
let mut h = Hash::new();
for i in 0..128u32 {
h.set(format!("f{i}").as_bytes(), b"v", &AT_128);
}
h.expire(b"f7", 4000, Cond::Always, 0);
h.expire(b"f9", 2000, Cond::Always, 0);
assert_eq!(h.encoding(), Encoding::ListpackEx);
h.set(b"one more", b"v", &AT_128);
assert_eq!(h.encoding(), Encoding::Hashtable, "and now it is a table");
assert_eq!(h.len(), 129);
assert_eq!(h.deadline(b"f7"), Ask::At(4000));
assert_eq!(h.deadline(b"f9"), Ask::At(2000));
assert_eq!(h.deadline(b"f8"), Ask::NoDeadline);
assert_eq!(h.deadline_count(), 2);
assert_eq!(h.soonest_deadline(), Some(2000));
assert_eq!(h.reap(3000), 1, "f9 and not f7");
assert!(!h.contains(b"f9") && h.contains(b"f7"));
}
#[test]
fn a_widened_hash_still_scans_and_draws_every_pair_once() {
for hint in [0usize, many(2000)] {
let mut h = Hash::with_hint(hint, &SMALL);
for i in 0..100u32 {
h.set(
format!("f{i}").as_bytes(),
format!("v{i}").as_bytes(),
&SMALL,
);
}
h.expire(b"f42", 9000, Cond::Always, 0);
let mut seen: Vec<(String, String)> = Vec::new();
let mut cursor = Cursor::START;
loop {
cursor = h.scan(cursor, 7, |f, v| {
seen.push((
String::from_utf8(text(f)).expect("utf8"),
String::from_utf8(text(v)).expect("utf8"),
));
});
if cursor.is_end() {
break;
}
}
seen.sort();
assert_eq!(seen.len(), 100, "at hint {hint}");
assert_eq!(seen, pairs(&h), "at hint {hint}");
for i in 0..h.len() {
let (f, v) = h.at(i).expect("under the length");
let f = String::from_utf8(text(f)).expect("utf8");
let v = String::from_utf8(text(v)).expect("utf8");
assert_eq!(
v,
f.replace('f', "v"),
"row {i} paired wrongly at hint {hint}"
);
}
}
}
#[test]
fn a_run_of_expired_fields_all_go_together() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = filled(6, limits);
for i in [1u32, 2, 3] {
h.expire(format!("f{i}").as_bytes(), 100, Cond::Always, 0);
}
assert_eq!(h.reap(200), 3);
assert_eq!(h.len(), 3);
for i in [0u32, 4, 5] {
assert!(h.contains(format!("f{i}").as_bytes()), "f{i} went too");
}
}
}
#[test]
fn an_empty_hash_has_allocated_almost_nothing() {
let h = Hash::new();
assert!(h.is_empty());
assert_eq!(h.len(), 0);
assert_eq!(h.get(b"a"), None);
assert!(h.memory_bytes() < 64, "{} bytes", h.memory_bytes());
}
fn round_trip(h: &Hash) -> Hash {
let mut out = Vec::new();
h.freeze(&mut out);
let back = Hash::thaw(&out).expect("it came back");
assert_eq!(back.len(), h.len(), "the field count");
assert_eq!(back.encoding(), h.encoding(), "the band");
assert_eq!(pairs(&back), pairs(h), "the fields");
back
}
#[test]
fn a_frozen_hash_comes_back_in_the_band_it_left() {
round_trip(&Hash::new());
round_trip(&filled(3, &SMALL));
round_trip(&filled(300, &AT_128));
round_trip(&filled(3, &AS_TABLE));
let mut h = Hash::new();
h.set(b"f", &[b'x'; 200], &SMALL);
assert_eq!(h.encoding(), Encoding::Hashtable);
let back = round_trip(&h);
assert_eq!(back.get(b"f").map(text), Some(vec![b'x'; 200]));
}
#[test]
fn every_field_deadline_survives_the_trip() {
for limits in [&SMALL, &AS_TABLE] {
let mut h = filled(4, limits);
h.expire(b"f1", 5000, Cond::Always, 0);
h.expire(b"f3", 9000, Cond::Always, 0);
let back = round_trip(&h);
assert_eq!(back.deadline(b"f1"), Ask::At(5000));
assert_eq!(back.deadline(b"f3"), Ask::At(9000));
assert_eq!(back.deadline(b"f0"), Ask::NoDeadline);
assert_eq!(back.deadline(b"f2"), Ask::NoDeadline);
assert_eq!(back.deadline_count(), 2);
assert_eq!(back.soonest_deadline(), Some(5000));
}
}
#[test]
fn a_widened_hash_with_no_deadlines_left_still_comes_back_widened() {
let mut h = filled(3, &SMALL);
h.expire(b"f1", 5000, Cond::Always, 0);
assert_eq!(h.persist(b"f1"), Ask::At(5000));
assert_eq!(h.encoding(), Encoding::ListpackEx);
assert_eq!(h.deadline_count(), 0);
let back = round_trip(&h);
assert_eq!(back.deadline_count(), 0);
assert_eq!(back.soonest_deadline(), Some(5000), "the bound only falls");
}
#[test]
fn a_frozen_hash_that_arrives_damaged_is_an_error_and_not_a_panic() {
for h in [filled(3, &SMALL), filled(3, &AS_TABLE)] {
let mut out = Vec::new();
h.freeze(&mut out);
for cut in 0..out.len() {
let _ = Hash::thaw(&out[..cut]);
}
}
assert_eq!(Hash::thaw(&[]).err(), Some(Broken::Short));
assert_eq!(Hash::thaw(&[9]).err(), Some(Broken::Form));
assert_eq!(Hash::thaw(&[FORM_PACKED, 1, 2]).err(), Some(Broken::Body));
assert_eq!(
Hash::thaw(&[FORM_FIELDS, 0xff, 0xff, 0x7f, 0]).err(),
Some(Broken::Body)
);
}
}