yo_common/addr.rs
1//! The 56 bit address: a 4 bit space and a 52 bit offset.
2//!
3//! This is the type that makes Y6 work. A vector's payload, a document's body,
4//! a graph node's properties and a Redis string are all reached through one
5//! index because the address says which world the offset lives in. None of the
6//! four models is layered on another, they just share an address width.
7//!
8//! 52 bits of offset is 4 PiB, which is past the point where a single file is
9//! the right answer, and 4 bits of space is 16 worlds against the 11 that exist.
10//! Both were sized once, here, and the sizes are checked by the tests below so
11//! that widening one later is a deliberate act.
12
13use core::fmt;
14
15/// Which world an [`Addr`] offset points into.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
17#[repr(u8)]
18#[non_exhaustive]
19pub enum Space {
20 /// The value is the 52 bits. Small integers and short strings live entirely
21 /// inside the index entry and cost zero dereferences. This is what Redis
22 /// calls `int` encoding and part of what it calls `embstr`.
23 Inline = 0,
24 /// A byte offset into the shard's arena segments (`05` section 3).
25 Arena = 1,
26 /// A byte offset into the shard's log region (`06` section 2).
27 Log = 2,
28 /// A hash, as an owner local element table.
29 Hash = 3,
30 /// A set, as an owner local element table or a dense member vector.
31 Set = 4,
32 /// A sorted set, as a counted B+ tree.
33 ZSet = 5,
34 /// A list, as a ring deque.
35 List = 6,
36 /// A stream, as a radix log.
37 Stream = 7,
38 /// A document, with its path indexes.
39 Doc = 8,
40 /// A vector partition set.
41 Vector = 9,
42 /// Graph adjacency.
43 Graph = 10,
44}
45
46impl Space {
47 /// Every space in numeric order.
48 pub const ALL: &'static [Space] = &[
49 Space::Inline,
50 Space::Arena,
51 Space::Log,
52 Space::Hash,
53 Space::Set,
54 Space::ZSet,
55 Space::List,
56 Space::Stream,
57 Space::Doc,
58 Space::Vector,
59 Space::Graph,
60 ];
61
62 /// The space for a raw 4 bit value, or `None` if nothing uses it yet.
63 #[inline]
64 pub const fn from_bits(bits: u8) -> Option<Space> {
65 match bits {
66 0 => Some(Space::Inline),
67 1 => Some(Space::Arena),
68 2 => Some(Space::Log),
69 3 => Some(Space::Hash),
70 4 => Some(Space::Set),
71 5 => Some(Space::ZSet),
72 6 => Some(Space::List),
73 7 => Some(Space::Stream),
74 8 => Some(Space::Doc),
75 9 => Some(Space::Vector),
76 10 => Some(Space::Graph),
77 _ => None,
78 }
79 }
80
81 /// The name that appears in `OBJECT ENCODING` style output and in errors.
82 #[inline]
83 pub const fn name(self) -> &'static str {
84 match self {
85 Space::Inline => "inline",
86 Space::Arena => "arena",
87 Space::Log => "log",
88 Space::Hash => "hash",
89 Space::Set => "set",
90 Space::ZSet => "zset",
91 Space::List => "list",
92 Space::Stream => "stream",
93 Space::Doc => "doc",
94 Space::Vector => "vector",
95 Space::Graph => "graph",
96 }
97 }
98}
99
100/// Bits of offset in an address.
101pub const OFFSET_BITS: u32 = 52;
102/// Bits of space in an address.
103pub const SPACE_BITS: u32 = 4;
104/// Total bits an address occupies in an index bucket.
105pub const ADDR_BITS: u32 = OFFSET_BITS + SPACE_BITS;
106
107/// The largest representable offset.
108pub const MAX_OFFSET: u64 = (1u64 << OFFSET_BITS) - 1;
109
110const OFFSET_MASK: u64 = MAX_OFFSET;
111
112/// A 56 bit address, held in the low 56 bits of a `u64`.
113///
114/// The zero address is reserved to mean "no entry" so that a cleared bucket
115/// needs no separate occupancy bit. That costs the `Inline` space its zero
116/// value, which is fine because an inline zero is stored as the integer 0 with
117/// a type tag rather than as a bare address.
118#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
119#[repr(transparent)]
120pub struct Addr(u64);
121
122impl Addr {
123 /// The absent address. A bucket entry holding this has no value.
124 pub const NONE: Addr = Addr(0);
125
126 /// Build an address.
127 ///
128 /// # Panics
129 ///
130 /// If `offset` does not fit in 52 bits. An offset that large means the file
131 /// has outgrown the address width, which is a design limit and not a
132 /// runtime condition, so it is a panic rather than an error value.
133 #[inline]
134 pub const fn new(space: Space, offset: u64) -> Addr {
135 assert!(offset <= MAX_OFFSET, "offset does not fit in 52 bits");
136 Addr(((space as u64) << OFFSET_BITS) | offset)
137 }
138
139 /// Build an address without checking the offset width.
140 ///
141 /// # Safety
142 ///
143 /// `offset` must be at or below [`MAX_OFFSET`]. Passing a wider value
144 /// silently corrupts the space bits, which sends a later read into the
145 /// wrong world.
146 #[inline]
147 pub const unsafe fn new_unchecked(space: Space, offset: u64) -> Addr {
148 Addr(((space as u64) << OFFSET_BITS) | offset)
149 }
150
151 /// The raw 56 bit value, as stored.
152 #[inline]
153 pub const fn to_bits(self) -> u64 {
154 self.0
155 }
156
157 /// Rebuild from a raw 56 bit value.
158 ///
159 /// Bits above 56 are dropped rather than trusted, because this value comes
160 /// off disk and a corrupt high byte should not become a wild pointer.
161 #[inline]
162 pub const fn from_bits(bits: u64) -> Addr {
163 Addr(bits & ((1u64 << ADDR_BITS) - 1))
164 }
165
166 /// Whether this address points at anything.
167 #[inline]
168 pub const fn is_none(self) -> bool {
169 self.0 == 0
170 }
171
172 /// Whether this address points at something.
173 #[inline]
174 pub const fn is_some(self) -> bool {
175 self.0 != 0
176 }
177
178 /// The offset part.
179 #[inline]
180 pub const fn offset(self) -> u64 {
181 self.0 & OFFSET_MASK
182 }
183
184 /// The raw space bits, before any check that they name a known space.
185 #[inline]
186 pub const fn space_bits(self) -> u8 {
187 (self.0 >> OFFSET_BITS) as u8
188 }
189
190 /// The space, or `None` if the bits name a space this build does not know.
191 ///
192 /// A file written by a newer release can carry a space we have never heard
193 /// of. That is a `VersionTooNew` condition for the caller to report, not a
194 /// panic, so this returns an option rather than unwrapping.
195 #[inline]
196 pub const fn space(self) -> Option<Space> {
197 Space::from_bits(self.space_bits())
198 }
199}
200
201impl fmt::Debug for Addr {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 if self.is_none() {
204 return f.write_str("Addr(none)");
205 }
206 match self.space() {
207 Some(s) => write!(f, "Addr({}+{:#x})", s.name(), self.offset()),
208 None => write!(f, "Addr(space{}+{:#x})", self.space_bits(), self.offset()),
209 }
210 }
211}
212
213/// Which shard owns a slot. Small because shard count is bounded by core count.
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
215#[repr(transparent)]
216pub struct ShardId(pub u16);
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn address_widths_are_what_the_bucket_assumes() {
224 // The index bucket packs seven addresses into 49 bytes and a link into
225 // 7 more. If this ever stops being 56, `05` section 2.1 is wrong.
226 assert_eq!(ADDR_BITS, 56);
227 assert_eq!(ADDR_BITS % 8, 0);
228 }
229
230 #[test]
231 fn round_trips_through_bits() {
232 for &space in Space::ALL {
233 for offset in [0u64, 1, 4096, MAX_OFFSET] {
234 let a = Addr::new(space, offset);
235 assert_eq!(a.offset(), offset);
236 assert_eq!(a.space(), Some(space));
237 assert_eq!(Addr::from_bits(a.to_bits()), a);
238 }
239 }
240 }
241
242 #[test]
243 fn zero_is_absent() {
244 assert!(Addr::NONE.is_none());
245 assert!(Addr::new(Space::Arena, 0).is_some());
246 assert!(Addr::new(Space::Inline, 1).is_some());
247 }
248
249 #[test]
250 fn unknown_space_reports_rather_than_panics() {
251 let a = Addr::from_bits(15u64 << OFFSET_BITS | 99);
252 assert_eq!(a.space(), None);
253 assert_eq!(a.space_bits(), 15);
254 assert_eq!(a.offset(), 99);
255 }
256
257 #[test]
258 fn high_byte_from_disk_is_dropped() {
259 let a = Addr::from_bits(0xFF00_0000_0000_0000 | 7);
260 assert_eq!(a.offset(), 7);
261 assert!(a.space_bits() <= 15);
262 }
263
264 #[test]
265 #[should_panic(expected = "52 bits")]
266 fn oversized_offset_panics() {
267 let _ = Addr::new(Space::Arena, MAX_OFFSET + 1);
268 }
269}