Skip to main content

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;