DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
dbm.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file dbm.cpp
6 * @brief Log-structured hash key-value store on the WAL (see dbm.h).
7 */
8
9#include "services/dbm/dbm.h"
10
11#if DETWS_ENABLE_DBM
12
13#include <string.h>
14
15namespace
16{
17// dbm record payload header: op u8 | key_len u16 | val_len u32.
18const size_t DBM_HDR = 7;
19
20void put_u16(uint8_t *p, uint16_t v)
21{
22 p[0] = (uint8_t)v;
23 p[1] = (uint8_t)(v >> 8);
24}
25void put_u32(uint8_t *p, uint32_t v)
26{
27 p[0] = (uint8_t)v;
28 p[1] = (uint8_t)(v >> 8);
29 p[2] = (uint8_t)(v >> 16);
30 p[3] = (uint8_t)(v >> 24);
31}
32uint16_t get_u16(const uint8_t *p)
33{
34 return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
35}
36uint32_t get_u32(const uint8_t *p)
37{
38 return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
39}
40
41// FNV-1a 64-bit over the key.
42uint64_t key_hash(const char *key, uint16_t len)
43{
44 uint64_t h = 0xcbf29ce484222325ULL;
45 for (uint16_t i = 0; i < len; i++)
46 {
47 h ^= (uint8_t)key[i];
48 h *= 0x100000001b3ULL;
49 }
50 return h;
51}
52
53// Find a live slot for (hash,key). Linear probe, stopping at the first empty. -1 if absent.
54int find_live(DetwsDbm *db, uint64_t hash, const char *key, uint16_t key_len)
55{
56 const size_t n = DETWS_DBM_SLOTS;
57 size_t start = (size_t)(hash % n);
58 for (size_t i = 0; i < n; i++)
59 {
60 size_t j = (start + i) % n;
61 DetwsDbmSlot *s = &db->slots[j];
62 if (s->state == 0)
63 return -1; // empty -> the probe chain ends, key not present
64 if (s->state == 1 && s->hash == hash && s->key_len == key_len && memcmp(s->key, key, key_len) == 0)
65 return (int)j;
66 }
67 return -1;
68}
69
70// Find the slot to write (hash,key): an existing live match, or the first reusable slot (tombstone/empty).
71// Sets *is_new when the returned slot is not already this key. -1 if the table has no room for a new key.
72int reserve(DetwsDbm *db, uint64_t hash, const char *key, uint16_t key_len, bool *is_new)
73{
74 const size_t n = DETWS_DBM_SLOTS;
75 size_t start = (size_t)(hash % n);
76 int first_free = -1;
77 for (size_t i = 0; i < n; i++)
78 {
79 size_t j = (start + i) % n;
80 DetwsDbmSlot *s = &db->slots[j];
81 if (s->state == 1)
82 {
83 if (s->hash == hash && s->key_len == key_len && memcmp(s->key, key, key_len) == 0)
84 {
85 *is_new = false;
86 return (int)j;
87 }
88 continue;
89 }
90 if (s->state == 2)
91 {
92 if (first_free < 0)
93 first_free = (int)j; // reusable tombstone; keep probing for a live match
94 continue;
95 }
96 // empty: the key is not present -> insert at the earliest reusable slot (tombstone or here)
97 *is_new = true;
98 return first_free >= 0 ? first_free : (int)j;
99 }
100 // No empty slot seen (table saturated with live/tombstones); only a tombstone can hold a new key.
101 *is_new = true;
102 return first_free;
103}
104
105struct ReplayCtx
106{
107 DetwsDbm *db;
108 bool overflow;
109};
110
111void replay_cb(uint64_t seq, uint64_t data_off, const uint8_t *payload, uint32_t len, void *ctx)
112{
113 (void)seq;
114 ReplayCtx *rc = (ReplayCtx *)ctx;
115 DetwsDbm *db = rc->db;
116 if (len < DBM_HDR)
117 return;
118 uint8_t op = payload[0];
119 uint16_t klen = get_u16(payload + 1);
120 uint32_t vlen = get_u32(payload + 3);
121 if (klen == 0 || klen > DETWS_DBM_KEY_MAX)
122 return;
123 if (DBM_HDR + (size_t)klen + vlen > len)
124 return; // truncated / malformed payload
125 const char *key = (const char *)(payload + DBM_HDR);
126 uint64_t h = key_hash(key, klen);
127 if (op == 0) // put
128 {
129 bool is_new = false;
130 int slot = reserve(db, h, key, klen, &is_new);
131 if (slot < 0)
132 {
133 rc->overflow = true;
134 return;
135 }
136 DetwsDbmSlot *s = &db->slots[slot];
137 if (s->state != 1)
138 db->count++;
139 s->state = 1;
140 s->hash = h;
141 s->key_len = klen;
142 memcpy(s->key, key, klen);
143 s->val_off = data_off + WAL_RECORD_HEADER + DBM_HDR + klen;
144 s->val_len = vlen;
145 }
146 else if (op == 1) // delete
147 {
148 int slot = find_live(db, h, key, klen);
149 if (slot >= 0)
150 {
151 db->slots[slot].state = 2;
152 db->count--;
153 }
154 }
155}
156} // namespace
157
158bool detws_dbm_open(DetwsDbm *db, WalStore *wal)
159{
160 memset(db, 0, sizeof(*db));
161 db->wal = wal;
162 ReplayCtx rc = {db, false};
163 uint8_t scratch[WAL_RECORD_HEADER + DBM_HDR + DETWS_DBM_KEY_MAX + DETWS_DBM_VAL_MAX];
164 wal_store_scan(wal, replay_cb, &rc, scratch, sizeof(scratch));
165 return !rc.overflow;
166}
167
168bool detws_dbm_put(DetwsDbm *db, const char *key, uint16_t key_len, const uint8_t *val, uint32_t val_len)
169{
170 if (key_len == 0 || key_len > DETWS_DBM_KEY_MAX || val_len > DETWS_DBM_VAL_MAX)
171 return false;
172 uint64_t h = key_hash(key, key_len);
173 bool is_new = false;
174 int slot = reserve(db, h, key, key_len, &is_new);
175 if (slot < 0)
176 return false; // index full: do not append an orphan record
177
178 uint8_t rec[DBM_HDR + DETWS_DBM_KEY_MAX + DETWS_DBM_VAL_MAX];
179 rec[0] = 0;
180 put_u16(rec + 1, key_len);
181 put_u32(rec + 3, val_len);
182 memcpy(rec + DBM_HDR, key, key_len);
183 if (val_len)
184 memcpy(rec + DBM_HDR + key_len, val, val_len);
185 uint64_t old_head = wal_store_used(db->wal);
186 if (!wal_store_append(db->wal, rec, (uint32_t)(DBM_HDR + key_len + val_len)))
187 return false; // WAL full: index unchanged
188
189 DetwsDbmSlot *s = &db->slots[slot];
190 if (s->state != 1)
191 db->count++;
192 s->state = 1;
193 s->hash = h;
194 s->key_len = key_len;
195 memcpy(s->key, key, key_len);
196 s->val_off = old_head + WAL_RECORD_HEADER + DBM_HDR + key_len;
197 s->val_len = val_len;
198 return true;
199}
200
201long detws_dbm_get(DetwsDbm *db, const char *key, uint16_t key_len, uint8_t *buf, size_t cap)
202{
203 uint64_t h = key_hash(key, key_len);
204 int slot = find_live(db, h, key, key_len);
205 if (slot < 0)
206 return -1;
207 DetwsDbmSlot *s = &db->slots[slot];
208 if (s->val_len > cap)
209 return -1;
210 if (s->val_len && !wal_store_pread(db->wal, s->val_off, buf, s->val_len))
211 return -1;
212 return (long)s->val_len;
213}
214
215bool detws_dbm_del(DetwsDbm *db, const char *key, uint16_t key_len)
216{
217 uint64_t h = key_hash(key, key_len);
218 int slot = find_live(db, h, key, key_len);
219 if (slot < 0)
220 return false;
221 uint8_t rec[DBM_HDR + DETWS_DBM_KEY_MAX];
222 rec[0] = 1;
223 put_u16(rec + 1, key_len);
224 put_u32(rec + 3, 0);
225 memcpy(rec + DBM_HDR, key, key_len);
226 if (!wal_store_append(db->wal, rec, (uint32_t)(DBM_HDR + key_len)))
227 return false; // WAL full: key stays live
228 db->slots[slot].state = 2;
229 db->count--;
230 return true;
231}
232
233bool detws_dbm_contains(DetwsDbm *db, const char *key, uint16_t key_len)
234{
235 return find_live(db, key_hash(key, key_len), key, key_len) >= 0;
236}
237
238uint32_t detws_dbm_count(DetwsDbm *db)
239{
240 return db->count;
241}
242
243bool detws_dbm_sync(DetwsDbm *db)
244{
245 return wal_store_checkpoint(db->wal);
246}
247
248uint32_t detws_dbm_iterate(DetwsDbm *db, DetwsDbmIterCb cb, void *ctx)
249{
250 uint32_t visited = 0;
251 for (uint32_t i = 0; i < DETWS_DBM_SLOTS; i++)
252 {
253 DetwsDbmSlot *s = &db->slots[i];
254 if (s->state != 1)
255 continue;
256 visited++;
257 if (cb && !cb(s->key, s->key_len, ctx))
258 break;
259 }
260 return visited;
261}
262
263uint64_t detws_dbm_live_bytes(DetwsDbm *db)
264{
265 uint64_t bytes = 0;
266 for (uint32_t i = 0; i < DETWS_DBM_SLOTS; i++)
267 {
268 const DetwsDbmSlot *s = &db->slots[i];
269 if (s->state == 1)
270 bytes += WAL_RECORD_HEADER + DBM_HDR + s->key_len + s->val_len; // one framed record per live key
271 }
272 return bytes;
273}
274
275bool detws_dbm_compact(DetwsDbm *db, WalStore *dst)
276{
277 // Copy each live key (latest value, no tombstones) into the fresh destination. Read the value straight
278 // from the old log so this needs no per-key RAM beyond one record buffer, the same the put path uses.
279 uint8_t rec[DBM_HDR + DETWS_DBM_KEY_MAX + DETWS_DBM_VAL_MAX];
280 for (uint32_t i = 0; i < DETWS_DBM_SLOTS; i++)
281 {
282 const DetwsDbmSlot *s = &db->slots[i];
283 if (s->state != 1)
284 continue;
285 rec[0] = 0; // put
286 put_u16(rec + 1, s->key_len);
287 put_u32(rec + 3, s->val_len);
288 memcpy(rec + DBM_HDR, s->key, s->key_len);
289 // On any failure, return before rebinding so db keeps using its intact original log (no data loss).
290 if (s->val_len && !wal_store_pread(db->wal, s->val_off, rec + DBM_HDR + s->key_len, s->val_len))
291 return false;
292 if (!wal_store_append(dst, rec, (uint32_t)(DBM_HDR + s->key_len + s->val_len)))
293 return false; // destination too small
294 }
295 if (!wal_store_checkpoint(dst))
296 return false;
297 return detws_dbm_open(db, dst); // rebind to the compacted log + rebuild the index with fresh offsets
298}
299
300#endif // DETWS_ENABLE_DBM
#define DETWS_DBM_SLOTS
#define DETWS_DBM_VAL_MAX
#define DETWS_DBM_KEY_MAX
Log-structured hash key-value store on the WAL (DETWS_ENABLE_DBM, requires DETWS_ENABLE_WAL).