DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
wal_store.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 wal_store.cpp
6 * @brief A/B superblock + checkpoint + mount/recover over a block-device seam (see wal_store.h).
7 */
8
10
11#if DETWS_ENABLE_WAL
12
13#include <string.h>
14
15namespace
16{
17void put_u32(uint8_t *p, uint32_t v)
18{
19 p[0] = (uint8_t)v;
20 p[1] = (uint8_t)(v >> 8);
21 p[2] = (uint8_t)(v >> 16);
22 p[3] = (uint8_t)(v >> 24);
23}
24void put_u64(uint8_t *p, uint64_t v)
25{
26 for (int i = 0; i < 8; i++)
27 p[i] = (uint8_t)(v >> (8 * i));
28}
29uint32_t get_u32(const uint8_t *p)
30{
31 return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
32}
33uint64_t get_u64(const uint8_t *p)
34{
35 uint64_t v = 0;
36 for (int i = 0; i < 8; i++)
37 v |= (uint64_t)p[i] << (8 * i);
38 return v;
39}
40
41// SUPER_USED bytes are CRC-covered; the u32 CRC follows immediately.
42const size_t SUPER_USED = 28;
43
44// Exact-length device I/O: any short move is a failure.
45bool dev_read(const WalDev &d, uint64_t off, uint8_t *buf, size_t len)
46{
47 return d.read && d.read(d.ctx, off, buf, len) == len;
48}
49bool dev_write(const WalDev &d, uint64_t off, const uint8_t *buf, size_t len)
50{
51 return d.write && d.write(d.ctx, off, buf, len) == len;
52}
53
54// Encode a superblock into a WAL_SUPER_SIZE scratch and write it to copy `ab` (0 or 1).
55bool write_super(WalStore *s, int ab, uint64_t gen, uint64_t head, uint64_t seq)
56{
57 uint8_t sb[WAL_SUPER_SIZE];
58 memset(sb, 0, sizeof(sb));
59 put_u32(sb + 0, WAL_SUPER_MAGIC);
60 put_u64(sb + 4, gen);
61 put_u64(sb + 12, head);
62 put_u64(sb + 20, seq);
63 put_u32(sb + 28, wal_crc32(sb, SUPER_USED));
64 return dev_write(s->dev, (uint64_t)ab * WAL_SUPER_SIZE, sb, sizeof(sb));
65}
66
67// Read + validate a superblock copy. On a good CRC+magic fills gen/head/seq and returns true.
68bool read_super(const WalStore *s, int ab, uint64_t &gen, uint64_t &head, uint64_t &seq)
69{
70 uint8_t sb[WAL_SUPER_SIZE];
71 if (!dev_read(s->dev, (uint64_t)ab * WAL_SUPER_SIZE, sb, sizeof(sb)))
72 return false;
73 if (get_u32(sb + 0) != WAL_SUPER_MAGIC)
74 return false;
75 if (get_u32(sb + 28) != wal_crc32(sb, SUPER_USED))
76 return false;
77 gen = get_u64(sb + 4);
78 head = get_u64(sb + 12);
79 seq = get_u64(sb + 20);
80 // A head past the data region is corruption a matching CRC cannot happen for, but guard anyway.
81 if (head > s->data_cap)
82 return false;
83 return true;
84}
85
86// Replay records appended after the committed head: each is CRC self-validating, so recover them one by
87// one and stop at the first torn / short / non-record. Advances s->head and s->next_seq.
88void wal_replay_tail(WalStore *s)
89{
90 uint8_t hdr[WAL_RECORD_HEADER];
91 uint8_t chunk[256];
92 for (;;)
93 {
94 uint64_t off = s->head;
95 if (off + WAL_RECORD_HEADER > s->data_cap)
96 break;
97 if (!dev_read(s->dev, s->data_off + off, hdr, WAL_RECORD_HEADER))
98 break;
99 if (get_u32(hdr + 0) != WAL_MAGIC)
100 break;
101 uint64_t seq = get_u64(hdr + 4);
102 uint32_t plen = get_u32(hdr + 12);
103 uint32_t crc_stored = get_u32(hdr + 16);
104 if (off + (uint64_t)WAL_RECORD_HEADER + plen > s->data_cap)
105 break; // truncated tail
106 // CRC over the 16 header bytes then the payload, streamed in small chunks.
107 uint32_t crc = wal_crc32_update(wal_crc32_init(), hdr, 16);
108 uint64_t pos = s->data_off + off + WAL_RECORD_HEADER;
109 uint32_t left = plen;
110 bool read_ok = true;
111 while (left)
112 {
113 size_t n = left < sizeof(chunk) ? left : sizeof(chunk);
114 if (!dev_read(s->dev, pos, chunk, n))
115 {
116 read_ok = false;
117 break;
118 }
119 crc = wal_crc32_update(crc, chunk, n);
120 pos += n;
121 left -= (uint32_t)n;
122 }
123 if (!read_ok || wal_crc32_final(crc) != crc_stored)
124 break; // torn / corrupt record - this is the durable end
125 s->head = off + (uint64_t)WAL_RECORD_HEADER + plen;
126 if (seq + 1 > s->next_seq)
127 s->next_seq = seq + 1;
128 }
129}
130} // namespace
131
132bool wal_store_format(WalStore *s, const WalDev *dev)
133{
134 if (!dev || dev->size <= WAL_DATA_OFFSET)
135 return false;
136 memset(s, 0, sizeof(*s));
137 s->dev = *dev;
138 s->data_off = WAL_DATA_OFFSET;
139 s->data_cap = dev->size - WAL_DATA_OFFSET;
140 s->head = 0;
141 s->committed = 0;
142 s->next_seq = 0;
143 s->gen = 1;
144 s->ab = 0;
145 // Invalidate copy B, then commit copy A as the live generation-1 superblock.
146 uint8_t zero[WAL_SUPER_SIZE];
147 memset(zero, 0, sizeof(zero));
148 if (!dev_write(s->dev, (uint64_t)1 * WAL_SUPER_SIZE, zero, sizeof(zero)))
149 return false;
150 if (!write_super(s, 0, 1, 0, 0))
151 return false;
152 return s->dev.sync ? s->dev.sync(s->dev.ctx) : true;
153}
154
155bool wal_store_mount(WalStore *s, const WalDev *dev)
156{
157 if (!dev || dev->size <= WAL_DATA_OFFSET)
158 return false;
159 memset(s, 0, sizeof(*s));
160 s->dev = *dev;
161 s->data_off = WAL_DATA_OFFSET;
162 s->data_cap = dev->size - WAL_DATA_OFFSET;
163
164 uint64_t genA = 0;
165 uint64_t headA = 0;
166 uint64_t seqA = 0;
167 uint64_t genB = 0;
168 uint64_t headB = 0;
169 uint64_t seqB = 0;
170 bool okA = read_super(s, 0, genA, headA, seqA);
171 bool okB = read_super(s, 1, genB, headB, seqB);
172 if (!okA && !okB)
173 return false; // unformatted or both torn
174
175 // Newest valid superblock wins (higher generation).
176 if (okA && (!okB || genA >= genB))
177 {
178 s->ab = 0;
179 s->gen = genA;
180 s->committed = headA;
181 s->next_seq = seqA;
182 }
183 else
184 {
185 s->ab = 1;
186 s->gen = genB;
187 s->committed = headB;
188 s->next_seq = seqB;
189 }
190 s->head = s->committed;
191
192 // Recover any records appended after the committed head (each CRC self-validating).
193 wal_replay_tail(s);
194 return true;
195}
196
197bool wal_store_append(WalStore *s, const uint8_t *payload, uint32_t len)
198{
199 uint64_t need = (uint64_t)WAL_RECORD_HEADER + len;
200 if (s->head + need > s->data_cap)
201 return false; // log full
202 // Assemble the 20-byte header (magic+seq+len+crc); CRC covers header + payload without buffering both.
203 uint8_t hdr[WAL_RECORD_HEADER];
204 put_u32(hdr + 0, WAL_MAGIC);
205 put_u64(hdr + 4, s->next_seq);
206 put_u32(hdr + 12, len);
207 uint32_t crc = wal_crc32_update(wal_crc32_init(), hdr, 16);
208 crc = wal_crc32_update(crc, payload, len);
209 put_u32(hdr + 16, wal_crc32_final(crc));
210
211 uint64_t at = s->data_off + s->head;
212 if (!dev_write(s->dev, at, hdr, WAL_RECORD_HEADER))
213 return false;
214 if (len && !dev_write(s->dev, at + WAL_RECORD_HEADER, payload, len))
215 return false;
216 s->head += need;
217 s->next_seq++;
218 return true;
219}
220
221bool wal_store_checkpoint(WalStore *s)
222{
223 // Data first: the appended records must be durable before the pointer that commits them advances.
224 if (s->dev.sync && !s->dev.sync(s->dev.ctx))
225 return false;
226 int target = 1 - s->ab;
227 uint64_t gen = s->gen + 1;
228 if (!write_super(s, target, gen, s->head, s->next_seq))
229 return false;
230 if (s->dev.sync && !s->dev.sync(s->dev.ctx))
231 return false;
232 // The superblock flip is the commit point.
233 s->gen = gen;
234 s->ab = target;
235 s->committed = s->head;
236 return true;
237}
238
239size_t wal_store_scan(WalStore *s, WalStoreRecordCb cb, void *ctx, uint8_t *scratch, size_t scratch_len)
240{
241 if (scratch_len < WAL_RECORD_HEADER)
242 return 0;
243 size_t count = 0;
244 uint64_t off = 0;
245 while (off + WAL_RECORD_HEADER <= s->head)
246 {
247 if (!dev_read(s->dev, s->data_off + off, scratch, WAL_RECORD_HEADER))
248 break;
249 if (get_u32(scratch) != WAL_MAGIC)
250 break;
251 uint32_t plen = get_u32(scratch + 12);
252 size_t total = (size_t)WAL_RECORD_HEADER + plen;
253 if (off + total > s->head || total > scratch_len)
254 break; // truncated within the log, or a record too large for the caller's scratch
255 if (!dev_read(s->dev, s->data_off + off, scratch, total))
256 break;
257 uint32_t crc = wal_crc32_update(wal_crc32_init(), scratch, 16);
258 crc = wal_crc32_update(crc, scratch + WAL_RECORD_HEADER, plen);
259 if (wal_crc32_final(crc) != get_u32(scratch + 16))
260 break;
261 if (cb)
262 cb(get_u64(scratch + 4), off, scratch + WAL_RECORD_HEADER, plen, ctx);
263 count++;
264 off += total;
265 }
266 return count;
267}
268
269bool wal_store_pread(WalStore *s, uint64_t off, uint8_t *buf, size_t len)
270{
271 if (off + len > s->data_cap)
272 return false;
273 return dev_read(s->dev, s->data_off + off, buf, len);
274}
275
276#endif // DETWS_ENABLE_WAL
Durable write-ahead store: A/B superblock + checkpoint over a block-device seam (DETWS_ENABLE_WAL).