rhdl_ubft/kv/kvstore.rs
1//! An explicit, bounded compatibility frontend for the current replica.
2//!
3//! CPU word: opcode[1:0], value[31:2], key[63:32]. Opcode one is PUT;
4//! other opcodes remain GET as upstream. PUT supports key16/value16 and
5//! rejects wider values rather than silently truncating. GET uses key32
6//! zero-extended to the table's key64. All response fields are initialized.
7//!
8//! The current controller duplicates each `(value32, sequence32)` decision
9//! onto output and remote. This adapter buffers both, verifies equality,
10//! and applies each accepted sequence once within a coordinated session.
11//! Only repeats of the latest applied sequence/value are drained; older replays
12//! fault. Reset discards this deduplication history. This is not an adapter for
13//! historical mutually exclusive local/remote ownership. Sequence zero,
14//! gaps, backwards movement, conflicting duplicates, and malformed beats
15//! fault closed. A session must reset before sequence wrap; reset must also
16//! reset/drain the table, controller, and all transport queues together. The
17//! first nonzero sequence is accepted: attaching mid-history requires an
18//! externally established consistent snapshot, not just a plausible sequence.
19//!
20//! Transfers occur on rising edges with valid and ready asserted. Producers
21//! hold valid and payload until transfer during healthy operation. A terminal
22//! fault requires coordinated recovery; reset suppresses all handshakes.
23//!
24//! One local PUT may wait while remote decisions continue to apply. A matching
25//! decided value completes that PUT, but the wire has no client/request ID:
26//! this proves value convergence, not attribution to a particular proposal.
27//! GET is a local read, not a linearizable distributed read. A successful PUT
28//! response says the table update completed, not that the protocol is safe.
29use super::hash_table::{HashTableIn, HashTableOut, LookupReq, UpdateReq};
30use rhdl::prelude::*;
31
32/// One 256-bit controller decision. Reserved lanes must be zero, last true.
33#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
34pub struct Decision {
35 /// Decision offered; hold payload and last until the associated ready.
36 pub valid: bool,
37 /// Lane0: key in bits15:0, value in bits31:16, sequence in bits63:32.
38 /// Lanes1 through 3 must be zero.
39 pub data: [b64; 4],
40 /// Must be true: every decision is exactly one beat.
41 pub last: bool,
42}
43
44/// Observable CPU result. Status: 0 success, 1 PUT range error,
45/// 2 table failure. Protocol fault is reported separately and is sticky.
46#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
47pub struct KvResult {
48 /// Result held until result_ready; no second local operation is admitted meanwhile.
49 pub valid: bool,
50 /// Original CPU opcode; one is PUT, all other encodings are GET.
51 pub opcode: b2,
52 /// Original full 32-bit CPU key.
53 pub key: b32,
54 /// GET value or zero on miss; PUT attempted value after table response,
55 /// including table failure. Range rejection returns zero.
56 pub value: b16,
57 /// GET found the key; false for every PUT result.
58 pub hit: bool,
59 /// GET completed (hit is separate), or a matching PUT decision was installed.
60 pub success: bool,
61 /// Zero completion, one PUT range rejection, two table insertion failure.
62 pub status: b2,
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
66/// CPU, controller, and table inputs for one outstanding local operation.
67pub struct KvIn {
68 /// CPU operation offered; retain it until operation_ready.
69 pub operation_valid: bool,
70 /// CPU word: key63:32, value31:2, opcode1:0.
71 pub operation: b64,
72 /// CPU accepts a held result this edge.
73 pub result_ready: bool,
74 /// Controller accepts the offered group-zero proposal this edge.
75 pub proposal_ready: bool,
76 /// Controller output copy of the decision.
77 pub output: Decision,
78 /// Controller remote copy, paired and checked against output.
79 pub remote: Decision,
80 /// Responses and admission readiness from the attached table.
81 pub table: HashTableOut,
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
85/// CPU results and requests to the external controller and local table.
86pub struct KvOut {
87 /// Accepts one CPU operation; low while local work/result is pending or faulted.
88 pub operation_ready: bool,
89 /// Held observable CPU completion or explicit range/table error.
90 pub result: KvResult,
91 /// A group-zero PUT proposal is offered; retained until proposal_ready.
92 pub proposal_valid: bool,
93 /// ProposedValue: low32 = key16 | value16<<16, high32 = group zero.
94 pub proposal: b64,
95 /// Room for one output copy; independent of the local PUT wait.
96 pub output_ready: bool,
97 /// Room for one remote copy; applying waits for both matching copies.
98 pub remote_ready: bool,
99 /// Lookup/update requests and response-consumer readiness.
100 pub table: HashTableIn,
101 /// Terminal until coordinated reset; stops new requests and application.
102 /// An already-held result remains consumable. Pending work need not complete.
103 pub fault: bool,
104 /// Wrapping diagnostic counters, not safety identifiers.
105 /// Wrapping count of successfully applied sequences, including upserts.
106 pub applied: b32,
107 /// Wrapping count of paired repeats of the most recently applied sequence/value.
108 pub duplicates: b32,
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
112/// Separate local-operation and replicated-application state machines.
113pub struct KvState {
114 /// 0 idle, 1 offer proposal, 2 await matching decision, 3 offer lookup,
115 /// 4 await lookup result. This machine serves one local CPU operation.
116 pub local_phase: b3,
117 /// 0 await paired decisions, 1 offer table update, 2 await update result.
118 /// It advances independently of the local CPU machine.
119 pub apply_phase: b2,
120 /// Original key of the outstanding CPU operation.
121 pub cpu_key: b32,
122 /// Original opcode of the outstanding CPU operation.
123 pub cpu_opcode: b2,
124 /// Packed key16/value16 offered for the local PUT.
125 pub proposed: b32,
126 /// CPU result retained until its consumer accepts it.
127 pub result: KvResult,
128 /// One buffered controller output copy.
129 pub output: Decision,
130 /// One buffered controller remote copy.
131 pub remote: Decision,
132 /// Packed key16/value16 of the decision currently entering the table.
133 pub applying: b32,
134 /// Sequence belonging to applying.
135 pub applying_seq: b32,
136 /// A table update has completed successfully in this session.
137 pub have_last: bool,
138 /// Most recently applied sequence; meaningful when have_last.
139 pub last_seq: b32,
140 /// Value paired with last_seq, used to check immediate replays.
141 pub last_value: b32,
142 /// Latched terminal framing, sequence, response, or application error.
143 pub fault: bool,
144 /// Wrapping successful-application diagnostic counter.
145 pub applied: b32,
146 /// Wrapping accepted immediate-replay diagnostic counter.
147 pub duplicates: b32,
148}
149
150/// Lossless-within-profile replacement for the upstream KV frontend.
151#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
152pub struct LegacyKv16 {
153 state: rhdl_fpga::core::dff::DFF<KvState>,
154}
155impl Default for LegacyKv16 {
156 fn default() -> Self {
157 Self {
158 state: rhdl_fpga::core::dff::DFF::new(KvState::default()),
159 }
160 }
161}
162impl SynchronousIO for LegacyKv16 {
163 type I = KvIn;
164 type O = KvOut;
165 type Kernel = kv_kernel;
166}
167
168/// CPU phases: idle, propose, wait-decision, lookup-request, lookup-response.
169/// Apply phases: idle, update-request, update-response.
170#[kernel]
171pub fn kv_kernel(cr: ClockReset, i: KvIn, q: LegacyKv16Q) -> (KvOut, LegacyKv16D) {
172 // r is this cycle's registered snapshot; s is the next snapshot. All state
173 // tests use r, so accepting a proposal cannot also complete it in this edge.
174 let r = q.state;
175 let mut s = r;
176 let mut o = KvOut {
177 operation_ready: false,
178 result: r.result,
179 proposal_valid: false,
180 proposal: r.proposed.resize::<64>(),
181 output_ready: false,
182 remote_ready: false,
183 table: HashTableIn {
184 lookup: LookupReq {
185 valid: false,
186 key: b64(0),
187 source: false,
188 },
189 update: UpdateReq {
190 valid: false,
191 op: b1(0),
192 key: b64(0),
193 value: b16(0),
194 source: true,
195 },
196 lookup_response_ready: false,
197 update_response_ready: false,
198 },
199 fault: r.fault,
200 applied: r.applied,
201 duplicates: r.duplicates,
202 };
203 if r.result.valid && i.result_ready {
204 s.result.valid = false;
205 }
206 if !r.fault {
207 // One buffer per stream absorbs skew without assuming simultaneous
208 // arrival. A full buffer backpressures only its own input stream.
209 o.output_ready = !r.output.valid;
210 o.remote_ready = !r.remote.valid;
211 if o.output_ready && i.output.valid {
212 s.output = i.output;
213 }
214 if o.remote_ready && i.remote.valid {
215 s.remote = i.remote;
216 }
217
218 o.operation_ready = r.local_phase == b3(0) && !r.result.valid && i.table.initialized;
219 if o.operation_ready && i.operation_valid {
220 let key: b32 = (i.operation >> b64(32)).resize();
221 let value: b30 = (i.operation >> b64(2)).resize();
222 let opcode: b2 = i.operation.resize();
223 s.cpu_key = key;
224 s.cpu_opcode = opcode;
225 if opcode == b2(1) {
226 if (key >> b32(16)) != b32(0) || (value >> b30(16)) != b30(0) {
227 s.result = KvResult {
228 valid: true,
229 opcode,
230 key,
231 value: b16(0),
232 hit: false,
233 success: false,
234 status: b2(1),
235 };
236 } else {
237 s.proposed = key | (value.resize::<32>() << b32(16));
238 s.local_phase = b3(1);
239 }
240 } else {
241 s.local_phase = b3(3);
242 }
243 }
244 if r.local_phase == b3(1) {
245 o.proposal_valid = true;
246 if i.proposal_ready {
247 s.local_phase = b3(2);
248 }
249 }
250
251 // Fair table arbitration is owned by HashTable; no local CPU wait
252 // prevents remote updates from reaching it.
253 if r.local_phase == b3(3) {
254 o.table.lookup = LookupReq {
255 valid: true,
256 key: r.cpu_key.resize::<64>(),
257 source: false,
258 };
259 if i.table.lookup_request_ready {
260 s.local_phase = b3(4);
261 }
262 }
263 if r.local_phase == b3(4) {
264 o.table.lookup_response_ready = true;
265 if i.table.lookup.valid {
266 if i.table.lookup.key != r.cpu_key.resize::<64>() || i.table.lookup.source {
267 s.fault = true;
268 } else {
269 s.result = KvResult {
270 valid: true,
271 opcode: r.cpu_opcode,
272 key: r.cpu_key,
273 value: if i.table.lookup.hit {
274 i.table.lookup.value
275 } else {
276 b16(0)
277 },
278 hit: i.table.lookup.hit,
279 success: true,
280 status: b2(0),
281 };
282 s.local_phase = b3(0);
283 }
284 }
285 }
286
287 if r.apply_phase == b2(0) && r.output.valid && r.remote.valid {
288 // The two ports carry copies of one decision, not two independent
289 // writes. Validate their full framing before touching table state.
290 let mut malformed = !r.output.last || !r.remote.last;
291 for lane in 0..4 {
292 if r.output.data[lane] != r.remote.data[lane] {
293 malformed = true;
294 }
295 if lane > 0 && r.remote.data[lane] != b64(0) {
296 malformed = true;
297 }
298 }
299 let seq: b32 = (r.remote.data[0] >> b64(32)).resize();
300 let value: b32 = r.remote.data[0].resize();
301 if seq == b32(0) {
302 malformed = true;
303 }
304 if r.have_last {
305 if seq == r.last_seq {
306 if value != r.last_value {
307 malformed = true;
308 }
309 } else if r.last_seq == b32(0xffff_ffff) || seq != r.last_seq + b32(1) {
310 malformed = true;
311 }
312 }
313 if malformed {
314 s.fault = true;
315 } else {
316 s.output.valid = false;
317 s.remote.valid = false;
318 if r.have_last && seq == r.last_seq {
319 // Only an identical replay of the last applied decision is
320 // harmless. Older decisions cannot silently rewind state.
321 s.duplicates = r.duplicates + b32(1);
322 } else {
323 s.applying = value;
324 s.applying_seq = seq;
325 s.apply_phase = b2(1);
326 }
327 }
328 }
329 if r.apply_phase == b2(1) {
330 let key: b16 = r.applying.resize();
331 let value: b16 = (r.applying >> b32(16)).resize();
332 o.table.update = UpdateReq {
333 valid: true,
334 op: b1(0),
335 key: key.resize::<64>(),
336 value,
337 source: true,
338 };
339 if i.table.update_request_ready {
340 s.apply_phase = b2(2);
341 }
342 }
343 if r.apply_phase == b2(2) {
344 o.table.update_response_ready = true;
345 if i.table.update.valid {
346 let key: b16 = r.applying.resize();
347 let value: b16 = (r.applying >> b32(16)).resize();
348 let malformed = i.table.update.op != b1(0)
349 || i.table.update.key != key.resize::<64>()
350 || i.table.update.value != value
351 || !i.table.update.source;
352 if malformed {
353 s.fault = true;
354 } else {
355 if !i.table.update.success {
356 s.fault = true;
357 }
358 if i.table.update.success {
359 // Advance replay protection after application succeeds,
360 // never merely when the controller announces a decision.
361 s.have_last = true;
362 s.last_seq = r.applying_seq;
363 s.last_value = r.applying;
364 s.applied = r.applied + b32(1);
365 }
366 if r.local_phase == b3(2) && r.proposed == r.applying {
367 // This is value matching, not requester identification:
368 // another client may have proposed the same PUT.
369 s.result = KvResult {
370 valid: true,
371 opcode: b2(1),
372 key: r.cpu_key,
373 value,
374 hit: false,
375 success: i.table.update.success,
376 status: if i.table.update.success { b2(0) } else { b2(2) },
377 };
378 s.local_phase = b3(0);
379 }
380 s.apply_phase = b2(0);
381 }
382 }
383 }
384 }
385 if cr.reset.any() {
386 o.operation_ready = false;
387 o.result.valid = false;
388 o.proposal_valid = false;
389 o.output_ready = false;
390 o.remote_ready = false;
391 o.table.lookup.valid = false;
392 o.table.update.valid = false;
393 o.table.lookup_response_ready = false;
394 o.table.update_response_ready = false;
395 }
396 (o, LegacyKv16D { state: s })
397}
398
399#[cfg(test)]
400#[path = "kvstore_tests.rs"]
401mod tests;