You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This patch contains basic sequence number initialization and allocation logic. For now, the logic is synchronous. Eventually we can move it to a separate thread so that we are unlikely to block on sequence number allocation.
Copy file name to clipboardExpand all lines: log/rfcs/0001-storage.md
+55-21Lines changed: 55 additions & 21 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,28 +45,29 @@ This encoding preserves lexicographic key ordering, enabling key-range scans. En
45
45
46
46
#### TerminatedBytes
47
47
48
-
A `TerminatedBytes` is a variable-length byte sequence that terminates with a `0xFF` delimiter. This delimiter provides an unambiguous boundary between the user key and the sequence number, which is necessary for correct lexicographic ordering when keys have variable length.
48
+
A `TerminatedBytes` is a variable-length byte sequence that terminates with a `0x00` delimiter. This delimiter provides an unambiguous boundary between the user key and the sequence number, which is necessary for correct lexicographic ordering when keys have variable length.
49
49
50
-
To allow arbitrary byte sequences in user keys (including `0xFE` and `0xFF`), the key bytes are escaped before encoding:
50
+
To allow arbitrary byte sequences in user keys (including `0x00`, `0x01`, and `0xFF`), the key bytes are escaped before encoding:
51
51
52
52
| Raw Byte | Encoded As |
53
53
|----------|--------------|
54
-
|`0xFE`|`0xFE 0x00`|
55
-
|`0xFF`|`0xFE 0x01`|
54
+
|`0x00`|`0x01 0x01`|
55
+
|`0x01`|`0x01 0x02`|
56
+
|`0xFF`|`0x01 0x03`|
56
57
| other | unchanged |
57
58
58
-
The escape character `0xFE` is always followed by either `0x00` (representing a literal `0xFE`) or `0x01` (representing a literal `0xFF`). After escaping, the `0xFF` byte only appears as the terminating delimiter.
59
+
The escape character `0x01` is always followed by `0x01` (literal `0x00`), `0x02` (literal `0x01`), or `0x03` (literal `0xFF`). After escaping, `0x00`only appears as the terminating delimiter, and `0xFF` is reserved for range query bounds (see [Prefix-Based Range Queries](#prefix-based-range-queries)).
59
60
60
61
**Example:**
61
62
62
63
For a user key `hello` (no special bytes):
63
64
```
64
-
Encoded: | h | e | l | l | o | 0xFF |
65
+
Encoded: | h | e | l | l | o | 0x00 |
65
66
```
66
67
67
-
For a user key containing `0xFE`and `0xFF` bytes (`a 0xFE b 0xFF c`):
68
+
For a user key containing `0x00`, `0x01`, and `0xFF` bytes (`a 0x00 b 0x01 c 0xFF d`):
68
69
```
69
-
Encoded: | a | 0xFE | 0x00 | b | 0xFE | 0x01 | c | 0xFF |
70
+
Encoded: | a | 0x01 | 0x01 | b | 0x01 | 0x02 | c | 0x01 | 0x03 | d | 0x00 |
70
71
```
71
72
72
73
This encoding preserves lexicographic ordering: if key A < key B in their raw form, then their escaped forms maintain the same ordering. The delimiter ensures that no key can be a prefix of another key's encoding, preventing interleaving of entries from different logs.
@@ -77,22 +78,54 @@ With variable-length keys, the boundary between key and sequence number would be
77
78
78
79
For example, consider keys `a` and `ab` with sequence numbers. Without delimiting:
79
80
```
80
-
Key "a" + seq 0x0100: | a | 0x00 | ... | 0x01 | 0x00 |
81
+
Key "a" + seq 0x6200: | a | 0x00 | ... | 0x62 | 0x00 |
81
82
Key "ab" + seq 0x0001: | a | b | ... | 0x00 | 0x01 |
82
83
```
83
84
84
85
Depending on the sequence number bytes, entries from `a` and `ab` could interleave in unexpected ways.
85
86
86
-
The `TerminatedBytes` encoding solves this by inserting a `0xFF` delimiter after the escaped key bytes. Since `0xFF` is the highest byte value and only appears as the delimiter (never within the escaped key), all entries for a given key are guaranteed to be contiguous and ordered by sequence number.
87
+
The `TerminatedBytes` encoding solves this by inserting a `0x00` delimiter after the escaped key bytes. Since `0x00` is the lowest byte value and only appears as the delimiter (never within the escaped key), all entries for a given key are guaranteed to be contiguous and ordered by sequence number.
87
88
88
89
With `TerminatedBytes`:
89
90
```
90
-
Key "a" + seq: | a | 0xFF | <sequence bytes> |
91
-
Key "ab" + seq: | a | b | 0xFF | <sequence bytes> |
91
+
Key "a" + seq: | a | 0x00 | <sequence bytes> |
92
+
Key "ab" + seq: | a | b | 0x00 | <sequence bytes> |
92
93
```
93
94
94
95
All entries for key `a` sort before all entries for key `ab`, and within each key, entries are ordered by sequence number.
95
96
97
+
#### Prefix-Based Range Queries
98
+
99
+
Using `0x00` as the terminator (the lowest byte value) ensures that shorter keys sort before longer keys with the same prefix. For example, `/foo` sorts before `/foo/bar`:
100
+
101
+
```
102
+
Key "/foo" encoded: | / | f | o | o | 0x00 |
103
+
Key "/foo/bar" encoded: | / | f | o | o | / | b | a | r | 0x00 |
104
+
```
105
+
106
+
At the comparison point after `foo`, the terminator `0x00` is less than `/` (`0x2F`), so `/foo` < `/foo/bar`.
107
+
108
+
This ordering simplifies prefix-based range queries. To scan all keys with a given prefix, the range bounds are:
109
+
110
+
-**Start (inclusive)**: `prefix + 0x00` — the exact prefix key (smallest key with this prefix)
111
+
-**End (exclusive)**: `prefix + 0xFF` — beyond all keys with this prefix
112
+
113
+
For example, to scan all keys starting with `/foo`:
114
+
```
115
+
Start: | / | f | o | o | 0x00 | (exact match "/foo")
116
+
End: | / | f | o | o | 0xFF | (beyond all "/foo*" keys)
117
+
```
118
+
119
+
This range `[start, end)` includes:
120
+
-`/foo` (exact match, encoded as `| / | f | o | o | 0x00 |`)
121
+
-`/foo/bar` (encoded as `| / | f | o | o | / | b | a | r | 0x00 |`)
122
+
-`/foobar` (encoded as `| / | f | o | o | b | a | r | 0x00 |`)
123
+
124
+
But excludes:
125
+
-`/bar` (does not start with `/foo`)
126
+
127
+
This works because after escaping, no encoded key contains `0x00` or `0xFF` except as control bytes. Any key with prefix `/foo` will have an encoding that starts with `| / | f | o | o |` followed by either `0x00` (exact match) or a byte in the range `0x02`–`0xFE` (longer key, possibly with escape sequences). Since all these values are less than `0xFF`, the end bound correctly excludes keys that don't share the prefix.
128
+
96
129
### Sequence Numbers
97
130
98
131
Sequence numbers are assigned from a single counter that is maintained by the SlateDB writer and is incremented after every append. Each key's log is monotonically ordered by sequence number, but the sequence numbers are not contiguous—other keys' appends are interleaved in the global sequence. Additionally, sequence numbers may have gaps due to crash recovery (see below). The only guarantee is monotonicity: within a key's log, sequence numbers are strictly increasing.
@@ -105,30 +138,30 @@ If SlateDB supports multi-writer in the future, each writer would maintain its o
105
138
106
139
To efficiently track and recover the sequence counter, the writer uses block-based allocation. Rather than persisting the sequence number after every append, the writer pre-allocates a block of sequence numbers and records the allocation in the LSM.
107
140
108
-
A new record type `LastBlock` (type discriminator `0x02`) stores the current allocation:
141
+
A new record type `SeqBlock` (type discriminator `0x02`) stores the current allocation:
The `LastBlock` key is static—it contains only the version and type discriminator with no user key component. This ensures there is exactly one such record in the database.
149
+
The `SeqBlock` key is static—it contains only the version and type discriminator with no user key component. This ensures there is exactly one such record in the database.
117
150
118
151
**Allocation procedure:**
119
152
120
-
1. On initialization, the writer reads the `LastBlock` record (if present) to determine the last allocated range `[base, base + size)`.
121
-
2. The writer allocates a new block starting at `base + size` and writes a new `LastBlock` record before processing any appends.
153
+
1. On initialization, the writer reads the `SeqBlock` record (if present) to determine the last allocated range `[base, base + size)`.
154
+
2. The writer allocates a new block starting at `base + size` and writes a new `SeqBlock` record before processing any appends.
122
155
3. During normal operation, the writer assigns sequence numbers from the current block, incrementing after each append.
123
-
4. When the current block is exhausted, the writer allocates a new block and writes an updated `LastBlock` record.
156
+
4. When the current block is exhausted, the writer allocates a new block and writes an updated `SeqBlock` record.
124
157
125
158
**Recovery:**
126
159
127
-
On crash recovery, the writer reads the `LastBlock` record and allocates a fresh block starting after the previous range. Any sequence numbers that were allocated but not used before the crash are simply skipped. This may create gaps in the sequence space, but monotonicity is preserved.
160
+
On crash recovery, the writer reads the `SeqBlock` record and allocates a fresh block starting after the previous range. Any sequence numbers that were allocated but not used before the crash are simply skipped. This may create gaps in the sequence space, but monotonicity is preserved.
128
161
129
162
**Block sizing:**
130
163
131
-
The block size is an internal implementation detail and is not exposed through configuration. The implementation may vary the block size to balance write amplification (larger blocks reduce `LastBlock` write frequency) against sequence space efficiency (smaller blocks waste fewer sequence numbers on crash).
164
+
The block size is an internal implementation detail and is not exposed through configuration. The implementation may vary the block size to balance write amplification (larger blocks reduce `SeqBlock` write frequency) against sequence space efficiency (smaller blocks waste fewer sequence numbers on crash).
132
165
133
166
### SST Representation
134
167
@@ -287,7 +320,7 @@ The simpler key+sequence encoding preserves key ordering and avoids the collisio
287
320
288
321
### SlateDB Sequence Numbers
289
322
290
-
SlateDB maintains its own internal sequence number for MVCC versioning. Ideally, OpenData-Log could reuse this counter rather than implementing separate tracking with `LastBlock` records. However, SlateDB's sequence number is not currently exposed in its public API. Furthermore, since the sequence number is embedded into the key, we would need to align the sequence number prior to writing. Current proposals to expose SlateDb sequence numbers do not offer such a mechanism, but we can reevaluate once the effort is complete. Finally, we should reserve the ability to offer an `edit` API so that a corrupt/incorrect record may be overwritten. This may be more difficult if we are too coupled with the SlateDb sequence number.
323
+
SlateDB maintains its own internal sequence number for MVCC versioning. Ideally, OpenData-Log could reuse this counter rather than implementing separate tracking with `SeqBlock` records. However, SlateDB's sequence number is not currently exposed in its public API. Furthermore, since the sequence number is embedded into the key, we would need to align the sequence number prior to writing. Current proposals to expose SlateDb sequence numbers do not offer such a mechanism, but we can reevaluate once the effort is complete. Finally, we should reserve the ability to offer an `edit` API so that a corrupt/incorrect record may be overwritten. This may be more difficult if we are too coupled with the SlateDb sequence number.
291
324
292
325
### Headers
293
326
@@ -301,3 +334,4 @@ Messaging systems often expose a way to attach headers to messages in order to e
0 commit comments