DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
wal_store.h
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.h
6 * @brief Durable write-ahead store: A/B superblock + checkpoint over a block-device seam (DETWS_ENABLE_WAL).
7 *
8 * Increment 2 on top of the pure record codec in wal.h. It turns the codec into a mountable, power-loss-safe
9 * append log on a fixed backing region (a preallocated file on any fs::FS - SD card or LittleFS - or any RAM
10 * buffer for host tests). All I/O goes through a ::WalDev of three function pointers, so the superblock and
11 * checkpoint logic is pure and fully host-testable over a RAM device.
12 *
13 * **On-media layout** of the backing region:
14 *
15 * [ superblock A : WAL_SUPER_SIZE ][ superblock B : WAL_SUPER_SIZE ][ WAL data region ... ]
16 *
17 * Records (wal.h framing) are **appended sequentially** into the data region and are *not* synced per op -
18 * that matches the measured envelope (docs/FEATURE_PERFORMANCE.md): batch, then checkpoint in bulk.
19 *
20 * **Checkpoint = the atomic commit.** ::wal_store_checkpoint syncs the appended data, then writes an updated
21 * superblock (with an incremented generation, the new durable head, and the next seq) to the *inactive* of
22 * the two copies and syncs that. Flipping which copy is newest is the single durable pointer move. If a
23 * crash tears the new superblock, its CRC fails and mount falls back to the older copy; either way mount
24 * then **replays the tail** past the committed head - each record is self-validating (CRC), so records
25 * appended after the last checkpoint are still recovered, and recovery stops at the first torn record.
26 *
27 * So durability is layered: the superblock bounds how far mount must scan, and the per-record CRC provides
28 * the actual atomicity. A crash costs at most the last, un-appended (torn) record.
29 */
30
31#ifndef DETERMINISTICESPASYNCWEBSERVER_WAL_STORE_H
32#define DETERMINISTICESPASYNCWEBSERVER_WAL_STORE_H
33
34#include "ServerConfig.h"
35#include "services/wal/wal.h"
36#include <stddef.h>
37#include <stdint.h>
38
39#if DETWS_ENABLE_WAL
40
41/** @brief Superblock magic ("WSB1", little-endian). */
42#define WAL_SUPER_MAGIC 0x31425357u
43
44/** @brief Reserved bytes per superblock copy (28 used + 4 CRC = 32; padded for headroom). */
45#define WAL_SUPER_SIZE 64
46
47/** @brief Byte offset where the WAL data region begins (past both superblock copies). */
48#define WAL_DATA_OFFSET (2u * (uint32_t)WAL_SUPER_SIZE)
49
50/**
51 * @brief The block-device seam. Every store I/O is one of these three ops, so the store is host-testable
52 * over a RAM buffer and binds to fs::FS (or anything else) with a thin adapter.
53 *
54 * @c read / @c write move exactly @p len bytes at absolute byte offset @p off and return the number of
55 * bytes moved (== @p len on success; a short/zero return is a failure). @c sync is the durability barrier
56 * (fsync / File::flush) and returns true on success. @c size is the total bytes of the backing region.
57 */
58struct WalDev
59{
60 size_t (*read)(void *ctx, uint64_t off, uint8_t *buf, size_t len);
61 size_t (*write)(void *ctx, uint64_t off, const uint8_t *buf, size_t len);
62 bool (*sync)(void *ctx);
63 void *ctx;
64 uint64_t size;
65};
66
67/** @brief A mounted durable WAL store. Treat fields as read-only; use the accessors below. */
68struct WalStore
69{
70 WalDev dev;
71 uint64_t data_off; ///< first byte of the data region (== ::WAL_DATA_OFFSET)
72 uint64_t data_cap; ///< usable data-region bytes (dev.size - data_off)
73 uint64_t head; ///< bytes used in the data region, including not-yet-checkpointed appends
74 uint64_t committed; ///< head as of the last durable checkpoint
75 uint64_t next_seq; ///< sequence number the next appended record will carry
76 uint64_t gen; ///< generation of the currently newest superblock
77 int ab; ///< index (0/1) of the newest superblock copy; the next checkpoint writes 1 - ab
78};
79
80/**
81 * @brief Format @p dev into an empty store (writes a generation-1 superblock). Erases any existing log.
82 * @return false if @p dev is too small to hold both superblocks plus a data region.
83 */
84bool wal_store_format(WalStore *s, const WalDev *dev);
85
86/**
87 * @brief Mount an existing store: pick the newest valid superblock, then replay the tail past its committed
88 * head to recover records appended since the last checkpoint. @return false if neither superblock is valid
89 * (unformatted / both torn).
90 */
91bool wal_store_mount(WalStore *s, const WalDev *dev);
92
93/**
94 * @brief Append one record (wal.h framing) at the current head. Does **not** sync - call ::wal_store_checkpoint
95 * to make appends durable. @return false if the record does not fit the remaining data region (log full) or a
96 * device write is short.
97 */
98bool wal_store_append(WalStore *s, const uint8_t *payload, uint32_t len);
99
100/**
101 * @brief Make every append so far durable and advance the committed head: sync data, write the next-generation
102 * superblock to the inactive copy, sync it. @return false on a device write/sync failure.
103 */
104bool wal_store_checkpoint(WalStore *s);
105
106/** @brief Per-record callback for ::wal_store_scan - like ::WalRecordCb but also gives the record's
107 * data-region byte offset (so an index can record where to re-read the payload later). */
108using WalStoreRecordCb = void (*)(uint64_t seq, uint64_t data_off, const uint8_t *payload, uint32_t len, void *ctx);
109
110/**
111 * @brief Walk every valid record currently in the store (offsets [0, head)) in order, invoking @p cb for each.
112 *
113 * Reads each record into @p scratch (which must be at least ::WAL_RECORD_HEADER plus the largest record
114 * payload) and re-validates it with the codec, stopping at the first bad record. The point is to rebuild an
115 * in-RAM index after mount (e.g. the dbm hash table replays puts/deletes this way). @return the record count.
116 */
117size_t wal_store_scan(WalStore *s, WalStoreRecordCb cb, void *ctx, uint8_t *scratch, size_t scratch_len);
118
119/**
120 * @brief Read @p len bytes from data-region offset @p off (as reported to ::WalStoreRecordCb) into @p buf.
121 * @return true on success. Lets an index re-read a value straight from the log without buffering it.
122 */
123bool wal_store_pread(WalStore *s, uint64_t off, uint8_t *buf, size_t len);
124
125/** @brief Bytes used in the data region (including appends not yet checkpointed). */
126static inline uint64_t wal_store_used(const WalStore *s)
127{
128 return s->head;
129}
130/** @brief The durable committed head as of the last checkpoint. */
131static inline uint64_t wal_store_committed(const WalStore *s)
132{
133 return s->committed;
134}
135/** @brief Total usable data-region capacity in bytes. */
136static inline uint64_t wal_store_capacity(const WalStore *s)
137{
138 return s->data_cap;
139}
140
141#endif // DETWS_ENABLE_WAL
142#endif // DETERMINISTICESPASYNCWEBSERVER_WAL_STORE_H
User-facing configuration for DeterministicESPAsyncWebServer.
Write-ahead journal for atomic buffer-to-flash storage (DETWS_ENABLE_WAL).