Stage ESP contact transaction startup between loop passes
Heltec V4 timing measured 2.19 seconds for recovery checks, temporary-file cleanup, open and buffer configuration combined in one lazy-save pass. Defer those startup operations across guarded companion loops while explicit durability callers still initialize synchronously. Track ownership only after attempting to create the temporary file, preserve unowned stale files on early cancellation, and reject premature writes or commits. Cover startup faults, interruption, mutation, cache pressure and synchronous-construction negative controls with native sanitizer tests.
This commit is contained in:
1 parent
7e1750ceac
commit
0a3187ac30
6 files changed
+326
-21
No files matched your search
@@ -2508,10 +2508,8 @@ bool DataStore::serviceContactWrite(DataStoreHost* host,
|
||||
}
|
||||
#endif
|
||||
_contact_write = new (std::nothrow) mesh::ContactFileTransaction(
|
||||
_getContactsChannelsFS(), "/contacts3", companionPathPresence);
|
||||
if (_contact_write == nullptr || !static_cast<bool>(*_contact_write)) {
|
||||
delete _contact_write;
|
||||
_contact_write = nullptr;
|
||||
_getContactsChannelsFS(), "/contacts3", companionPathPresence, true);
|
||||
if (_contact_write == nullptr) {
|
||||
#if MESH_CONTACT_CACHE
|
||||
paths.endCommit(false);
|
||||
#endif
|
||||
@@ -2524,6 +2522,17 @@ bool DataStore::serviceContactWrite(DataStoreHost* host,
|
||||
_contact_write_verifying = false;
|
||||
return true;
|
||||
}
|
||||
if (!static_cast<bool>(*_contact_write)) {
|
||||
// The revision/session guard above also applies to every setup step.
|
||||
// Keep initialization separate from serialization, including its final
|
||||
// buffer configuration, so transports are serviced between operations.
|
||||
const auto progress = _contact_write->serviceBegin();
|
||||
if (progress == mesh::ContactFileTransaction::BeginProgress::Failed) {
|
||||
cancelContactWrite();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!_contact_write_verifying) {
|
||||
#if MESH_CONTACT_CACHE
|
||||
ContactInfo* contact;
|
||||
|
||||
@@ -28,6 +28,16 @@ private:
|
||||
uint32_t _verify_crc = 0xffffffff;
|
||||
size_t _verify_remaining = 0;
|
||||
PresenceProbe _presence;
|
||||
#if defined(ESP32_PLATFORM)
|
||||
enum class BeginStage : uint8_t {
|
||||
Target, Backup, Recover, Temp, Remove, Open, Configure, Ready, Failed
|
||||
};
|
||||
BeginStage _begin_stage = BeginStage::Target;
|
||||
bool _begin_target_exists = false;
|
||||
bool _begin_backup_exists = false;
|
||||
bool _begin_temp_exists = false;
|
||||
bool _owns_temp = false;
|
||||
#endif
|
||||
static bool probe(FILESYSTEM* fs, const char* path, bool& present,
|
||||
PresenceProbe presence) {
|
||||
if (presence) return presence(fs, path, present);
|
||||
@@ -52,10 +62,21 @@ public:
|
||||
return !backup_exists || fs->rename(backup, target);
|
||||
}
|
||||
ContactFileTransaction(FILESYSTEM* fs, const char* target,
|
||||
PresenceProbe presence = nullptr)
|
||||
PresenceProbe presence = nullptr
|
||||
#if defined(ESP32_PLATFORM)
|
||||
, bool defer_begin = false
|
||||
#endif
|
||||
)
|
||||
: _fs(fs), _target(target), _presence(presence) {
|
||||
snprintf(_temp, sizeof(_temp), "%s.tmp", target);
|
||||
snprintf(_backup, sizeof(_backup), "%s.bak", target);
|
||||
#if defined(ESP32_PLATFORM)
|
||||
// Explicit durability callers retain synchronous construction. The lazy
|
||||
// contact writer requests one setup operation per transport-serviced pass.
|
||||
if (!defer_begin) {
|
||||
while (serviceBegin() == BeginProgress::Pending) {}
|
||||
}
|
||||
#else
|
||||
if (!recover(fs, target, presence)) return;
|
||||
if (!removeIfPresent(fs, _temp, presence)) return;
|
||||
#if defined(RP2040_PLATFORM)
|
||||
@@ -64,19 +85,75 @@ public:
|
||||
_file = fs->open(_temp, "w", true);
|
||||
#endif
|
||||
_ok = static_cast<bool>(_file);
|
||||
#if defined(ESP32_PLATFORM)
|
||||
// Arduino's default 4 KiB stdio buffer hides large SPIFFS writes inside a
|
||||
// single small write() pass. Configure this newly opened stream before
|
||||
// its first I/O, keeping one contact record as the largest buffered batch.
|
||||
_ok = _ok && _file.setBufferSize(storage::CONTACT_RECORD_SIZE);
|
||||
#endif
|
||||
}
|
||||
~ContactFileTransaction() {
|
||||
if (_file) _file.close();
|
||||
if (_verify) _verify.close();
|
||||
if (!_finished) removeIfPresent(_fs, _temp, _presence);
|
||||
if (!_finished
|
||||
#if defined(ESP32_PLATFORM)
|
||||
&& _owns_temp
|
||||
#endif
|
||||
) removeIfPresent(_fs, _temp, _presence);
|
||||
}
|
||||
operator bool() const { return _ok; }
|
||||
#if defined(ESP32_PLATFORM)
|
||||
enum class BeginProgress : uint8_t { Pending, Ready, Failed };
|
||||
BeginProgress serviceBegin() {
|
||||
bool ok = true;
|
||||
switch (_begin_stage) {
|
||||
case BeginStage::Target:
|
||||
ok = probe(_fs, _target, _begin_target_exists, _presence);
|
||||
if (ok) _begin_stage = BeginStage::Backup;
|
||||
break;
|
||||
case BeginStage::Backup:
|
||||
ok = probe(_fs, _backup, _begin_backup_exists, _presence);
|
||||
if (ok) _begin_stage = BeginStage::Recover;
|
||||
break;
|
||||
case BeginStage::Recover:
|
||||
if (!_begin_target_exists && _begin_backup_exists)
|
||||
ok = _fs->rename(_backup, _target);
|
||||
if (ok) _begin_stage = BeginStage::Temp;
|
||||
break;
|
||||
case BeginStage::Temp:
|
||||
ok = probe(_fs, _temp, _begin_temp_exists, _presence);
|
||||
if (ok) _begin_stage = BeginStage::Remove;
|
||||
break;
|
||||
case BeginStage::Remove:
|
||||
if (_begin_temp_exists) ok = _fs->remove(_temp);
|
||||
if (ok) _begin_stage = BeginStage::Open;
|
||||
break;
|
||||
case BeginStage::Open:
|
||||
// The previous stages proved this name absent. Even an unsuccessful
|
||||
// create/open can leave a partial inode belonging to this attempt.
|
||||
// Before this point cancellation must leave an unowned stale temp.
|
||||
_owns_temp = true;
|
||||
_file = _fs->open(_temp, "w", true);
|
||||
ok = static_cast<bool>(_file);
|
||||
if (ok) _begin_stage = BeginStage::Configure;
|
||||
break;
|
||||
case BeginStage::Configure:
|
||||
// Arduino otherwise batches small writes into a 4 KiB stdio buffer.
|
||||
// Configure the newly opened stream before its first I/O.
|
||||
ok = _file.setBufferSize(storage::CONTACT_RECORD_SIZE);
|
||||
if (ok) {
|
||||
_ok = true;
|
||||
_begin_stage = BeginStage::Ready;
|
||||
return BeginProgress::Ready;
|
||||
}
|
||||
break;
|
||||
case BeginStage::Ready:
|
||||
return BeginProgress::Ready;
|
||||
case BeginStage::Failed:
|
||||
return BeginProgress::Failed;
|
||||
}
|
||||
if (!ok) {
|
||||
_begin_stage = BeginStage::Failed;
|
||||
return BeginProgress::Failed;
|
||||
}
|
||||
return BeginProgress::Pending;
|
||||
}
|
||||
#endif
|
||||
bool readyToPublish() const {
|
||||
return !_finished && _commit_stage == CommitStage::Publish;
|
||||
}
|
||||
@@ -92,6 +169,11 @@ public:
|
||||
// service its transports between passes without exposing the rename gap.
|
||||
// A synchronous durability operation uses commit() to drain the same steps.
|
||||
CommitProgress serviceCommit(bool valid = true) {
|
||||
#if defined(ESP32_PLATFORM)
|
||||
// A premature commit must not publish, clean up, or implicitly aggregate
|
||||
// the deferred setup operations inside this call.
|
||||
if (_begin_stage != BeginStage::Ready) return CommitProgress::Failed;
|
||||
#endif
|
||||
if (_finished) return CommitProgress::Failed;
|
||||
bool ok = _ok && valid;
|
||||
if (ok && _commit_stage == CommitStage::Flush) {
|
||||
|
||||
+9
-1
@@ -57,6 +57,8 @@ public:
|
||||
unsigned buffer_config_calls = 0, fail_buffer_config = 0;
|
||||
std::string fail_read;
|
||||
bool metadata_error = false, fail_create = false, fail_remove = false;
|
||||
bool partial_create_failure = false;
|
||||
unsigned metadata_probes = 0, fail_metadata_probe = 0;
|
||||
unsigned removes = 0, missing_remove_logs = 0;
|
||||
unsigned fail_rename = 0, renames = 0, writes = 0, reads = 0, opens = 0;
|
||||
std::vector<Files> rename_snapshots;
|
||||
@@ -73,13 +75,19 @@ public:
|
||||
File open(const char* path, const char* mode = "r", bool = false) {
|
||||
++opens;
|
||||
if (*mode != 'r') {
|
||||
if (fail_create) return File();
|
||||
if (fail_create) {
|
||||
if (partial_create_failure) files[path].clear();
|
||||
return File();
|
||||
}
|
||||
files[path].clear();
|
||||
return File(this, path, true);
|
||||
}
|
||||
if (fail_read == path || !exists(path)) return File();
|
||||
return File(this, path, false);
|
||||
}
|
||||
unsigned startupOperations() const {
|
||||
return metadata_probes + opens + removes + renames + buffer_config_calls;
|
||||
}
|
||||
bool rename(const char* from, const char* to) {
|
||||
++renames;
|
||||
if (renames == fail_rename || !exists(from)) return false;
|
||||
|
||||
+38
-2
@@ -103,6 +103,13 @@ public:
|
||||
bool releaseContact(const ContactInfo&);
|
||||
bool hasPendingContactWrites() const;
|
||||
bool serviceContactWrites(DataStoreHost*, bool (*filter)(const ContactInfo&));
|
||||
bool starting() const {
|
||||
return _contact_write != nullptr && !static_cast<bool>(*_contact_write);
|
||||
}
|
||||
bool activeJob() const { return _contact_write != nullptr; }
|
||||
bool readyToPublish() const {
|
||||
return _contact_write && _contact_write->readyToPublish();
|
||||
}
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
bool loadContactPages(DataStoreHost*, uint16_t, uint32_t);
|
||||
@@ -460,11 +467,13 @@ static void cooperative_save_services_commands_and_preserves_snapshots() {
|
||||
}
|
||||
|
||||
static void cooperative_mutations_never_publish_obsolete_indices() {
|
||||
for (unsigned mutation_step : {4u, 46u, 110u, 140u}) {
|
||||
for (unsigned mutation_step : {4u, 46u, 110u, 0u}) {
|
||||
Fixture f(40);
|
||||
const auto old = SPIFFS.files.at("/contacts3");
|
||||
assert(f.store.markContactDirty(f.host.contacts[0]));
|
||||
for (unsigned step = 0; step < mutation_step; ++step) {
|
||||
unsigned step = 0;
|
||||
while (mutation_step == 0 ? !f.store.readyToPublish() : step < mutation_step) {
|
||||
assert(++step < 1000);
|
||||
assert(f.store.serviceContactWrites(&f.host, cachedContactFilter));
|
||||
assert(SPIFFS.files.at("/contacts3") == old);
|
||||
}
|
||||
@@ -507,6 +516,7 @@ static void cooperative_failures_keep_the_old_file_and_pending_mutations() {
|
||||
failed = !f.store.serviceContactWrites(&f.host, cachedContactFilter);
|
||||
}
|
||||
assert(failed && f.store.hasPendingContactWrites());
|
||||
assert(!f.store.activeJob());
|
||||
assert(SPIFFS.files.at("/contacts3") == old);
|
||||
assert(!SPIFFS.exists("/contacts3.tmp"));
|
||||
f.check(0, route(993));
|
||||
@@ -555,6 +565,31 @@ static void interleaved_path_pressure_preserves_guarded_eviction() {
|
||||
f.check(i, route(i < 26 ? 1200 + i : i));
|
||||
}
|
||||
|
||||
static void pending_startup_pressure_preserves_guarded_eviction() {
|
||||
for (unsigned completed : {0u, 3u, 6u}) {
|
||||
Fixture f(40);
|
||||
for (unsigned i = 0; i < 16; ++i) {
|
||||
assert(f.host.contacts[i].setRawPath(route(1600 + i).data()));
|
||||
assert(f.store.markContactDirty(f.host.contacts[i]));
|
||||
}
|
||||
assert(f.host.cache_flushes == 0);
|
||||
SPIFFS.files["/contacts3.tmp"] = {8, 9};
|
||||
assert(f.store.serviceContactWrites(&f.host, cachedContactFilter)); // allocate
|
||||
for (unsigned pass = 0; pass < completed; ++pass)
|
||||
assert(f.store.serviceContactWrites(&f.host, cachedContactFilter));
|
||||
assert(f.store.starting());
|
||||
// The seventeenth resident update must cancel a pending begin, release
|
||||
// _committing, then run the existing guarded synchronous eviction flush.
|
||||
assert(f.host.contacts[16].setRawPath(route(1616).data()));
|
||||
assert(f.host.cache_flushes == 1);
|
||||
assert(!f.store.activeJob());
|
||||
assert(f.store.markContactDirty(f.host.contacts[16]));
|
||||
drain_cooperative_save(f);
|
||||
for (unsigned i = 0; i < 40; ++i)
|
||||
f.check(i, route(i < 17 ? 1600 + i : i));
|
||||
}
|
||||
}
|
||||
|
||||
static void lifecycle_cancels_jobs_and_closes_prior_filesystem_readers() {
|
||||
{
|
||||
Fixture f(30);
|
||||
@@ -755,6 +790,7 @@ int main() {
|
||||
cooperative_failures_keep_the_old_file_and_pending_mutations();
|
||||
synchronous_flush_cancels_pending_jobs_and_stays_durable();
|
||||
interleaved_path_pressure_preserves_guarded_eviction();
|
||||
pending_startup_pressure_preserves_guarded_eviction();
|
||||
lifecycle_cancels_jobs_and_closes_prior_filesystem_readers();
|
||||
routes_survive_eviction_and_full_sync();
|
||||
snapshot_rollback_and_dirty_eviction();
|
||||
|
||||
+163
-2
@@ -12,7 +12,10 @@ static_assert(MESH_CONTACT_CACHE == 0,
|
||||
// Replace only the SDK metadata syscall; the production presence/error logic
|
||||
// and transaction implementation are compiled unchanged.
|
||||
int fixtureStat(FakeFilesystem* fs, const char* vfs_path, struct stat*) {
|
||||
if (fs->metadata_error) { errno = EIO; return -1; }
|
||||
++fs->metadata_probes;
|
||||
if (fs->metadata_error || fs->metadata_probes == fs->fail_metadata_probe) {
|
||||
errno = EIO; return -1;
|
||||
}
|
||||
assert(strncmp(vfs_path, "/spiffs", 7) == 0);
|
||||
if (!fs->exists(vfs_path + 7)) { errno = ENOENT; return -1; }
|
||||
return 0;
|
||||
@@ -63,6 +66,10 @@ public:
|
||||
bool hasPendingContactWrites() const;
|
||||
bool deleteBlobByKey(const uint8_t[], int);
|
||||
bool verifying() const { return _contact_write_verifying; }
|
||||
bool starting() const {
|
||||
return _contact_write == nullptr || !static_cast<bool>(*_contact_write);
|
||||
}
|
||||
bool activeJob() const { return _contact_write != nullptr; }
|
||||
bool readyToPublish() const {
|
||||
return _contact_write && _contact_write->readyToPublish();
|
||||
}
|
||||
@@ -116,8 +123,12 @@ unsigned drain(DataStore& store, Host& h, Filter filter = nullptr) {
|
||||
while (store.hasPendingContactWrites()) {
|
||||
const unsigned writes = SPIFFS.writes, reads = SPIFFS.reads;
|
||||
const unsigned backend_writes = SPIFFS.backend_writes, backend_reads = SPIFFS.backend_reads;
|
||||
const bool starting = store.starting();
|
||||
const unsigned startup_before = SPIFFS.startupOperations();
|
||||
assert(++passes < 5000);
|
||||
assert(store.serviceContactWrites(&h, filter));
|
||||
const unsigned startup_operations = SPIFFS.startupOperations() - startup_before;
|
||||
if (starting) assert(startup_operations <= 1);
|
||||
assert(SPIFFS.writes - writes <= 1);
|
||||
assert(SPIFFS.reads - reads <= 1);
|
||||
assert(SPIFFS.backend_writes - backend_writes <= 1);
|
||||
@@ -163,6 +174,152 @@ void boundedFullTableAndFirstSave() {
|
||||
bootCheck(h, keepContact);
|
||||
}
|
||||
|
||||
void deferredBeginCancellationAndCrashRecovery() {
|
||||
using Writer = mesh::ContactFileTransaction;
|
||||
// Observe/destruct at every startup boundary, including before any recovery
|
||||
// and after open but before setBufferSize. A stale temp is never authoritative.
|
||||
for (unsigned completed = 0; completed <= 7; ++completed) {
|
||||
SPIFFS = FakeFilesystem();
|
||||
SPIFFS.emulate_stdio = true;
|
||||
const Host h = table(1);
|
||||
const auto old = image(h);
|
||||
const std::vector<uint8_t> stale = {9, 8, 7};
|
||||
SPIFFS.files["/contacts3"] = old;
|
||||
SPIFFS.files["/contacts3.tmp"] = stale;
|
||||
unsigned before_destructor = 0;
|
||||
{
|
||||
Writer writer(&SPIFFS, "/contacts3", companionPathPresence, true);
|
||||
assert(SPIFFS.startupOperations() == 0 && !writer);
|
||||
for (unsigned pass = 0; pass < completed; ++pass) {
|
||||
const unsigned before = SPIFFS.startupOperations();
|
||||
const auto progress = writer.serviceBegin();
|
||||
assert(progress != Writer::BeginProgress::Failed);
|
||||
const unsigned startup_operations = SPIFFS.startupOperations() - before;
|
||||
assert(startup_operations <= 1);
|
||||
assert(SPIFFS.files.at("/contacts3") == old);
|
||||
// A physical reset at this boundary sees the prior complete image.
|
||||
FakeFilesystem rebooted;
|
||||
rebooted.files = SPIFFS.files;
|
||||
assert(Writer::recover(&rebooted, "/contacts3", companionPathPresence));
|
||||
assert(rebooted.files.at("/contacts3") == old);
|
||||
}
|
||||
if (completed < 7) {
|
||||
const unsigned before = SPIFFS.startupOperations();
|
||||
assert(!writer);
|
||||
assert(writer.write(old.data(), old.size()) == 0);
|
||||
assert(writer.serviceCommit() == Writer::CommitProgress::Failed);
|
||||
assert(!writer.commit());
|
||||
assert(SPIFFS.startupOperations() == before); // no implicit startup/cleanup
|
||||
} else {
|
||||
assert(writer);
|
||||
}
|
||||
before_destructor = SPIFFS.startupOperations();
|
||||
}
|
||||
if (completed < 6) {
|
||||
assert(SPIFFS.startupOperations() == before_destructor); // never owned a temp
|
||||
}
|
||||
if (completed < 5) assert(SPIFFS.files.at("/contacts3.tmp") == stale);
|
||||
else assert(!SPIFFS.exists("/contacts3.tmp"));
|
||||
assert(SPIFFS.files.at("/contacts3") == old && SPIFFS.missing_remove_logs == 0);
|
||||
bootCheck(h);
|
||||
}
|
||||
// An interrupted prior publication recovers identically with deferred setup.
|
||||
SPIFFS = FakeFilesystem();
|
||||
const Host h = table(1);
|
||||
const auto old = image(h);
|
||||
SPIFFS.files["/contacts3.bak"] = old;
|
||||
{
|
||||
Writer writer(&SPIFFS, "/contacts3", companionPathPresence, true);
|
||||
for (unsigned pass = 0; pass < 7; ++pass) {
|
||||
assert(writer.serviceBegin() != Writer::BeginProgress::Failed);
|
||||
FakeFilesystem rebooted;
|
||||
rebooted.files = SPIFFS.files;
|
||||
assert(Writer::recover(&rebooted, "/contacts3", companionPathPresence));
|
||||
assert(rebooted.files.at("/contacts3") == old);
|
||||
}
|
||||
assert(writer && SPIFFS.files.at("/contacts3") == old);
|
||||
}
|
||||
bootCheck(h);
|
||||
}
|
||||
|
||||
void deferredBeginFaultsAndSynchronousDefaults() {
|
||||
using Writer = mesh::ContactFileTransaction;
|
||||
for (unsigned fault = 0; fault < 8; ++fault) {
|
||||
SPIFFS = FakeFilesystem();
|
||||
SPIFFS.emulate_stdio = true;
|
||||
const Host h = table(1);
|
||||
const auto old = image(h);
|
||||
const std::vector<uint8_t> stale = {1, 2, 3};
|
||||
SPIFFS.files["/contacts3"] = old;
|
||||
SPIFFS.files["/contacts3.tmp"] = stale;
|
||||
if (fault == 0) SPIFFS.fail_metadata_probe = 1; // target
|
||||
if (fault == 1) SPIFFS.fail_metadata_probe = 2; // backup
|
||||
if (fault == 2) {
|
||||
SPIFFS.files.erase("/contacts3");
|
||||
SPIFFS.files["/contacts3.bak"] = old;
|
||||
SPIFFS.fail_rename = 1; // failed interrupted-publication recovery
|
||||
}
|
||||
if (fault == 3) SPIFFS.fail_metadata_probe = 3; // stale temp
|
||||
if (fault == 4) SPIFFS.fail_remove = true;
|
||||
if (fault == 5 || fault == 6) SPIFFS.fail_create = true;
|
||||
if (fault == 6) SPIFFS.partial_create_failure = true;
|
||||
if (fault == 7) SPIFFS.fail_buffer_config = 1;
|
||||
{
|
||||
Writer writer(&SPIFFS, "/contacts3", companionPathPresence, true);
|
||||
auto progress = Writer::BeginProgress::Pending;
|
||||
for (unsigned pass = 0; pass < 8 && progress == Writer::BeginProgress::Pending; ++pass)
|
||||
progress = writer.serviceBegin();
|
||||
assert(progress == Writer::BeginProgress::Failed && !writer);
|
||||
assert(writer.write(old.data(), old.size()) == 0 && !writer.commit());
|
||||
}
|
||||
if (fault <= 4) assert(SPIFFS.files.at("/contacts3.tmp") == stale);
|
||||
else assert(!SPIFFS.exists("/contacts3.tmp")); // includes a failed partial create
|
||||
if (fault == 2) assert(SPIFFS.files.at("/contacts3.bak") == old);
|
||||
else assert(SPIFFS.files.at("/contacts3") == old);
|
||||
assert(SPIFFS.missing_remove_logs == 0);
|
||||
SPIFFS.fail_metadata_probe = SPIFFS.fail_rename = SPIFFS.fail_buffer_config = 0;
|
||||
SPIFFS.fail_create = SPIFFS.fail_remove = false;
|
||||
// All pre-existing callers still finish initialization inside construction.
|
||||
Writer synchronous(&SPIFFS, "/contacts3", companionPathPresence);
|
||||
assert(synchronous && SPIFFS.files.at("/contacts3") == old);
|
||||
assert(synchronous.write(old.data(), old.size()) == old.size());
|
||||
assert(synchronous.commit());
|
||||
assert(SPIFFS.files.at("/contacts3") == old);
|
||||
bootCheck(h);
|
||||
}
|
||||
}
|
||||
|
||||
void mutationDuringEveryBeginStage() {
|
||||
for (unsigned completed = 0; completed <= 7; ++completed) {
|
||||
SPIFFS = FakeFilesystem();
|
||||
SPIFFS.emulate_stdio = true;
|
||||
Host h = table();
|
||||
const auto old = image(h);
|
||||
const std::vector<uint8_t> stale = {4, 5, 6};
|
||||
SPIFFS.files["/contacts3"] = old;
|
||||
SPIFFS.files["/contacts3.tmp"] = stale;
|
||||
DataStore store(SPIFFS);
|
||||
assert(store.markContactDirty(h.contacts[0]));
|
||||
assert(store.serviceContactWrites(&h, nullptr)); // allocate only
|
||||
assert(SPIFFS.startupOperations() == 0);
|
||||
for (unsigned pass = 0; pass < completed; ++pass)
|
||||
assert(store.serviceContactWrites(&h, nullptr));
|
||||
const unsigned opens = SPIFFS.opens, configurations = SPIFFS.buffer_config_calls;
|
||||
const unsigned before_cancel = SPIFFS.startupOperations();
|
||||
h.contacts[2] = contact(909);
|
||||
assert(store.markContactDirty(h.contacts[2]));
|
||||
assert(store.serviceContactWrites(&h, nullptr)); // guard precedes every begin step
|
||||
assert(store.hasPendingContactWrites());
|
||||
assert(SPIFFS.opens == opens && SPIFFS.buffer_config_calls == configurations);
|
||||
if (completed < 6) assert(SPIFFS.startupOperations() == before_cancel);
|
||||
if (completed < 5) assert(SPIFFS.files.at("/contacts3.tmp") == stale);
|
||||
else assert(!SPIFFS.exists("/contacts3.tmp"));
|
||||
assert(SPIFFS.files.at("/contacts3") == old);
|
||||
drain(store, h);
|
||||
bootCheck(h);
|
||||
}
|
||||
}
|
||||
|
||||
void mutationRestart(unsigned phase) {
|
||||
SPIFFS = FakeFilesystem();
|
||||
SPIFFS.emulate_stdio = true;
|
||||
@@ -172,7 +329,7 @@ void mutationRestart(unsigned phase) {
|
||||
DataStore store(SPIFFS);
|
||||
assert(store.markContactDirty(h.contacts[0]));
|
||||
unsigned passes = 0;
|
||||
while ((phase == 0 && passes < 5)
|
||||
while ((phase == 0 && SPIFFS.writes < 5)
|
||||
|| (phase == 1 && (!store.verifying() || SPIFFS.reads == 0))
|
||||
|| (phase == 2 && !store.readyToPublish())) {
|
||||
assert(++passes < 1000);
|
||||
@@ -222,6 +379,7 @@ void faultAndRetry(unsigned fault) {
|
||||
success = store.serviceContactWrites(&h, nullptr);
|
||||
}
|
||||
assert(injected && !success);
|
||||
assert(!store.activeJob()); // no Ready/_ok=false job can spin in initialization
|
||||
assert(store.hasPendingContactWrites());
|
||||
assert(SPIFFS.files.at("/contacts3") == old);
|
||||
assert(!SPIFFS.exists("/contacts3.tmp"));
|
||||
@@ -359,6 +517,9 @@ void expectedAbsenceAndRealDeleteFailures() {
|
||||
|
||||
int main() {
|
||||
boundedFullTableAndFirstSave();
|
||||
deferredBeginCancellationAndCrashRecovery();
|
||||
deferredBeginFaultsAndSynchronousDefaults();
|
||||
mutationDuringEveryBeginStage();
|
||||
for (unsigned phase = 0; phase < 3; ++phase) mutationRestart(phase);
|
||||
for (unsigned fault = 0; fault < 10; ++fault) faultAndRetry(fault);
|
||||
syncFlushAndFilterChange();
|
||||
|
||||
@@ -22,6 +22,9 @@ class UncachedESPContactsTest(unittest.TestCase):
|
||||
def test_default_stdio_reader_readahead_is_detected(self):
|
||||
self.run_fixture(["-DBOARD_HAS_PSRAM=1"], disable="reader")
|
||||
|
||||
def test_synchronous_startup_batch_is_detected(self):
|
||||
self.run_fixture(["-DBOARD_HAS_PSRAM=1"], disable="begin")
|
||||
|
||||
def run_fixture(self, policy, disable=None):
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-esp-inline-contacts-") as directory:
|
||||
temp = Path(directory)
|
||||
@@ -47,6 +50,11 @@ class UncachedESPContactsTest(unittest.TestCase):
|
||||
"inline void makeBlobPath(",
|
||||
)
|
||||
implementation = "\n".join(method(source, signature) for signature in signatures)
|
||||
if disable == "begin":
|
||||
self.assertIn('"/contacts3", companionPathPresence, true)', implementation)
|
||||
implementation = implementation.replace(
|
||||
'"/contacts3", companionPathPresence, true)',
|
||||
'"/contacts3", companionPathPresence, false)')
|
||||
# Only the native SDK stat seam is substituted. The production
|
||||
# presence logic still distinguishes ENOENT from metadata failure.
|
||||
presence = method(source, "static bool companionPathPresence(")
|
||||
@@ -64,10 +72,10 @@ class UncachedESPContactsTest(unittest.TestCase):
|
||||
(temp / "store_under_test.h").write_text(implementation)
|
||||
transaction = (ROOT / "src/helpers/ContactFileTransaction.h").read_text()
|
||||
if disable == "writer":
|
||||
self.assertIn("_ok = _ok && _file.setBufferSize(storage::CONTACT_RECORD_SIZE);", transaction)
|
||||
self.assertIn("ok = _file.setBufferSize(storage::CONTACT_RECORD_SIZE);", transaction)
|
||||
transaction = transaction.replace(
|
||||
"_ok = _ok && _file.setBufferSize(storage::CONTACT_RECORD_SIZE);",
|
||||
"// Negative control: retain the SDK's default writer buffer.")
|
||||
"ok = _file.setBufferSize(storage::CONTACT_RECORD_SIZE);",
|
||||
"ok = true; // Negative control: retain default writer buffer.")
|
||||
elif disable == "reader":
|
||||
self.assertIn("ok = ok && _verify.setBufferSize(64);", transaction)
|
||||
transaction = transaction.replace("ok = ok && _verify.setBufferSize(64);",
|
||||
@@ -105,8 +113,9 @@ class UncachedESPContactsTest(unittest.TestCase):
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
else:
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("largest_backend_" + ("write" if disable == "writer" else "read"),
|
||||
result.stderr)
|
||||
marker = "startup_operations" if disable == "begin" else (
|
||||
"largest_backend_" + ("write" if disable == "writer" else "read"))
|
||||
self.assertIn(marker, result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in new issue
Block a user