Preserve accepted contacts after transient path read errors

Recheck cached immutable paths before resetting the live table after raw I/O failure. Keep accepted RAM mutations, current jobs and save deadlines when reads recover; stronger source or publication uncertainty still forces full recovery. Use the existing recovery byte and a fixed scratch buffer, with no cache allocation or eviction.

Cover full-cache transient recovery, persistent I/O and checksum failures, nRF unread-scope restoration, active ESP jobs, real uncertain publication and unchanged lazy deadlines with sanitizer tests and restored-reset negative controls.
This commit is contained in:
mikecarper committed 2026-10-16 08:54:12 -07:00
1 parent a3e9bec997
commit 587700a36a
7 files changed
+339 -9

No files matched your search

+32 -4
View File
@@ -2194,7 +2194,35 @@ bool DataStore::prepareContactSave() {
#endif
void DataStore::recoverContacts(DataStoreHost* host) {
if (!contactRecoveryPending() || !host->resetContactsForRecovery()) return;
if (!contactRecoveryPending()) return;
#if MESH_CONTACT_CACHE
if (_contact_recovery & ContactRecoveryRawOnly) {
// A failed cold read did not change names/generations. Retry immutable
// current paths before retiring accepted, not-yet-persisted RAM changes.
const uint8_t previous = _contact_recovery;
_contact_recovery &= ~(ContactRecoveryPending | ContactRecoveryRawOnly
| ContactRecoveryLegacy);
#if defined(NRF52_PLATFORM)
const auto unread = _unread_contact_pages;
_unread_contact_pages.clearAll();
#endif
uint8_t path[64];
bool readable = true;
for (uint32_t i = 0;; ++i) {
const ContactInfo* contact = host->getContactForStore(i);
if (contact == nullptr) break;
// copyPathTo uses checksum-checked read(), not allocating/evicting view().
if (!contact->copyPathTo(path)) { readable = false; break; }
}
if (readable && !contactRecoveryPending()) return;
#if defined(NRF52_PLATFORM)
for (uint8_t page = 0; page < mesh::storage::CONTACT_PAGE_COUNT; ++page)
if (unread.bits() & (1UL << page)) _unread_contact_pages.mark(page);
#endif
queueContactRecovery((previous & ContactRecoveryLegacy) != 0);
}
#endif
if (!host->resetContactsForRecovery()) return;
#if defined(ESP32_PLATFORM)
cancelContactWrite();
_contact_write_requested = false;
@@ -4590,7 +4618,7 @@ bool DataStore::readStoredPath(uint16_t source, uint8_t path[64]) {
&& file.seek(offset + 76) && file.read(path, 64) == 64;
if (!ok) {
file.close();
queueContactRecovery(true);
queueContactReadRecovery(true);
}
return ok;
}
@@ -4615,9 +4643,9 @@ bool DataStore::readStoredPath(uint16_t source, uint8_t path[64]) {
if (source & mesh::ContactPathStorage::PAGED)
_unread_contact_pages.mark((source & ~mesh::ContactPathStorage::PAGED)
/ mesh::storage::CONTACTS_PER_PAGE);
queueContactRecovery((source & mesh::ContactPathStorage::PAGED) == 0);
queueContactReadRecovery((source & mesh::ContactPathStorage::PAGED) == 0);
#else
queueContactRecovery(true);
queueContactReadRecovery(true);
#endif
}
// The pool checks the CRC captured when this immutable path value was bound.
+17 -1
View File
@@ -50,12 +50,28 @@ class DataStore
// permission to discard contacts. No recovery files or boot counters needed.
enum : uint8_t { ContactRecoveryPending = 1, ContactRecoveryLegacy = 2,
ContactRecoveryBackup = 4, ContactStorageUnavailable = 8,
ContactMigrationDeferred = 16 };
ContactMigrationDeferred = 16,
#if MESH_CONTACT_CACHE
ContactRecoveryRawOnly = 32,
#endif
};
uint8_t _contact_recovery = 0;
void queueContactRecovery(bool legacy = false) {
#if MESH_CONTACT_CACHE
// Source/publication uncertainty always supersedes a raw-read retry.
_contact_recovery &= ~ContactRecoveryRawOnly;
#endif
_contact_recovery |= ContactRecoveryPending
| (legacy ? ContactRecoveryLegacy : 0);
}
#if MESH_CONTACT_CACHE
void queueContactReadRecovery(bool legacy = false) {
if (!(_contact_recovery & ContactRecoveryPending))
_contact_recovery |= ContactRecoveryRawOnly;
_contact_recovery |= ContactRecoveryPending
| (legacy ? ContactRecoveryLegacy : 0);
}
#endif
void clearContactLoadFailure();
#if !defined(NRF52_PLATFORM)
const char* selectContactFile(bool& present);
+1 -1
View File
@@ -11352,7 +11352,7 @@ void MyMesh::loop() {
if (_store->contactRecoveryPending() && !mesh::morseTimingActive(_radio)) {
_store->recoverContacts(this);
updateGpsTelemetryPolicy();
if (_store->hasPendingContactWrites()) {
if (!dirty_contacts_expiry && _store->hasPendingContactWrites()) {
dirty_contacts_expiry = mesh::nonzeroLazyPersistenceDeadline(
futureMillis(LAZY_CONTACTS_WRITE_DELAY));
}