Skip to content

Commit 45cb8e7

Browse files
RCGV1jp-bennettthebenternBenjamin Faershtein
authored
feat: preserve normal radio profiles across event firmware (#11110)
* feat: isolate event radio profiles * fix: preserve identity on degraded config boot * fix: guard event profile storage * style: align event profile storage names * fix: discard event profile on normal boot * refactor: simplify event profile cleanup * fix: limit inactive profile migration to event firmware * fix(event): make event-profile capacity check portable across filesystems The USERPREFS_EVENT_MODE storage preflight called FSCom.totalBytes() and FSCom.usedBytes(), which only exist on ESP32's LittleFS wrapper. Every other backend failed to compile once event mode was enabled: error: 'class InternalFileSystem' has no member named 'totalBytes' (nRF52) error: no member named 'totalBytes' in 'fs::FS' (Portduino) This was not caught earlier because the event block is behind #if USERPREFS_EVENT_MODE, and the existing unit tests only cover the pure helpers, which compile identically either way. Add fsTotalBytes()/fsUsedBytes() to FSCommon and implement them per backend: littlefs v1 traversal for nRF52 (Adafruit_LittleFS) and STM32 (STM32_LittleFS), FSInfo for RP2040, statvfs for Portduino, and the native methods for ESP32 and nRF54L15. The nRF52/STM32 path reports "full" if the traversal errors so the capacity check fails safe. Verified with USERPREFS_EVENT_MODE=1: native-macos test_event_profile_storage passes 5/5, and heltec-v3, rak4631, rak11310 and rak3172 all build clean. * fix(event): use std::filesystem for Portduino capacity, bump native-suite-count Two CI fixes: - native-windows has no <sys/statvfs.h>, so the Portduino branch of fsTotalBytes()/fsUsedBytes() broke the Windows build. Switch to std::filesystem::space(), which is already used under ARCH_PORTDUINO in HostMetrics.cpp and works on both POSIX and MinGW. Errors report "full" so the capacity check still fails safe. - This PR adds test/test_event_profile_storage, taking test/ from 40 to 41 suite directories, which trips the native-suite-count reconciliation check. * fix(event): scope boot-write deferral to the radio profile, address review Review feedback: - Copilot: saveProto()'s boot-write deferral applied to every file, so loadFromDisk()'s recovery writes (e.g. restoring owner fields into devicestate) were silently dropped and never retried, because the ctor CRC baselines are computed after loadFromDisk(). Deferral now applies only to the radio-profile files, via isRadioProfileFile(). - jp-bennett: eventConfigFromStandard() wrapped a struct copy plus one field assignment in a header helper with its own unit test. Inlined at its single call site and removed; the behaviour is covered end-to-end by hardware validation instead (RAK4631 normal -> event -> normal preserves NodeNum and public key while swapping LoRa, and restores the original channel byte-identically). - jp-bennett: the active-backup encrypted-storage migration ran for normal builds too, which changed non-event behaviour and contradicted the PR's stated event-only scope. Scoped to USERPREFS_EVENT_MODE; the adjacent block already covers the inactive standard backup in event builds. New tests (all verified to fail under mutation, not vacuous): - test_event_paths_never_collide_with_standard_or_shared_files: the core safety property. A path-table slip would make event firmware overwrite the user's real config, channels or backup, or capture a shared file like devicestate/nodedb. Nothing asserted this before. - test_only_radio_profile_files_defer_boot_writes: guards the deferral fix above so re-widening it fails loudly. - test_event_reservation_fits_smallest_supported_filesystem: the reservation is compile-time but must fit filesystems as small as 14 KiB (STM32WL) and 28 KiB (nRF52840). Protobuf growth pushing it past those would silently stop event profiles persisting - the exact failure mode confirmed by fault injection on a RAK4631. Verified with USERPREFS_EVENT_MODE both on and off: 7/7 tests pass, and rak4631, heltec-v3, rak11310 and rak3172 all build clean. * fix: preserve event profile storage recovery --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Benjamin Faershtein <benjaminfaershtein@Benjamins-MacBook-Pro-2.local>
1 parent 5c5cb09 commit 45cb8e7

6 files changed

Lines changed: 401 additions & 7 deletions

File tree

src/FSCommon.cpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,85 @@
1212
#include "SPILock.h"
1313
#include "configuration.h"
1414

15+
#if defined(ARCH_PORTDUINO)
16+
#include <filesystem>
17+
#endif
18+
19+
#if defined(ARCH_NRF52) || defined(ARCH_STM32)
20+
// Adafruit_LittleFS (nRF52) and STM32_LittleFS both wrap littlefs v1, which predates lfs_fs_size().
21+
// Count the blocks currently in use with lfs_traverse() instead.
22+
static int fsCountBlockCb(void *ctx, lfs_block_t)
23+
{
24+
*static_cast<size_t *>(ctx) += 1;
25+
return 0;
26+
}
27+
28+
size_t fsTotalBytes()
29+
{
30+
lfs_t *fs = FSCom._getFS();
31+
if (!fs || !fs->cfg)
32+
return 0;
33+
return (size_t)fs->cfg->block_count * (size_t)fs->cfg->block_size;
34+
}
35+
36+
size_t fsUsedBytes()
37+
{
38+
lfs_t *fs = FSCom._getFS();
39+
if (!fs || !fs->cfg)
40+
return 0;
41+
size_t blocks = 0;
42+
FSCom._lockFS();
43+
int err = lfs_traverse(fs, fsCountBlockCb, &blocks);
44+
FSCom._unlockFS();
45+
if (err < 0)
46+
return fsTotalBytes(); // report "full" so capacity checks fail safe
47+
return blocks * (size_t)fs->cfg->block_size;
48+
}
49+
#elif defined(ARCH_RP2040)
50+
// arduino-pico reports capacity through FSInfo rather than as methods.
51+
size_t fsTotalBytes()
52+
{
53+
FSInfo info;
54+
return FSCom.info(info) ? (size_t)info.totalBytes : 0;
55+
}
56+
57+
size_t fsUsedBytes()
58+
{
59+
FSInfo info;
60+
return FSCom.info(info) ? (size_t)info.usedBytes : 0;
61+
}
62+
#elif defined(ARCH_PORTDUINO)
63+
// Portduino is backed by the host filesystem; ask the OS about the volume holding the working
64+
// directory. std::filesystem keeps this working on native-windows too, where statvfs() does not
65+
// exist. On error we report "full" so capacity checks fail safe.
66+
size_t fsTotalBytes()
67+
{
68+
std::error_code ec;
69+
const auto info = std::filesystem::space(".", ec);
70+
return ec ? 0 : (size_t)info.capacity;
71+
}
72+
73+
size_t fsUsedBytes()
74+
{
75+
std::error_code ec;
76+
const auto info = std::filesystem::space(".", ec);
77+
if (ec || info.capacity < info.available)
78+
return fsTotalBytes();
79+
return (size_t)(info.capacity - info.available);
80+
}
81+
#else
82+
// ESP32 LittleFS and the nRF54L15 wrapper expose these directly.
83+
size_t fsTotalBytes()
84+
{
85+
return FSCom.totalBytes();
86+
}
87+
88+
size_t fsUsedBytes()
89+
{
90+
return FSCom.usedBytes();
91+
}
92+
#endif
93+
1594
// Software SPI is used by MUI so disable SD card here until it's also implemented
1695
#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI)
1796
#include <SD.h>

src/FSCommon.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ using namespace Adafruit_LittleFS_Namespace;
5656
using namespace Adafruit_LittleFS_Namespace;
5757
#endif
5858

59+
// Filesystem capacity, in bytes. Only ESP32's LittleFS and the nRF54L15 wrapper expose totalBytes()/usedBytes()
60+
// directly; the other backends need per-platform work (littlefs v1 traversal, FSInfo, statvfs), so callers must
61+
// use these helpers rather than reaching into FSCom.
62+
size_t fsTotalBytes();
63+
size_t fsUsedBytes();
64+
5965
void fsInit();
6066
bool renameFile(const char *pathFrom, const char *pathTo);
6167
bool fsFormat();

src/mesh/NodeDB.cpp

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,7 @@ NodeDB::NodeDB()
638638
#endif
639639
sortMeshDB();
640640
saveToDisk(saveWhat);
641+
bootInitializationInProgress = false;
641642
}
642643

