lfs improvements

This commit is contained in:
liquidraver
2026-08-24 12:50:13 +02:00
parent f550d06c19
commit dcd0be7ded
3 changed files with 85 additions and 8 deletions
@@ -1084,12 +1084,27 @@ void ZephyrDataStore::loadContacts(DataStoreHost *host)
void ZephyrDataStore::saveContacts(DataStoreHost *host)
{
const char *path = contactsFile();
/* Atomic replace ONLY where there is external flash — deliberately, and
* not to be "fixed" later.
*
* atomicWriteTempFile() needs room for a second full copy before the
* rename. contacts3 is by far the largest store here (152 B per record,
* ~47 KB at 313 contacts) and internal /lfs on these boards is 128 KB
* total, shared with identity, prefs and channels2. Two copies would sit
* at ~94 KB of 128 KB before LittleFS metadata, so the atomic path could
* fail with ENOSPC exactly when it is most needed — a worse failure than
* the one it prevents.
*
* channels2, identity and prefs are atomic everywhere because they are
* small enough for the second copy to be free. Only contacts is gated.
*
* The non-atomic branch below is therefore the constrained-board path,
* and it writes in place and truncates rather than unlinking first — see
* the note there. */
bool use_atomic = _has_ext_fs;
const char *save_mode = use_atomic ? "atomic" : "direct";
if (!use_atomic && exists(path)) {
fs_unlink(path);
}
struct fs_file_t file;
uint8_t rec[CONTACT_DATA_SZ];
@@ -1126,6 +1141,21 @@ void ZephyrDataStore::saveContacts(DataStoreHost *host)
};
write_ok = atomicWriteTempFile(path, atomic_contacts_writer, &ctx, "saveContacts");
} else {
/* Overwrite in place, then truncate — never unlink first.
*
* This branch runs on boards with no external flash, i.e. the ones
* that cannot afford the atomic temp-file dance. It used to
* fs_unlink() the contacts file before recreating it, which left a
* window spanning the whole ~47 KB write where contacts3 did not
* exist at all: a power cut there lost every contact rather than
* corrupting some. The unlink was only ever a way to truncate.
*
* Truncating afterwards is the same guarantee without the window —
* the file is always present, and at worst briefly longer than its
* new contents (stale records past the end, which the truncate then
* removes). FS_O_TRUNC is NOT usable here: Zephyr's LittleFS
* backend maps only CREATE/READ/WRITE/APPEND and drops TRUNC
* silently, so asking for it would leave the stale tail in place. */
fs_file_t_init(&file);
int rc = fs_open(&file, path, FS_O_CREATE | FS_O_WRITE);
if (rc < 0) {
@@ -1133,6 +1163,20 @@ void ZephyrDataStore::saveContacts(DataStoreHost *host)
return;
}
write_ok = write_contacts(&file);
int trunc_rc = 0;
if (write_ok) {
trunc_rc = fs_truncate(&file,
(off_t)written * CONTACT_DATA_SZ);
if (trunc_rc < 0) {
LOG_ERR("saveContacts: truncate to %u failed: %d",
(unsigned)(written * CONTACT_DATA_SZ),
trunc_rc);
write_ok = false;
}
}
int sync_rc = fs_sync(&file);
fs_close(&file);
if (!write_ok || sync_rc < 0) {
+21 -4
View File
@@ -387,12 +387,24 @@ void CompanionMesh::onChannelAdded(ChannelDetails *)
markChannelsDirty();
}
void CompanionMesh::markContactsDirty()
void CompanionMesh::markContactsDirty(bool substantive)
{
int64_t deadline = _ms->getMillis() +
(substantive ? LAZY_WRITE_DELAY_MS
: LAZY_WRITE_LIVENESS_MS);
/* Only set the timer on first dirty — don't keep pushing
* the deadline forward or a busy mesh never flushes. */
if (!_dirty_contacts_expiry) {
_dirty_contacts_expiry = _ms->getMillis() + LAZY_WRITE_DELAY_MS;
_dirty_contacts_expiry = deadline;
return;
}
/* ...but a substantive change must not have to sit out a liveness wait
* that is already pending. Pulling the deadline IN cannot starve the
* flush, only hasten it. */
if (substantive && deadline < _dirty_contacts_expiry) {
_dirty_contacts_expiry = deadline;
}
}
@@ -727,8 +739,13 @@ void CompanionMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8
LOG_INF("onDiscoveredContact: '%s' is_new=%d path_len=%d num_contacts=%d",
contact.name, is_new, path_len, getNumContacts());
// Mark contacts dirty for lazy save
markContactsDirty();
/* A re-heard advert from a contact we already have is liveness only —
* the base class has just refreshed last_advert_timestamp and lastmod,
* and nothing else typically moved. Persist it lazily rather than
* spending a full-file rewrite per advert; a genuinely new contact, a
* path change (onContactPathUpdated) or a message all still flush on the
* short deadline. */
markContactsDirty(is_new);
// Update advert path table
if (path && mesh::Packet::isValidPathLen(path_len)) {
+17 -1
View File
@@ -417,9 +417,25 @@ private:
int64_t _dirty_channels_expiry;
static constexpr int64_t LAZY_WRITE_DELAY_MS = 5000; /* 5 seconds, matches Arduino */
/* Deadline for liveness-only contact updates — a re-heard advert from a
* contact we already know, where the only fields that moved are
* last_advert_timestamp and lastmod.
*
* saveContacts() rewrites the WHOLE file (fixed 152-byte records, no
* incremental path), which on a board without external flash is ~47 KB
* into a 128 KB LittleFS partition. Measured on a T1000-E 2026-08-24:
* 21 full rewrites in 90 minutes, one per advert arrival, because every
* re-advert marked the file dirty on the same 5 s deadline as a real
* change. That is a flash-wear problem on a battery tracker.
*
* Liveness still persists it just waits, so an hour of re-adverts
* costs one write instead of fourteen. A substantive change (new
* contact, message, path update) still pulls the deadline back in. */
static constexpr int64_t LAZY_WRITE_LIVENESS_MS = 600000; /* 10 minutes */
void onLoginSent(const ContactInfo &contact) override;
void onChannelAdded(ChannelDetails *ch) override;
void markContactsDirty();
void markContactsDirty(bool substantive = true);
void markChannelsDirty();
void flushDirtyContacts();
void flushDirtyChannels();