cubelinux: v3 — the image carries the addresses, so a coordinate is a place
A packed list (`space | key | len | value`, repeated) cannot answer "where is this coordinate":
record N's offset is the sum of every record before it. The key gives ORDER, and order is not an
address, so no coordinate could be turned into a place and not even a binary search was possible —
the middle record's offset is just as unknowable. Every lookup walked, every batch of a listing
walked again, and a read of one record cost as much as the store (measured: 110 ms per get, 119 ms
per `spaces`, 1,206 ms for a 6,127-record listing).
v3 puts the addresses in the image:
[header 46] magic, version, curve, extents, counts
[space table: space_count x 48] space | first index | records
[index: record_count x 40] key | value offset | value length
[values: packed, in index order]
A lookup is now a binary search over arithmetic addresses (`index_off + i * 40`); a listing starts
at its space's first index entry and streams, reading a span of values per batch; asking which
spaces exist is the space table; a spatial range is a contiguous run of index entries. An index
entry is 40 bytes against the packed frame's 64, so the image also gets smaller.
Measured by `verify-enum-cost.sh` between a 500-record store and a 20,000-record one:
listing the same 10-record space: 3.22 ms -> 3.27 ms (1.0x, store grew 40x)
asking which spaces exist: 3.25 ms -> 3.08 ms (0.9x)
reading a coordinate that is there: 1.07 ms -> 1.25 ms (1.2x)
reading one that is not: 1.19 ms -> 1.22 ms (1.0x)
Nothing scales with the store any more, which is the premise: knowing a coordinate is what lets you
reach it.
Two bugs found on the way, both of the kind only a real image shows. The index was written with a
4-byte value length (`Entry` carries it in a u32) while the header's arithmetic and both readers
assumed 8 — every entry four bytes short, the last of them overlapping the values. And the digest's
`bytes` figure added the layout's own header length, so two layouts of one store digested
differently; it is now the records' logical size, which is the same number either way.
Verified: verify-kernel-append.sh, verify-kernel-checkpoint.sh, verify-enum.sh, verify-enum-cost.sh
and verify-frontend.sh all pass, and the workspace tests pass — including a new fixture test in
cube-store-raw that reads a 700-byte image the kernel itself wrote, because a hand-built image
cannot catch a misunderstanding shared by the builder.
This commit is contained in:
+572
-16
@@ -67,10 +67,38 @@ module! {
|
|||||||
const MAGIC: &[u8; 4] = b"CUBE";
|
const MAGIC: &[u8; 4] = b"CUBE";
|
||||||
/// The original format: magic, version, curve, then records until zero padding.
|
/// The original format: magic, version, curve, then records until zero padding.
|
||||||
const VERSION_V1: u8 = 1;
|
const VERSION_V1: u8 = 1;
|
||||||
/// The current format: the same, plus the image's byte extent and record count.
|
/// The packed format: the same, plus the image's byte extent and record count.
|
||||||
const VERSION: u8 = 2;
|
const VERSION: u8 = 2;
|
||||||
|
/// The addressed format: the same, plus a fixed-size index, a space table, and packed values.
|
||||||
|
///
|
||||||
|
/// v2 is a packed list — `space | key | len | value`, repeated — so record N's offset is the sum of
|
||||||
|
/// every record before it. A coordinate gives the key, and the key gives ORDER, and order is not an
|
||||||
|
/// address: no coordinate can be turned into a place, and not even a binary search is possible,
|
||||||
|
/// because the middle record's offset cannot be computed either. Every lookup, therefore, walks.
|
||||||
|
///
|
||||||
|
/// v3 puts the addresses in the image:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// [header 46] magic, version, curve, extents, counts
|
||||||
|
/// [space table: space_count x 48] space | first index | records
|
||||||
|
/// [index: record_count x 40] key | value offset | value length
|
||||||
|
/// [values: packed, in index order]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The index is fixed-size and sorted by key, so a lookup is a binary search over arithmetic
|
||||||
|
/// addresses (`index_off + i * 40`), a listing starts at its space's first index entry and streams,
|
||||||
|
/// asking which spaces exist is the space table, and a spatial range is a contiguous run of index
|
||||||
|
/// entries. An index entry is 40 bytes against v2's 64-byte frame, so the image also gets smaller.
|
||||||
|
const VERSION_V3: u8 = 3;
|
||||||
const HEADER_LEN_V1: usize = 6;
|
const HEADER_LEN_V1: usize = 6;
|
||||||
const HEADER_LEN_V2: usize = 6 + 8 + 8;
|
const HEADER_LEN_V2: usize = 6 + 8 + 8;
|
||||||
|
/// magic(4) version(1) curve(1) image_bytes(8) record_count(8) space_count(8) index_off(8)
|
||||||
|
/// values_off(8).
|
||||||
|
const HEADER_LEN_V3: usize = 4 + 1 + 1 + 8 + 8 + 8 + 8 + 8;
|
||||||
|
/// `key(24) | value_off(8) | value_len(8)`.
|
||||||
|
const INDEX_ENTRY: usize = RAW_KEY_LEN + 8 + 8;
|
||||||
|
/// `space(32) | first index(8) | records(8)`.
|
||||||
|
const SPACE_ENTRY: usize = SPACE_ID_LEN + 8 + 8;
|
||||||
const SPACE_ID_LEN: usize = 32;
|
const SPACE_ID_LEN: usize = 32;
|
||||||
const RAW_KEY_LEN: usize = 24;
|
const RAW_KEY_LEN: usize = 24;
|
||||||
const RECORD_FIXED: usize = SPACE_ID_LEN + RAW_KEY_LEN + 8;
|
const RECORD_FIXED: usize = SPACE_ID_LEN + RAW_KEY_LEN + 8;
|
||||||
@@ -309,6 +337,60 @@ fn parse_header(image: &[u8]) -> Result<Header, &'static str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The v3 header: what a reader needs to address the image by arithmetic.
|
||||||
|
struct HeaderV3 {
|
||||||
|
image_bytes: u64,
|
||||||
|
record_count: u64,
|
||||||
|
space_count: u64,
|
||||||
|
index_off: u64,
|
||||||
|
values_off: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeaderV3 {
|
||||||
|
fn encode(&self, out: &mut KVVec<u8>, curve: u8) -> Result<(), AllocError> {
|
||||||
|
out.extend_from_slice(MAGIC, GFP_KERNEL)?;
|
||||||
|
out.extend_from_slice(&[VERSION_V3, curve], GFP_KERNEL)?;
|
||||||
|
out.extend_from_slice(&self.image_bytes.to_le_bytes(), GFP_KERNEL)?;
|
||||||
|
out.extend_from_slice(&self.record_count.to_le_bytes(), GFP_KERNEL)?;
|
||||||
|
out.extend_from_slice(&self.space_count.to_le_bytes(), GFP_KERNEL)?;
|
||||||
|
out.extend_from_slice(&self.index_off.to_le_bytes(), GFP_KERNEL)?;
|
||||||
|
out.extend_from_slice(&self.values_off.to_le_bytes(), GFP_KERNEL)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode(image: &[u8]) -> Result<Self, &'static str> {
|
||||||
|
if image.len() < HEADER_LEN_V3 || &image[0..4] != MAGIC || image[4] != VERSION_V3 {
|
||||||
|
return Err("not-a-v3-header");
|
||||||
|
}
|
||||||
|
let word = |at: usize| -> u64 {
|
||||||
|
let mut w = [0u8; 8];
|
||||||
|
w.copy_from_slice(&image[at..at + 8]);
|
||||||
|
u64::from_le_bytes(w)
|
||||||
|
};
|
||||||
|
let header = HeaderV3 {
|
||||||
|
image_bytes: word(6),
|
||||||
|
record_count: word(14),
|
||||||
|
space_count: word(22),
|
||||||
|
index_off: word(30),
|
||||||
|
values_off: word(38),
|
||||||
|
};
|
||||||
|
// Every extent inside the image and in order, or a reader would take somebody else's bytes
|
||||||
|
// for a record — the same rule the v2 header is held to.
|
||||||
|
if header.image_bytes < HEADER_LEN_V3 as u64 {
|
||||||
|
return Err("bad-extent");
|
||||||
|
}
|
||||||
|
let table_end = HEADER_LEN_V3 as u64 + header.space_count * SPACE_ENTRY as u64;
|
||||||
|
let index_end = header.index_off + header.record_count * INDEX_ENTRY as u64;
|
||||||
|
if header.index_off != table_end || header.values_off != index_end {
|
||||||
|
return Err("bad-tables");
|
||||||
|
}
|
||||||
|
if header.values_off > header.image_bytes {
|
||||||
|
return Err("tables-past-the-image");
|
||||||
|
}
|
||||||
|
Ok(header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn digest(image: &[u8]) -> Line {
|
fn digest(image: &[u8]) -> Line {
|
||||||
let mut line = Line::new();
|
let mut line = Line::new();
|
||||||
|
|
||||||
@@ -424,7 +506,10 @@ fn digest(image: &[u8]) -> Line {
|
|||||||
// records, whatever padding the reader happened to see. Reporting what was read made
|
// records, whatever padding the reader happened to see. Reporting what was read made
|
||||||
// the same records disagree between a file and a device — and the gates compare this
|
// the same records disagree between a file and a device — and the gates compare this
|
||||||
// line, so it has to mean the same thing on both sides.
|
// line, so it has to mean the same thing on both sides.
|
||||||
header_len + count * RECORD_FIXED as u64 + value_bytes,
|
// The records' logical size, and not the layout's: a v2 image and a v3 image of one
|
||||||
|
// store hold the same records in different shapes, and this line is how the two are
|
||||||
|
// compared — a figure that changes with the layout would make them look different.
|
||||||
|
count * RECORD_FIXED as u64 + value_bytes,
|
||||||
count,
|
count,
|
||||||
value_bytes,
|
value_bytes,
|
||||||
h,
|
h,
|
||||||
@@ -853,9 +938,10 @@ fn merged_digest(image: &[u8], log: &[u8], header: &Header) -> Result<Line, &'st
|
|||||||
value_bytes += value.len() as u64;
|
value_bytes += value.len() as u64;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `bytes` is the size a checkpoint of this store would produce — the same number
|
// `bytes` is the records' logical size, not the size of a particular layout: a v2 image and a
|
||||||
// userspace reports for the image it writes from the same records.
|
// v3 image of one store hold the same records in different shapes, and this line is what the
|
||||||
let folded = HEADER_LEN_V2 as u64 + count * RECORD_FIXED as u64 + value_bytes;
|
// two are compared by, so the figure must not depend on which shape is on disk.
|
||||||
|
let folded = count * RECORD_FIXED as u64 + value_bytes;
|
||||||
let mut line = Line::new();
|
let mut line = Line::new();
|
||||||
// The digest line is field-for-field what `cube-image digest` prints, so the two are
|
// The digest line is field-for-field what `cube-image digest` prints, so the two are
|
||||||
// compared by diff. What the log contributed goes on its own line: a padded log region
|
// compared by diff. What the log contributed goes on its own line: a padded log region
|
||||||
@@ -1108,6 +1194,7 @@ fn serialize_image(merged: &mut Merged, curve: u8) -> Result<KVVec<u8>, AllocErr
|
|||||||
merged.sort_entries();
|
merged.sort_entries();
|
||||||
let entries = merged.entries.as_slice();
|
let entries = merged.entries.as_slice();
|
||||||
|
|
||||||
|
// The winners first: one live record per coordinate, in the store's own (space, key) order.
|
||||||
let mut winners = KVVec::<u32>::new();
|
let mut winners = KVVec::<u32>::new();
|
||||||
let mut values_len: u64 = 0;
|
let mut values_len: u64 = 0;
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
@@ -1127,20 +1214,57 @@ fn serialize_image(merged: &mut Merged, curve: u8) -> Result<KVVec<u8>, AllocErr
|
|||||||
i = last + 1;
|
i = last + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let image_bytes = HEADER_LEN_V2 as u64 + winners.len() as u64 * RECORD_FIXED as u64 + values_len;
|
// The space table: where each space's records begin in the index, and how many. It comes from
|
||||||
let mut out = KVVec::<u8>::with_capacity(image_bytes as usize, GFP_KERNEL)?;
|
// the same order the index is written in, so a walk of the store is a walk of this table.
|
||||||
out.extend_from_slice(MAGIC, GFP_KERNEL)?;
|
let mut spaces = KVVec::<([u8; SPACE_ID_LEN], u64, u64)>::new();
|
||||||
out.extend_from_slice(&[VERSION, curve], GFP_KERNEL)?;
|
for (at, w) in winners.as_slice().iter().enumerate() {
|
||||||
out.extend_from_slice(&image_bytes.to_le_bytes(), GFP_KERNEL)?;
|
let space = merged.entries.as_slice()[*w as usize].space;
|
||||||
out.extend_from_slice(&(winners.len() as u64).to_le_bytes(), GFP_KERNEL)?;
|
match spaces.as_mut_slice().last_mut() {
|
||||||
|
Some((last_space, _, records)) if *last_space == space => *records += 1,
|
||||||
|
_ => spaces.push((space, at as u64, 1), GFP_KERNEL)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = HeaderV3 {
|
||||||
|
record_count: winners.len() as u64,
|
||||||
|
space_count: spaces.len() as u64,
|
||||||
|
index_off: (HEADER_LEN_V3 + spaces.len() * SPACE_ENTRY) as u64,
|
||||||
|
values_off: (HEADER_LEN_V3 + spaces.len() * SPACE_ENTRY + winners.len() * INDEX_ENTRY) as u64,
|
||||||
|
image_bytes: 0,
|
||||||
|
};
|
||||||
|
let image_bytes = header.values_off + values_len;
|
||||||
|
|
||||||
|
let mut out = KVVec::<u8>::with_capacity(image_bytes as usize, GFP_KERNEL)?;
|
||||||
|
let mut header = header;
|
||||||
|
header.image_bytes = image_bytes;
|
||||||
|
header.encode(&mut out, curve)?;
|
||||||
|
|
||||||
|
for (space, first, records) in spaces.as_slice() {
|
||||||
|
out.extend_from_slice(space, GFP_KERNEL)?;
|
||||||
|
out.extend_from_slice(&first.to_le_bytes(), GFP_KERNEL)?;
|
||||||
|
out.extend_from_slice(&records.to_le_bytes(), GFP_KERNEL)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The index, whose entries are the addresses: a fixed stride is what makes `index_off +
|
||||||
|
// i * INDEX_ENTRY` a place a reader can go to without reading anything before it.
|
||||||
|
let mut value_at = header.values_off;
|
||||||
for w in winners.as_slice() {
|
for w in winners.as_slice() {
|
||||||
let e = &merged.entries.as_slice()[*w as usize];
|
let e = &merged.entries.as_slice()[*w as usize];
|
||||||
let value = merged.value(e);
|
|
||||||
out.extend_from_slice(&e.space, GFP_KERNEL)?;
|
|
||||||
out.extend_from_slice(&e.key, GFP_KERNEL)?;
|
out.extend_from_slice(&e.key, GFP_KERNEL)?;
|
||||||
out.extend_from_slice(&(value.len() as u64).to_le_bytes(), GFP_KERNEL)?;
|
out.extend_from_slice(&value_at.to_le_bytes(), GFP_KERNEL)?;
|
||||||
out.extend_from_slice(value, GFP_KERNEL)?;
|
// The length is written as 8 bytes even though `Entry` carries it in 4: an index entry
|
||||||
|
// is 40 bytes and the header's `values_off` is computed from that, so a 4-byte field here
|
||||||
|
// shortens every entry by four and leaves the last ones overlapping the values. Both
|
||||||
|
// readers assume the stride the header declares, which is the point of a fixed stride.
|
||||||
|
out.extend_from_slice(&(e.value_len as u64).to_le_bytes(), GFP_KERNEL)?;
|
||||||
|
value_at += e.value_len as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The values, packed in index order: a batch of records is one span of this, which is why a
|
||||||
|
// listing can read what it returns in one go rather than record by record.
|
||||||
|
for w in winners.as_slice() {
|
||||||
|
let e = &merged.entries.as_slice()[*w as usize];
|
||||||
|
out.extend_from_slice(merged.value(e), GFP_KERNEL)?;
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
@@ -1995,7 +2119,419 @@ impl Batch<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `CUBE_OP_GET`: read a coordinate.
|
// ── v3: the addressed reader ────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// A v3 image is read at the offsets the header describes, so an operation reads the bytes it names
|
||||||
|
// and nothing else. This is what the coordinate finally buys: a lookup is a binary search over a
|
||||||
|
// fixed stride, a listing starts where its space starts, and neither reads the store to find out.
|
||||||
|
|
||||||
|
/// A v3 image open for reading, with the few bytes an operation needs read on demand.
|
||||||
|
struct Addressed {
|
||||||
|
file: *mut bindings::file,
|
||||||
|
/// Where the image begins in the device.
|
||||||
|
image_off: u64,
|
||||||
|
header: HeaderV3,
|
||||||
|
/// The log window, which is what can override the image and is small.
|
||||||
|
log: KVVec<u8>,
|
||||||
|
/// Reused for every span read, so a lookup does not allocate per probe.
|
||||||
|
scratch: KVVec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Addressed {
|
||||||
|
/// Open the store and read its control block, v3 header, and log window — not its records.
|
||||||
|
fn open() -> Result<Option<Self>> {
|
||||||
|
// SAFETY: `store_device()` is a NUL-terminated C string the module parameter filled at
|
||||||
|
// boot; filp_open returns a valid `struct file *` or an error pointer, checked below.
|
||||||
|
let file = unsafe { bindings::filp_open(store_device(), 0, 0) };
|
||||||
|
let file = kernel::error::from_err_ptr(file)?;
|
||||||
|
if file.is_null() {
|
||||||
|
return Err(EINVAL);
|
||||||
|
}
|
||||||
|
let mut opened = Addressed {
|
||||||
|
file,
|
||||||
|
image_off: 0,
|
||||||
|
header: HeaderV3 {
|
||||||
|
image_bytes: 0,
|
||||||
|
record_count: 0,
|
||||||
|
space_count: 0,
|
||||||
|
index_off: 0,
|
||||||
|
values_off: 0,
|
||||||
|
},
|
||||||
|
log: KVVec::new(),
|
||||||
|
scratch: KVVec::new(),
|
||||||
|
};
|
||||||
|
opened.scratch = KVVec::<u8>::with_capacity(4096, GFP_KERNEL)?;
|
||||||
|
opened.scratch.resize(4096, 0, GFP_KERNEL)?;
|
||||||
|
|
||||||
|
let mut head = KVVec::<u8>::with_capacity(CTL_COPY_B + CTL_COPY_LEN, GFP_KERNEL)?;
|
||||||
|
read_exact_at(file, 0, CTL_COPY_B + CTL_COPY_LEN, &mut head, &mut opened.scratch)?;
|
||||||
|
if &head.as_slice()[0..4] != CTL_MAGIC.as_slice() {
|
||||||
|
return Ok(None); // a bare image, which the walking reader handles
|
||||||
|
}
|
||||||
|
let control = read_control(head.as_slice()).ok_or(EINVAL)?;
|
||||||
|
opened.image_off = control.image_off() as u64;
|
||||||
|
|
||||||
|
let mut raw = KVVec::<u8>::with_capacity(HEADER_LEN_V3, GFP_KERNEL)?;
|
||||||
|
read_exact_at(file, opened.image_off, HEADER_LEN_V3, &mut raw, &mut opened.scratch)?;
|
||||||
|
if raw.as_slice()[4] != VERSION_V3 {
|
||||||
|
return Ok(None); // v1 or v2: the walking reader
|
||||||
|
}
|
||||||
|
opened.header = HeaderV3::decode(raw.as_slice()).map_err(|what| {
|
||||||
|
pr_err!("cubelinux: {}\n", what);
|
||||||
|
EINVAL
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if control.log_used > 0 {
|
||||||
|
let len = core::cmp::min(WAL_HEADER_LEN + control.log_used as usize, MAX_BYTES);
|
||||||
|
read_exact_at(file, control.log_off() as u64, len, &mut opened.log, &mut opened.scratch)?;
|
||||||
|
}
|
||||||
|
Ok(Some(opened))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read `len` bytes at an offset *within the image*.
|
||||||
|
fn read_image_at(&mut self, off: u64, len: usize, out: &mut KVVec<u8>) -> Result<()> {
|
||||||
|
if off + len as u64 > self.header.image_bytes {
|
||||||
|
return Err(EINVAL);
|
||||||
|
}
|
||||||
|
read_exact_at(self.file, self.image_off + off, len, out, &mut self.scratch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The space table entry for a space: where its records start in the index, and how many.
|
||||||
|
///
|
||||||
|
/// The table is sorted by space and fixed-size, so this is a binary search over arithmetic
|
||||||
|
/// addresses — the same reason the index works.
|
||||||
|
fn space_entry(&mut self, space: &[u8; SPACE_ID_LEN]) -> Result<Option<(u64, u64)>> {
|
||||||
|
let mut buf = KVVec::<u8>::new();
|
||||||
|
let mut lo = 0u64;
|
||||||
|
let mut hi = self.header.space_count;
|
||||||
|
while lo < hi {
|
||||||
|
let mid = lo + (hi - lo) / 2;
|
||||||
|
let at = HEADER_LEN_V3 as u64 + mid * SPACE_ENTRY as u64;
|
||||||
|
self.read_image_at(at, SPACE_ENTRY, &mut buf)?;
|
||||||
|
let entry = buf.as_slice();
|
||||||
|
let found = &entry[..SPACE_ID_LEN];
|
||||||
|
if found < &space[..] {
|
||||||
|
lo = mid + 1;
|
||||||
|
} else if found > &space[..] {
|
||||||
|
hi = mid;
|
||||||
|
} else {
|
||||||
|
let mut w = [0u8; 8];
|
||||||
|
w.copy_from_slice(&entry[SPACE_ID_LEN..SPACE_ID_LEN + 8]);
|
||||||
|
let first = u64::from_le_bytes(w);
|
||||||
|
w.copy_from_slice(&entry[SPACE_ID_LEN + 8..SPACE_ENTRY]);
|
||||||
|
return Ok(Some((first, u64::from_le_bytes(w))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One index entry, by its position in the index.
|
||||||
|
fn index_entry(&mut self, at: u64, buf: &mut KVVec<u8>) -> Result<(u64, u64)> {
|
||||||
|
let off = self.header.index_off + at * INDEX_ENTRY as u64;
|
||||||
|
self.read_image_at(off, INDEX_ENTRY, buf)?;
|
||||||
|
let entry = buf.as_slice();
|
||||||
|
let mut w = [0u8; 8];
|
||||||
|
w.copy_from_slice(&entry[RAW_KEY_LEN..RAW_KEY_LEN + 8]);
|
||||||
|
let value_off = u64::from_le_bytes(w);
|
||||||
|
w.copy_from_slice(&entry[RAW_KEY_LEN + 8..INDEX_ENTRY]);
|
||||||
|
Ok((value_off, u64::from_le_bytes(w)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The address of `key` inside a space's index range, or nothing if the space does not hold it.
|
||||||
|
fn find(&mut self, key: &[u8; RAW_KEY_LEN], first: u64, records: u64) -> Result<Option<(u64, u64)>> {
|
||||||
|
let mut buf = KVVec::<u8>::new();
|
||||||
|
let mut lo = 0u64;
|
||||||
|
let mut hi = records;
|
||||||
|
while lo < hi {
|
||||||
|
let mid = lo + (hi - lo) / 2;
|
||||||
|
let off = self.header.index_off + (first + mid) * INDEX_ENTRY as u64;
|
||||||
|
self.read_image_at(off, INDEX_ENTRY, &mut buf)?;
|
||||||
|
let found = &buf.as_slice()[..RAW_KEY_LEN];
|
||||||
|
if found < &key[..] {
|
||||||
|
lo = mid + 1;
|
||||||
|
} else if found > &key[..] {
|
||||||
|
hi = mid;
|
||||||
|
} else {
|
||||||
|
return self.index_entry(first + mid, &mut buf).map(Some);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of an index entry, read from where the index says it is.
|
||||||
|
fn value_at(&mut self, value_off: u64, value_len: u64) -> Result<KVVec<u8>> {
|
||||||
|
let mut out = KVVec::<u8>::new();
|
||||||
|
self.read_image_at(value_off, value_len as usize, &mut out)?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the log's window back into `buf` — used to hand the log to the merge helpers, which
|
||||||
|
/// take a slice of a log that has already been read.
|
||||||
|
fn log(&self) -> &[u8] {
|
||||||
|
self.log.as_slice()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Addressed {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// SAFETY: `self.file` came from filp_open and is closed exactly once, here; the owner
|
||||||
|
// argument is only meaningful for locks that nobody holds.
|
||||||
|
unsafe { bindings::filp_close(self.file, core::ptr::null_mut()) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `CUBE_OP_GET` against a v3 image: the log's newest word, else a binary search.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `buf` must hold `len` writable bytes.
|
||||||
|
unsafe fn v3_get(mut image: Addressed, sp: [u8; SPACE_ID_LEN], key: [u8; RAW_KEY_LEN], buf: *mut u8, len: usize) -> isize {
|
||||||
|
// The log is owned for the duration: what it says borrows it, and reading the image needs
|
||||||
|
// `&mut image`. It is what a checkpoint has not folded, so it is small.
|
||||||
|
let mut log = KVVec::<u8>::new();
|
||||||
|
if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() {
|
||||||
|
return -12; // -ENOMEM
|
||||||
|
}
|
||||||
|
match log_effect(log.as_slice(), &sp, &key) {
|
||||||
|
Some(Effect::Delete) => return -2, // -ENOENT
|
||||||
|
Some(Effect::Write(value)) => return copy_out(value, buf, len),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
let (first, records) = match image.space_entry(&sp) {
|
||||||
|
Ok(Some(entry)) => entry,
|
||||||
|
Ok(None) => return -2,
|
||||||
|
Err(e) => return -(e.to_errno() as isize),
|
||||||
|
};
|
||||||
|
let (value_off, value_len) = match image.find(&key, first, records) {
|
||||||
|
Ok(Some(entry)) => entry,
|
||||||
|
Ok(None) => return -2,
|
||||||
|
Err(e) => return -(e.to_errno() as isize),
|
||||||
|
};
|
||||||
|
let value = match image.value_at(value_off, value_len) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return -(e.to_errno() as isize),
|
||||||
|
};
|
||||||
|
copy_out(value.as_slice(), buf, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `CUBE_OP_SPACES` against a v3 image: the space table, and whatever only the log writes.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `space_out` must point to 32 writable bytes.
|
||||||
|
unsafe fn v3_spaces(mut image: Addressed, cursor: u64, space_out: *mut u8) -> i32 {
|
||||||
|
let mut log_only = KVVec::<[u8; SPACE_ID_LEN]>::new();
|
||||||
|
if log_spaces(image.log(), &mut log_only).is_err() {
|
||||||
|
return -12;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The table is sorted and fixed-size: the cursor is an index into the *store's* spaces, which
|
||||||
|
// is the table's spaces merged with the log's. Reading the table a row at a time is a few dozen
|
||||||
|
// reads of 48 bytes, and it keeps the merge in one order.
|
||||||
|
let mut index: u64 = 0;
|
||||||
|
let mut log_at = 0usize;
|
||||||
|
let mut buf = KVVec::<u8>::new();
|
||||||
|
let mut row: u64 = 0;
|
||||||
|
while row < image.header.space_count {
|
||||||
|
if image.read_image_at(HEADER_LEN_V3 as u64 + row * SPACE_ENTRY as u64, SPACE_ENTRY, &mut buf).is_err() {
|
||||||
|
return -12;
|
||||||
|
}
|
||||||
|
let mut space = [0u8; SPACE_ID_LEN];
|
||||||
|
space.copy_from_slice(&buf.as_slice()[..SPACE_ID_LEN]);
|
||||||
|
while log_at < log_only.len() && log_only.as_slice()[log_at] < space {
|
||||||
|
let candidate = log_only.as_slice()[log_at];
|
||||||
|
log_at += 1;
|
||||||
|
if !space_has_records_v3(&mut image, &candidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if index == cursor {
|
||||||
|
// SAFETY: the caller guarantees 32 writable bytes at `space_out`.
|
||||||
|
unsafe { core::ptr::copy_nonoverlapping(candidate.as_ptr(), space_out, SPACE_ID_LEN) };
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
if log_at < log_only.len() && log_only.as_slice()[log_at] == space {
|
||||||
|
log_at += 1;
|
||||||
|
}
|
||||||
|
row += 1;
|
||||||
|
if !space_has_records_v3(&mut image, &space) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if index == cursor {
|
||||||
|
// SAFETY: as above.
|
||||||
|
unsafe { core::ptr::copy_nonoverlapping(space.as_ptr(), space_out, SPACE_ID_LEN) };
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
while log_at < log_only.len() {
|
||||||
|
let candidate = log_only.as_slice()[log_at];
|
||||||
|
log_at += 1;
|
||||||
|
if !space_has_records_v3(&mut image, &candidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if index == cursor {
|
||||||
|
// SAFETY: as above.
|
||||||
|
unsafe { core::ptr::copy_nonoverlapping(candidate.as_ptr(), space_out, SPACE_ID_LEN) };
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
-2 // -ENOENT: no such space; the walk is finished
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does a v3 image's space hold a live record? One index entry, not a listing.
|
||||||
|
fn space_has_records_v3(image: &mut Addressed, space: &[u8; SPACE_ID_LEN]) -> bool {
|
||||||
|
match image.space_entry(space) {
|
||||||
|
Ok(Some((_, records))) => records > 0,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `CUBE_OP_ENUM` against a v3 image.
|
||||||
|
///
|
||||||
|
/// With no log edits for the space — the ordinary case, and always the case just after a
|
||||||
|
/// checkpoint — the cursor is a count of records already returned and the index is fixed-size, so
|
||||||
|
/// the batch starts at `first + cursor`: arithmetic, and the cost is the records returned. When the
|
||||||
|
/// log does hold edits, they interleave with the image's records and the walk has to merge them, so
|
||||||
|
/// it streams the index from the space's start and counts.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `buf` must hold `cap` writable bytes; `out_len` and `out_cursor` must point to writable `u64`s.
|
||||||
|
unsafe fn v3_enum(
|
||||||
|
mut image: Addressed,
|
||||||
|
wanted: [u8; SPACE_ID_LEN],
|
||||||
|
cursor: u64,
|
||||||
|
buf: *mut u8,
|
||||||
|
cap: usize,
|
||||||
|
out_len: *mut u64,
|
||||||
|
out_cursor: *mut u64,
|
||||||
|
) -> i32 {
|
||||||
|
let (first, records) = match image.space_entry(&wanted) {
|
||||||
|
Ok(Some(entry)) => entry,
|
||||||
|
Ok(None) => (0, 0),
|
||||||
|
Err(e) => return -(e.to_errno() as i32),
|
||||||
|
};
|
||||||
|
// Own the log: the edits borrow it, and walking the image needs `&mut image`.
|
||||||
|
let mut log = KVVec::<u8>::new();
|
||||||
|
if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() {
|
||||||
|
return -12; // -ENOMEM
|
||||||
|
}
|
||||||
|
let mut edits = KVVec::<LogEdit<'_>>::new();
|
||||||
|
if log_edits(log.as_slice(), &wanted, &mut edits).is_err() {
|
||||||
|
return -12;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: the shim guarantees `cap` writable bytes at `buf`.
|
||||||
|
let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) };
|
||||||
|
let mut batch = Batch {
|
||||||
|
out,
|
||||||
|
written: 0,
|
||||||
|
returned: 0,
|
||||||
|
seen: cursor,
|
||||||
|
cursor,
|
||||||
|
too_big: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if edits.is_empty() {
|
||||||
|
// Straight through the index, from the record the cursor names, reading a page of index
|
||||||
|
// entries at a time and then the span of values they point at.
|
||||||
|
let mut entry = KVVec::<u8>::new();
|
||||||
|
let mut at = first + cursor;
|
||||||
|
while at < first + records {
|
||||||
|
let (value_off, value_len) = match image.index_entry(at, &mut entry) {
|
||||||
|
Ok(pair) => pair,
|
||||||
|
Err(e) => return -(e.to_errno() as i32),
|
||||||
|
};
|
||||||
|
let value = match image.value_at(value_off, value_len) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return -(e.to_errno() as i32),
|
||||||
|
};
|
||||||
|
let key: &[u8; RAW_KEY_LEN] = match entry.as_slice()[..RAW_KEY_LEN].try_into() {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(_) => return -22,
|
||||||
|
};
|
||||||
|
if !batch.offer(key, value.as_slice()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
at += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The log's edits interleave, so the merged order is what the cursor counts. Stream the
|
||||||
|
// space's index entries rather than all of them: the space is what is being listed.
|
||||||
|
let mut entry = KVVec::<u8>::new();
|
||||||
|
let mut at = first;
|
||||||
|
let mut edit_at = 0usize;
|
||||||
|
loop {
|
||||||
|
while edit_at + 1 < edits.len() && edits[edit_at + 1].key == edits[edit_at].key {
|
||||||
|
edit_at += 1;
|
||||||
|
}
|
||||||
|
let edit = edits.as_slice().get(edit_at).copied();
|
||||||
|
let image_entry = if at < first + records {
|
||||||
|
match image.index_entry(at, &mut entry) {
|
||||||
|
Ok(pair) => Some(pair),
|
||||||
|
Err(e) => return -(e.to_errno() as i32),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let image_key: Option<&[u8; RAW_KEY_LEN]> = match image_entry {
|
||||||
|
Some(_) => entry.as_slice()[..RAW_KEY_LEN].try_into().ok(),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
match (edit, image_entry, image_key) {
|
||||||
|
(Some(e), Some((_, value_len)), Some(key)) if e.key == key => {
|
||||||
|
let _ = value_len;
|
||||||
|
edit_at += 1;
|
||||||
|
at += 1;
|
||||||
|
if !e.deleted && !batch.offer(e.key, e.value) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(e), Some((value_off, value_len)), Some(key)) if e.key < key => {
|
||||||
|
edit_at += 1;
|
||||||
|
if !e.deleted && !batch.offer(e.key, e.value) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let _ = (value_off, value_len);
|
||||||
|
}
|
||||||
|
(_, Some((value_off, value_len)), Some(_)) => {
|
||||||
|
let value = match image.value_at(value_off, value_len) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return -(e.to_errno() as i32),
|
||||||
|
};
|
||||||
|
let key = match entry.as_slice()[..RAW_KEY_LEN].try_into() {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(_) => return -22,
|
||||||
|
};
|
||||||
|
at += 1;
|
||||||
|
if !batch.offer(key, value.as_slice()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(e), None, _) => {
|
||||||
|
edit_at += 1;
|
||||||
|
if !e.deleted && !batch.offer(e.key, e.value) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, None, _) => break,
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: both out-pointers are writable under this function's contract.
|
||||||
|
unsafe {
|
||||||
|
if batch.too_big > 0 && batch.written == 0 {
|
||||||
|
*out_len = batch.too_big as u64;
|
||||||
|
*out_cursor = cursor;
|
||||||
|
return -34; // -ERANGE
|
||||||
|
}
|
||||||
|
*out_len = batch.written as u64;
|
||||||
|
*out_cursor = cursor + batch.returned;
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
///
|
///
|
||||||
/// Returns the record's length, or a negative errno. When the value does not fit in `len` the
|
/// Returns the record's length, or a negative errno. When the value does not fit in `len` the
|
||||||
/// bytes are *not* copied and the length is returned anyway, so the caller can size its buffer
|
/// bytes are *not* copied and the length is returned anyway, so the caller can size its buffer
|
||||||
@@ -2017,6 +2553,13 @@ pub unsafe extern "C" fn cubelinux_kernel_get(
|
|||||||
len: usize,
|
len: usize,
|
||||||
) -> isize {
|
) -> isize {
|
||||||
let (sp, key) = unsafe { coord_key(space, x, y, z) };
|
let (sp, key) = unsafe { coord_key(space, x, y, z) };
|
||||||
|
// A v3 image is addressed, so a read is a binary search and a few bytes. v1 and v2 images are
|
||||||
|
// packed lists, which the walking reader below handles.
|
||||||
|
match Addressed::open() {
|
||||||
|
Ok(Some(image)) => return unsafe { v3_get(image, sp, key, buf, len) },
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(e) => return -(e.to_errno() as isize),
|
||||||
|
}
|
||||||
let view = match read_view() {
|
let view = match read_view() {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => return -(e.to_errno() as isize),
|
Err(e) => return -(e.to_errno() as isize),
|
||||||
@@ -2094,6 +2637,14 @@ pub unsafe extern "C" fn cubelinux_kernel_enum(
|
|||||||
// SAFETY: the caller guarantees 32 readable bytes at `space`.
|
// SAFETY: the caller guarantees 32 readable bytes at `space`.
|
||||||
unsafe { core::ptr::copy_nonoverlapping(space, wanted.as_mut_ptr(), SPACE_ID_LEN) };
|
unsafe { core::ptr::copy_nonoverlapping(space, wanted.as_mut_ptr(), SPACE_ID_LEN) };
|
||||||
|
|
||||||
|
match Addressed::open() {
|
||||||
|
Ok(Some(image)) => {
|
||||||
|
return unsafe { v3_enum(image, wanted, cursor, buf, cap, out_len, out_cursor) }
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(e) => return -(e.to_errno() as i32),
|
||||||
|
}
|
||||||
|
|
||||||
let view = match read_view() {
|
let view = match read_view() {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => return -(e.to_errno() as i32),
|
Err(e) => return -(e.to_errno() as i32),
|
||||||
@@ -2147,6 +2698,11 @@ pub unsafe extern "C" fn cubelinux_kernel_enum(
|
|||||||
/// `space_out` must point to 32 writable bytes.
|
/// `space_out` must point to 32 writable bytes.
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn cubelinux_kernel_spaces(cursor: u64, space_out: *mut u8) -> i32 {
|
pub unsafe extern "C" fn cubelinux_kernel_spaces(cursor: u64, space_out: *mut u8) -> i32 {
|
||||||
|
match Addressed::open() {
|
||||||
|
Ok(Some(image)) => return unsafe { v3_spaces(image, cursor, space_out) },
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(e) => return -(e.to_errno() as i32),
|
||||||
|
}
|
||||||
let view = match read_view() {
|
let view = match read_view() {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => return -(e.to_errno() as i32),
|
Err(e) => return -(e.to_errno() as i32),
|
||||||
|
|||||||
Reference in New Issue
Block a user