643644
/**
@@ -2193,6 +2194,41 @@ void NodeDB::loadFromDisk()
21932194

21942195
migrationSavePending = false;
21952196
configDecodeFailed = false;
2197+
configLoadComplete = false;
2198+
2199+
#if !USERPREFS_EVENT_MODE
2200+
#ifdef FSCom
2201+
const char *eventProfileFiles[] = {EVENT_CONFIG_FILE_NAME, EVENT_CHANNEL_FILE_NAME, EVENT_BACKUP_FILE_NAME};
2202+
spiLock->lock();
2203+
for (const char *filename : eventProfileFiles) {
2204+
if (FSCom.exists(filename) && !FSCom.remove(filename))
2205+
LOG_WARN("Unable to remove stale event profile file %s", filename);
2206+
}
2207+
spiLock->unlock();
2208+
#endif
2209+
#endif
2210+
2211+
#if USERPREFS_EVENT_MODE
2212+
// Seed only a missing event config; never overwrite normal files after a corrupt event config.
2213+
bool eventConfigMissing = false;
2214+
eventProfileStorageUnavailable = false;
2215+
#ifdef FSCom
2216+
spiLock->lock();
2217+
eventConfigMissing = !FSCom.exists(configFileName);
2218+
if (eventConfigMissing) {
2219+
const size_t totalBytes = fsTotalBytes();
2220+
const size_t usedBytes = fsUsedBytes();
2221+
eventProfileStorageUnavailable = !hasEventProfileStorageSpace(totalBytes, usedBytes);
2222+
if (eventProfileStorageUnavailable) {
2223+
LOG_ERROR("Event profile requires %u bytes free; only %u bytes available. Profile changes will not persist.",
2224+
static_cast<unsigned>(EVENT_PROFILE_STORAGE_RESERVATION_BYTES),
2225+
static_cast<unsigned>(totalBytes >= usedBytes ? totalBytes - usedBytes : 0));
2226+
}
2227+
}
2228+
spiLock->unlock();
2229+
#endif
2230+
bool initializedEventConfig = false;
2231+
#endif
21962232

21972233
meshtastic_Config_SecurityConfig backupSecurity = meshtastic_Config_SecurityConfig_init_zero;
21982234

@@ -2382,6 +2418,31 @@ void NodeDB::loadFromDisk()
23822418

23832419
state = loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg,
23842420
&config);
2421+
#if USERPREFS_EVENT_MODE
2422+
if (eventConfigMissing && state != LoadFileResult::LOAD_SUCCESS) {
2423+
const LoadFileResult eventConfigState = state;
2424+
const LoadFileResult standardConfigState =
2425+
loadProto(STANDARD_CONFIG_FILE_NAME, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig),
2426+
&meshtastic_LocalConfig_msg, &config);
2427+
if (standardConfigState == LoadFileResult::LOAD_SUCCESS) {
2428+
// Preserve the user's identity and non-radio preferences, then
2429+
// replace only LoRa with this event build's compiled defaults.
2430+
const meshtastic_LocalConfig standardConfig = config;
2431+
installDefaultConfig(true);
2432+
const meshtastic_Config_LoRaConfig eventLora = config.lora;
2433+
config = standardConfig;
2434+
config.has_lora = true;
2435+
config.lora = eventLora;
2436+
state = LoadFileResult::LOAD_SUCCESS;
2437+
initializedEventConfig = true;
2438+
LOG_INFO("Initialized event config without modifying %s", STANDARD_CONFIG_FILE_NAME);
2439+
} else {
2440+
// Keep the event load outcome because loadProto() clears config before decoding.
2441+
// A normal decode failure must not create a replacement identity.
2442+
state = standardConfigState == LoadFileResult::DECODE_FAILED ? LoadFileResult::DECODE_FAILED : eventConfigState;
2443+
}
2444+
}
2445+
#endif
23852446
if (state == LoadFileResult::DECODE_FAILED) {
23862447
// Config file present but undecodable this boot (corruption / torn write / transient decrypt fail).
23872448
// loadProto() already zeroed `config`, so the keypair is gone from RAM; minting a new one would change
@@ -2403,6 +2464,7 @@ void NodeDB::loadFromDisk()
24032464
} else {
24042465
LOG_INFO("Loaded saved config version %d", config.version);
24052466
}
2467+
configLoadComplete = true;
24062468

24072469
// Coerce LoRa config fields derived from presets while bootstrapping.
24082470
// Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode.
@@ -2443,6 +2505,15 @@ void NodeDB::loadFromDisk()
24432505
config.lora.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
24442506
#endif
24452507

2508+
#if USERPREFS_EVENT_MODE
2509+
if (initializedEventConfig) {
2510+
// This is the first durable event-profile write. A failed write is
2511+
// safe: normal files remain untouched and the next event boot retries.
2512+
if (!saveToDisk(SEGMENT_CONFIG))
2513+
LOG_ERROR("Unable to persist initial event config");
2514+
}
2515+
#endif
2516+
24462517
if (backupSecurity.private_key.size > 0) {
24472518
LOG_DEBUG("Restoring backup of security config");
24482519
config.security = backupSecurity;
@@ -2557,7 +2628,7 @@ void NodeDB::loadFromDisk()
25572628
const int segments[] = {SEGMENT_CONFIG, SEGMENT_MODULECONFIG, SEGMENT_CHANNELS, SEGMENT_DEVICESTATE,
25582629
SEGMENT_NODEDATABASE};
25592630
int toSave = 0;
2560-
for (int i = 0; i < 5; i++) {
2631+
for (size_t i = 0; i < sizeof(segments) / sizeof(segments[0]); i++) {
25612632
if (!EncryptedStorage::isEncrypted(filesToCheck[i])) {
25622633
toSave |= segments[i];
25632634
}
@@ -2574,6 +2645,43 @@ void NodeDB::loadFromDisk()
25742645
EncryptedStorage::migrateFile(fn);
25752646
}
25762647
}
2648+
2649+
// Backups are outside saveToDisk(), but can contain radio profile PSKs. Only event builds
2650+
// introduce a second backup file, so leave normal-firmware backup handling unchanged.
2651+
#if USERPREFS_EVENT_MODE
2652+
#ifdef FSCom
2653+
spiLock->lock();
2654+
const bool activeBackupExists = FSCom.exists(backupFileName);
2655+
spiLock->unlock();
2656+
if (activeBackupExists && !EncryptedStorage::isEncrypted(backupFileName)) {
2657+
LOG_INFO("Migrating %s to encrypted storage", backupFileName);
2658+
if (!EncryptedStorage::migrateFile(backupFileName)) {
2659+
LOG_ERROR("Unable to migrate %s to encrypted storage", backupFileName);
2660+
storageCorruptThisLoad = true;
2661+
}
2662+
}
2663+
#endif
2664+
#endif
2665+
2666+
// Event firmware keeps the normal radio profile inactive, so migrate it separately.
2667+
#if USERPREFS_EVENT_MODE
2668+
#ifdef FSCom
2669+
const char *inactiveRadioProfileFiles[] = {STANDARD_CONFIG_FILE_NAME, STANDARD_CHANNEL_FILE_NAME,
2670+
STANDARD_BACKUP_FILE_NAME};
2671+
for (const char *fn : inactiveRadioProfileFiles) {
2672+
spiLock->lock();
2673+
const bool exists = FSCom.exists(fn);
2674+
spiLock->unlock();
2675+
if (exists && !EncryptedStorage::isEncrypted(fn)) {
2676+
LOG_INFO("Migrating inactive radio profile %s to encrypted storage", fn);
2677+
if (!EncryptedStorage::migrateFile(fn)) {
2678+
LOG_ERROR("Unable to migrate %s to encrypted storage", fn);
2679+
storageCorruptThisLoad = true;
2680+
}
2681+
}
2682+
}
2683+
#endif
2684+
#endif
25772685
}
25782686
#endif
25792687

@@ -2708,8 +2816,9 @@ bool NodeDB::disableLockdownToPlaintext()
27082816
// plaintext->encrypted migrate loop above. Order does not matter here;
27092817
// EncryptedStorage::removeLockdownArtifacts() (which deletes the DEK,
27102818
// the commit point) only runs after every file is confirmed plaintext.
2711-
const char *filesToCheck[] = {configFileName, moduleConfigFileName, channelFileName, deviceStateFileName,
2712-
nodeDatabaseFileName};
2819+
const char *filesToCheck[] = {STANDARD_CONFIG_FILE_NAME, STANDARD_CHANNEL_FILE_NAME, STANDARD_BACKUP_FILE_NAME,
2820+
EVENT_CONFIG_FILE_NAME, EVENT_CHANNEL_FILE_NAME, EVENT_BACKUP_FILE_NAME,
2821+
moduleConfigFileName, deviceStateFileName, nodeDatabaseFileName};
27132822
for (const char *fn : filesToCheck) {
27142823
if (!EncryptedStorage::migrateFileToPlaintext(fn)) {
27152824
LOG_ERROR("NodeDB: failed to revert %s to plaintext; aborting disable (device stays in lockdown)", fn);
@@ -2729,6 +2838,15 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_
27292838
bool fullAtomic)
27302839
{
27312840

2841+
// Only the radio profile is at risk from an unverified config load, so only defer those writes.
2842+
// Devicestate/nodedb/module writes must still land, otherwise boot-time recovery (e.g. loadFromDisk()
2843+
// restoring owner fields) is dropped and never retried.
2844+
if (isRadioProfileFile(filename) &&
2845+
shouldDeferBootPersistence(bootInitializationInProgress, configLoadComplete, configDecodeFailed)) {
2846+
LOG_WARN("NodeDB: deferred boot write to %s until config recovery completes", filename);
2847+
return true;
2848+
}
2849+
27322850
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
27332851
// and all writes will fail anyway. Device should be sleeping at this point anyway.
27342852
if (!powerHAL_isPowerLevelSafe()) {
@@ -2981,6 +3099,19 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
29813099
spiLock->unlock();
29823100
#endif
29833101

3102+
#if USERPREFS_EVENT_MODE
3103+
if (eventProfileStorageUnavailable) {
3104+
if (saveWhat & SEGMENT_CONFIG) {
3105+
LOG_WARN("Skipping event config write: insufficient profile storage at boot");
3106+
saveWhat &= ~SEGMENT_CONFIG;
3107+
}
3108+
if (saveWhat & SEGMENT_CHANNELS) {
3109+
LOG_WARN("Skipping event channel write: insufficient profile storage at boot");
3110+
saveWhat &= ~SEGMENT_CHANNELS;
3111+
}
3112+
}
3113+
#endif
3114+
29843115
if (saveWhat & SEGMENT_CONFIG) {
29853116
config.has_device = true;
29863117
config.has_display = true;

src/mesh/NodeDB.h

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,57 @@ extern meshtastic_Position localPosition;
112112
static constexpr const char *deviceStateFileName = "/prefs/device.proto";
113113
static constexpr const char *legacyPrefFileName = "/prefs/db.proto";
114114
static constexpr const char *nodeDatabaseFileName = "/prefs/nodes.proto";
115-
static constexpr const char *configFileName = "/prefs/config.proto";
115+
// Event builds isolate radio profiles so normal firmware resumes its config and channels after OTA.
116+
static constexpr const char *STANDARD_CONFIG_FILE_NAME = "/prefs/config.proto";
117+
static constexpr const char *STANDARD_CHANNEL_FILE_NAME = "/prefs/channels.proto";
118+
static constexpr const char *EVENT_CONFIG_FILE_NAME = "/prefs/event-config.proto";
119+
static constexpr const char *EVENT_CHANNEL_FILE_NAME = "/prefs/event-channels.proto";
120+
static constexpr const char *STANDARD_BACKUP_FILE_NAME = "/backups/backup.proto";
121+
static constexpr const char *EVENT_BACKUP_FILE_NAME = "/backups/event-backup.proto";
122+
123+
struct RadioProfileStoragePaths {
124+
const char *config;
125+
const char *channels;
126+
const char *backup;
127+
};
128+
129+
constexpr RadioProfileStoragePaths radioProfileStoragePaths(bool eventMode)
130+
{
131+
return eventMode ? RadioProfileStoragePaths{EVENT_CONFIG_FILE_NAME, EVENT_CHANNEL_FILE_NAME, EVENT_BACKUP_FILE_NAME}
132+
: RadioProfileStoragePaths{STANDARD_CONFIG_FILE_NAME, STANDARD_CHANNEL_FILE_NAME, STANDARD_BACKUP_FILE_NAME};
133+
}
134+
135+
// Reserve event files and their atomic temporaries before seeding, preventing retry formatting on full storage.
136+
static constexpr size_t EVENT_PROFILE_STORAGE_RESERVATION_BYTES = 2 * (meshtastic_LocalConfig_size + meshtastic_ChannelFile_size);
137+
138+
constexpr bool hasEventProfileStorageSpace(size_t totalBytes, size_t usedBytes)
139+
{
140+
return usedBytes <= totalBytes && totalBytes - usedBytes >= EVENT_PROFILE_STORAGE_RESERVATION_BYTES;
141+
}
142+
143+
constexpr bool shouldDeferBootPersistence(bool bootInitializationInProgress, bool configLoadComplete, bool configDecodeFailed)
144+
{
145+
return bootInitializationInProgress && (!configLoadComplete || configDecodeFailed);
146+
}
147+
148+
#if USERPREFS_EVENT_MODE
149+
static constexpr auto RADIO_PROFILE_STORAGE = radioProfileStoragePaths(true);
150+
#else
151+
static constexpr auto RADIO_PROFILE_STORAGE = radioProfileStoragePaths(false);
152+
#endif
153+
static constexpr const char *configFileName = RADIO_PROFILE_STORAGE.config;
154+
static constexpr const char *channelFileName = RADIO_PROFILE_STORAGE.channels;
155+
static constexpr const char *backupFileName = RADIO_PROFILE_STORAGE.backup;
116156
static constexpr const char *uiconfigFileName = "/prefs/uiconfig.proto";
117157
static constexpr const char *moduleConfigFileName = "/prefs/module.proto";
118-
static constexpr const char *channelFileName = "/prefs/channels.proto";
119-
static constexpr const char *backupFileName = "/backups/backup.proto";
158+
159+
// An unverified config load only endangers the radio profile, so only these files take part in
160+
// boot-write deferral. Lives next to the path table above so the two cannot drift apart.
161+
inline bool isRadioProfileFile(const char *filename)
162+
{
163+
return strcmp(filename, configFileName) == 0 || strcmp(filename, channelFileName) == 0 ||
164+
strcmp(filename, backupFileName) == 0;
165+
}
120166

121167
/// Given a node, return how many seconds in the past (vs now) that we last heard from it
122168
uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n);
@@ -560,6 +606,14 @@ class NodeDB
560606
/// skip boot keygen and skip persisting defaults, so a transient read failure can't change our NodeNum
561607
/// or overwrite the on-disk config. Cleared at the top of every loadFromDisk() run.
562608
bool configDecodeFailed = false;
609+
// Defer automatic writes until config load is healthy to protect device and node data from damaged configs.
610+
bool bootInitializationInProgress = true;
611+
bool configLoadComplete = false;
612+
#if USERPREFS_EVENT_MODE
613+
// The active event profile is intentionally non-durable when there was not
614+
// enough room to create it safely at boot. A later boot retries the check.
615+
bool eventProfileStorageUnavailable = false;
616+
#endif
563617
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
564618
uint32_t lastFullEvictionMs = 0; // when we last evicted to admit a new node, once the db is full
565619
uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually

test/native-suite-count

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
40
1+
41

0 commit comments

Comments
 (0)