mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-28 13:34:17 +00:00
fix: fail closed on SD recovery errors
This commit is contained in:
@@ -330,38 +330,41 @@ TileStoreResult MapTileStore::recoverEvictionTransaction() {
|
||||
TileStoreResult MapTileStore::recoverIndex() {
|
||||
TileStoreResult result = recoverEvictionTransaction();
|
||||
if (result != TileStoreResult::OK) return result;
|
||||
// With no manifest, every .evict file belongs to a committed transaction.
|
||||
// Remove these in a separate list pass so they cannot consume recovery
|
||||
// index slots, including when max_entries == HARD_MAX_ENTRIES.
|
||||
TileKey committed_evictions[HARD_MAX_ENTRIES] = {};
|
||||
std::uint16_t committed_count = 0U;
|
||||
result = storage_.beginList();
|
||||
if (result != TileStoreResult::OK) return result;
|
||||
// With no manifest, every .evict file belongs to a committed transaction
|
||||
// and every .tmp tile belongs to an uncommitted put. Remove cleanup artifacts
|
||||
// in bounded batches before collecting live generations, so any number of
|
||||
// stale tombstones and HARD_MAX live keys plus one crash temp remain recoverable.
|
||||
while (true) {
|
||||
char name[PATH_CAPACITY] = {};
|
||||
bool done = false;
|
||||
result = storage_.nextList(name, sizeof(name), done);
|
||||
if (result != TileStoreResult::OK) { storage_.endList(); return result; }
|
||||
if (done) break;
|
||||
TileKey key = {0U, 0U, 0U};
|
||||
std::uint8_t flag = 0U;
|
||||
result = parseOwnedPath(name, key, flag);
|
||||
if (result != TileStoreResult::OK) { storage_.endList(); return result; }
|
||||
if (flag == HAS_EVICT) {
|
||||
if (committed_count >= HARD_MAX_ENTRIES) {
|
||||
storage_.endList();
|
||||
return TileStoreResult::INDEX_FULL;
|
||||
TileKey cleanup_keys[HARD_MAX_ENTRIES] = {};
|
||||
std::uint8_t cleanup_flags[HARD_MAX_ENTRIES] = {};
|
||||
std::uint16_t cleanup_count = 0U;
|
||||
result = storage_.beginList();
|
||||
if (result != TileStoreResult::OK) return result;
|
||||
while (cleanup_count < HARD_MAX_ENTRIES) {
|
||||
char name[PATH_CAPACITY] = {};
|
||||
bool done = false;
|
||||
result = storage_.nextList(name, sizeof(name), done);
|
||||
if (result != TileStoreResult::OK) { storage_.endList(); return result; }
|
||||
if (done) break;
|
||||
TileKey key = {0U, 0U, 0U};
|
||||
std::uint8_t flag = 0U;
|
||||
result = parseOwnedPath(name, key, flag);
|
||||
if (result != TileStoreResult::OK) { storage_.endList(); return result; }
|
||||
if ((flag == HAS_EVICT) || (flag == HAS_TEMP)) {
|
||||
cleanup_keys[cleanup_count] = key;
|
||||
cleanup_flags[cleanup_count++] = flag;
|
||||
}
|
||||
committed_evictions[committed_count++] = key;
|
||||
}
|
||||
}
|
||||
storage_.endList();
|
||||
for (std::uint16_t i = 0U; i < committed_count; ++i) {
|
||||
char live[PATH_CAPACITY] = {}, evicted[PATH_CAPACITY] = {};
|
||||
canonicalPath(committed_evictions[i], live, sizeof(live));
|
||||
appendSuffix(live, ".evict", evicted, sizeof(evicted));
|
||||
result = storage_.remove(evicted);
|
||||
if ((result != TileStoreResult::OK) && (result != TileStoreResult::MISS)) return result;
|
||||
storage_.endList();
|
||||
if (cleanup_count == 0U) break;
|
||||
for (std::uint16_t i = 0U; i < cleanup_count; ++i) {
|
||||
char live[PATH_CAPACITY] = {}, artifact[PATH_CAPACITY] = {};
|
||||
canonicalPath(cleanup_keys[i], live, sizeof(live));
|
||||
appendSuffix(live, (cleanup_flags[i] == HAS_EVICT) ? ".evict" : ".tmp",
|
||||
artifact, sizeof(artifact));
|
||||
result = storage_.remove(artifact);
|
||||
if ((result != TileStoreResult::OK) && (result != TileStoreResult::MISS)) return result;
|
||||
}
|
||||
}
|
||||
|
||||
result = storage_.beginList();
|
||||
|
||||
@@ -4,11 +4,27 @@
|
||||
#include "MapTileStoreSD.h"
|
||||
|
||||
#ifdef ARDUINO
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <sys/stat.h>
|
||||
|
||||
namespace Hardware {
|
||||
namespace TDeck {
|
||||
|
||||
namespace {
|
||||
TileStoreResult statMountedPathLocked(const char* name, struct stat& info) {
|
||||
char mounted[MapTileStore::PATH_CAPACITY + 4U] = {};
|
||||
const int written = std::snprintf(mounted, sizeof(mounted), "/sd%s", name);
|
||||
if ((written < 0) || (static_cast<std::size_t>(written) >= sizeof(mounted))) {
|
||||
return TileStoreResult::INVALID_ARGUMENT;
|
||||
}
|
||||
errno = 0;
|
||||
if (::stat(mounted, &info) == 0) return TileStoreResult::OK;
|
||||
return (errno == ENOENT) ? TileStoreResult::MISS : TileStoreResult::IO_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
MapTileStoreSD::MapTileStoreSD()
|
||||
: stream_(), list_root_(), list_zoom_(), list_x_(), writing_(false), healthy_(true) {}
|
||||
MapTileStoreSD::~MapTileStoreSD() { abortWrite(); endRead(); endList(); }
|
||||
@@ -49,7 +65,9 @@ bool MapTileStoreSD::makeParentDirectoriesLocked(const char* name) {
|
||||
TileStoreResult MapTileStoreSD::beginRead(const char* name, std::uint32_t& size) {
|
||||
if (!healthy_ || !SDAccess::is_ready() || !SDAccess::acquire_bus(500U)) return TileStoreResult::STORAGE_UNAVAILABLE;
|
||||
if (!cardPresentLocked()) { SDAccess::release_bus(); return TileStoreResult::STORAGE_UNAVAILABLE; }
|
||||
if (!SD.exists(name)) { SDAccess::release_bus(); return TileStoreResult::MISS; }
|
||||
struct stat info = {};
|
||||
const TileStoreResult present = statMountedPathLocked(name, info);
|
||||
if (present != TileStoreResult::OK) { SDAccess::release_bus(); return present; }
|
||||
stream_ = SD.open(name, FILE_READ);
|
||||
if (!stream_) { SDAccess::release_bus(); return TileStoreResult::IO_ERROR; }
|
||||
const std::size_t file_size = stream_.size();
|
||||
@@ -124,17 +142,25 @@ void MapTileStoreSD::abortWrite() {
|
||||
TileStoreResult MapTileStoreSD::remove(const char* name) {
|
||||
if (!healthy_ || !SDAccess::is_ready() || !SDAccess::acquire_bus(500U)) return TileStoreResult::STORAGE_UNAVAILABLE;
|
||||
if (!cardPresentLocked()) { SDAccess::release_bus(); return TileStoreResult::STORAGE_UNAVAILABLE; }
|
||||
const bool existed = SD.exists(name);
|
||||
const bool removed = !existed || SD.remove(name);
|
||||
struct stat info = {};
|
||||
const TileStoreResult present = statMountedPathLocked(name, info);
|
||||
if (present != TileStoreResult::OK) { SDAccess::release_bus(); return present; }
|
||||
const bool removed = SD.remove(name);
|
||||
SDAccess::release_bus();
|
||||
return !existed ? TileStoreResult::MISS : (removed ? TileStoreResult::OK : TileStoreResult::IO_ERROR);
|
||||
return removed ? TileStoreResult::OK : TileStoreResult::IO_ERROR;
|
||||
}
|
||||
|
||||
TileStoreResult MapTileStoreSD::rename(const char* from, const char* to) {
|
||||
if (!healthy_ || !SDAccess::is_ready() || !SDAccess::acquire_bus(500U)) return TileStoreResult::STORAGE_UNAVAILABLE;
|
||||
if (!cardPresentLocked()) { SDAccess::release_bus(); return TileStoreResult::STORAGE_UNAVAILABLE; }
|
||||
if (!SD.exists(from)) { SDAccess::release_bus(); return TileStoreResult::MISS; }
|
||||
if (SD.exists(to) && !SD.remove(to)) { SDAccess::release_bus(); return TileStoreResult::IO_ERROR; }
|
||||
struct stat source_info = {}, destination_info = {};
|
||||
const TileStoreResult source = statMountedPathLocked(from, source_info);
|
||||
if (source != TileStoreResult::OK) { SDAccess::release_bus(); return source; }
|
||||
const TileStoreResult destination = statMountedPathLocked(to, destination_info);
|
||||
if ((destination != TileStoreResult::MISS) && (destination != TileStoreResult::OK)) {
|
||||
SDAccess::release_bus(); return destination;
|
||||
}
|
||||
if ((destination == TileStoreResult::OK) && !SD.remove(to)) { SDAccess::release_bus(); return TileStoreResult::IO_ERROR; }
|
||||
const bool renamed = SD.rename(from, to);
|
||||
SDAccess::release_bus();
|
||||
return renamed ? TileStoreResult::OK : TileStoreResult::IO_ERROR;
|
||||
@@ -143,13 +169,12 @@ TileStoreResult MapTileStoreSD::rename(const char* from, const char* to) {
|
||||
TileStoreResult MapTileStoreSD::stat(const char* name, std::uint32_t& size) {
|
||||
if (!healthy_ || !SDAccess::is_ready() || !SDAccess::acquire_bus(500U)) return TileStoreResult::STORAGE_UNAVAILABLE;
|
||||
if (!cardPresentLocked()) { SDAccess::release_bus(); return TileStoreResult::STORAGE_UNAVAILABLE; }
|
||||
fs::File file = SD.open(name, FILE_READ);
|
||||
if (!file) { SDAccess::release_bus(); return TileStoreResult::MISS; }
|
||||
const std::size_t file_size = file.size();
|
||||
file.close();
|
||||
struct stat info = {};
|
||||
const TileStoreResult present = statMountedPathLocked(name, info);
|
||||
SDAccess::release_bus();
|
||||
if (file_size > UINT32_MAX) return TileStoreResult::TOO_LARGE;
|
||||
size = static_cast<std::uint32_t>(file_size);
|
||||
if (present != TileStoreResult::OK) return present;
|
||||
if ((info.st_size < 0) || (static_cast<std::uint64_t>(info.st_size) > UINT32_MAX)) return TileStoreResult::TOO_LARGE;
|
||||
size = static_cast<std::uint32_t>(info.st_size);
|
||||
return TileStoreResult::OK;
|
||||
}
|
||||
|
||||
@@ -157,7 +182,14 @@ TileStoreResult MapTileStoreSD::beginList() {
|
||||
endList();
|
||||
if (!healthy_ || !SDAccess::is_ready() || !SDAccess::acquire_bus(500U)) return TileStoreResult::STORAGE_UNAVAILABLE;
|
||||
if (!cardPresentLocked()) { SDAccess::release_bus(); return TileStoreResult::STORAGE_UNAVAILABLE; }
|
||||
struct stat info = {};
|
||||
const TileStoreResult present = statMountedPathLocked("/pyxis-map/tiles", info);
|
||||
if (present == TileStoreResult::MISS) { SDAccess::release_bus(); return TileStoreResult::OK; }
|
||||
if ((present != TileStoreResult::OK) || !S_ISDIR(info.st_mode)) {
|
||||
SDAccess::release_bus(); return TileStoreResult::IO_ERROR;
|
||||
}
|
||||
list_root_ = SD.open("/pyxis-map/tiles", FILE_READ);
|
||||
if (!list_root_) { SDAccess::release_bus(); return TileStoreResult::IO_ERROR; }
|
||||
SDAccess::release_bus();
|
||||
return TileStoreResult::OK; // An absent cache directory is an empty cache.
|
||||
}
|
||||
@@ -170,28 +202,34 @@ TileStoreResult MapTileStoreSD::nextList(char* name, std::size_t capacity, bool&
|
||||
if (!cardPresentLocked()) { SDAccess::release_bus(); return TileStoreResult::STORAGE_UNAVAILABLE; }
|
||||
while (true) {
|
||||
if (list_x_) {
|
||||
errno = 0;
|
||||
fs::File item = list_x_.openNextFile();
|
||||
if (item) {
|
||||
const TileStoreResult copied = copyName(item.path(), name, capacity);
|
||||
item.close(); SDAccess::release_bus(); return copied;
|
||||
}
|
||||
if (errno != 0) { SDAccess::release_bus(); return TileStoreResult::IO_ERROR; }
|
||||
list_x_.close();
|
||||
}
|
||||
if (list_zoom_) {
|
||||
errno = 0;
|
||||
fs::File item = list_zoom_.openNextFile();
|
||||
if (item) {
|
||||
if (item.isDirectory()) { list_x_ = item; continue; }
|
||||
const TileStoreResult copied = copyName(item.path(), name, capacity);
|
||||
item.close(); SDAccess::release_bus(); return copied;
|
||||
}
|
||||
if (errno != 0) { SDAccess::release_bus(); return TileStoreResult::IO_ERROR; }
|
||||
list_zoom_.close();
|
||||
}
|
||||
errno = 0;
|
||||
fs::File item = list_root_.openNextFile();
|
||||
if (item) {
|
||||
if (item.isDirectory()) { list_zoom_ = item; continue; }
|
||||
const TileStoreResult copied = copyName(item.path(), name, capacity);
|
||||
item.close(); SDAccess::release_bus(); return copied;
|
||||
}
|
||||
if (errno != 0) { SDAccess::release_bus(); return TileStoreResult::IO_ERROR; }
|
||||
done = true;
|
||||
SDAccess::release_bus();
|
||||
return TileStoreResult::OK;
|
||||
|
||||
@@ -55,5 +55,14 @@ def test_sd_adapter_distinguishes_missing_files_from_open_failures():
|
||||
source = SD_CPP.read_text()
|
||||
body = source[source.index("TileStoreResult MapTileStoreSD::beginRead"):
|
||||
source.index("TileStoreResult MapTileStoreSD::readChunk")]
|
||||
assert body.index("SD.exists(name)") < body.index("SD.open(name, FILE_READ)")
|
||||
assert "statMountedPathLocked(name, info)" in body
|
||||
assert body.index("statMountedPathLocked(name, info)") < body.index("SD.open(name, FILE_READ)")
|
||||
assert "if (!stream_) { SDAccess::release_bus(); return TileStoreResult::IO_ERROR; }" in body
|
||||
stat_body = source[source.index("TileStoreResult MapTileStoreSD::stat"):
|
||||
source.index("TileStoreResult MapTileStoreSD::beginList")]
|
||||
assert "SD.open" not in stat_body
|
||||
assert "statMountedPathLocked(name, info)" in stat_body
|
||||
list_body = source[source.index("TileStoreResult MapTileStoreSD::beginList"):
|
||||
source.index("TileStoreResult MapTileStoreSD::nextList")]
|
||||
assert "present == TileStoreResult::MISS" in list_body
|
||||
assert "if (!list_root_)" in list_body and "TileStoreResult::IO_ERROR" in list_body
|
||||
|
||||
@@ -266,8 +266,18 @@ void testPostCommitCleanupResidueKeepsWholeNewGeneration() { beginTest(); FakeSt
|
||||
std::uint32_t size=0U; CHECK(s.beginGet(a,size)==TileStoreResult::MISS);
|
||||
CHECK(fs.find("/pyxis-map/tiles/1/0/0.png.evict")<0);
|
||||
}
|
||||
void testHardMaxLivePlusCrashTempRecovers() { beginTest(); FakeStorage fs;
|
||||
for(std::uint32_t i=0U;i<128U;++i) { const std::string path="/pyxis-map/tiles/8/"+std::to_string(i)+"/0.png"; fs.add(path.c_str(),png()); }
|
||||
fs.add("/pyxis-map/tiles/8/128/0.png.tmp",png());
|
||||
MapTileStore s(fs,config(128U,5120U,80U)); CHECK(s.initialize()==TileStoreResult::OK); CHECK(s.entryCount()==128U); CHECK(fs.find("/pyxis-map/tiles/8/128/0.png.tmp")<0);
|
||||
}
|
||||
void testUnboundedCommittedEvictionResidueCleansInBatches() { beginTest(); FakeStorage fs;
|
||||
for(std::uint32_t i=0U;i<129U;++i) { const std::string path="/pyxis-map/tiles/8/"+std::to_string(i)+"/0.png.evict"; fs.add(path.c_str(),png()); }
|
||||
MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::OK); CHECK(s.entryCount()==0U);
|
||||
for(std::uint32_t i=0U;i<129U;++i) { const std::string path="/pyxis-map/tiles/8/"+std::to_string(i)+"/0.png.evict"; CHECK(fs.find(path.c_str())<0); }
|
||||
}
|
||||
void testDeterministicStress() { beginTest(); FakeStorage fs; MapTileStore s(fs,config(3U,120U,80U)); CHECK(s.initialize()==TileStoreResult::OK); CHECK(put(s,TileKey{2U,0U,0U},png())==TileStoreResult::OK);
|
||||
std::uint32_t size=0U; for(std::uint32_t i=0U;i<100000U;++i) { const TileKey k={2U,i&3U,(i>>2)&3U}; TileStoreResult r=s.beginGet(k,size); CHECK(r==TileStoreResult::OK||r==TileStoreResult::MISS); if(r==TileStoreResult::OK)s.endGet(); }
|
||||
}
|
||||
}
|
||||
int main() { testKeyAndCanonicalPath(); testMissHitAndRemoval(); testMalformedPngs(); testShortWriteAbortsTemp(); testExactQuotaAndLruEviction(); testDuplicateAtomicReplacement(); testInterruptedFilesRecover(); testLiveWinsRecovery(); testCorruptLiveRecoversValidBackup(); testCorruptLiveWithoutBackupIsRemoved(); testStaleTempRemovalFailureAbortsPut(); testRecoveryRejectsMalformedAndExhaustion(); testRecoveryQuotaFailsClosed(); testRenameFailureRestoresDuplicate(); testDuplicateRollbackFailureInvalidatesStore(); testPromotionFailureDoesNotEvictVictims(); testEvictionPreflightFailurePreservesAllVictims(); testEvictionStageFailureRollsBackAllVictims(); testEvictionPowerCutsRestoreWholeOldGeneration(); testDuplicateEvictionPowerCutsRestoreOldCandidateAndVictim(); testStaleDuplicateBackupMustClearBeforeManifest(); testSemanticManifestValidationPrecedesMutation(); testMalformedEvictionManifestFailsClosed(); testPostCommitCleanupResidueKeepsWholeNewGeneration(); testDeterministicStress(); std::cout<<"map tile store: "<<tests_run<<" tests passed\n"; }
|
||||
int main() { testKeyAndCanonicalPath(); testMissHitAndRemoval(); testMalformedPngs(); testShortWriteAbortsTemp(); testExactQuotaAndLruEviction(); testDuplicateAtomicReplacement(); testInterruptedFilesRecover(); testLiveWinsRecovery(); testCorruptLiveRecoversValidBackup(); testCorruptLiveWithoutBackupIsRemoved(); testStaleTempRemovalFailureAbortsPut(); testRecoveryRejectsMalformedAndExhaustion(); testRecoveryQuotaFailsClosed(); testRenameFailureRestoresDuplicate(); testDuplicateRollbackFailureInvalidatesStore(); testPromotionFailureDoesNotEvictVictims(); testEvictionPreflightFailurePreservesAllVictims(); testEvictionStageFailureRollsBackAllVictims(); testEvictionPowerCutsRestoreWholeOldGeneration(); testDuplicateEvictionPowerCutsRestoreOldCandidateAndVictim(); testStaleDuplicateBackupMustClearBeforeManifest(); testSemanticManifestValidationPrecedesMutation(); testMalformedEvictionManifestFailsClosed(); testPostCommitCleanupResidueKeepsWholeNewGeneration(); testHardMaxLivePlusCrashTempRecovers(); testUnboundedCommittedEvictionResidueCleansInBatches(); testDeterministicStress(); std::cout<<"map tile store: "<<tests_run<<" tests passed\n"; }
|
||||
|
||||
@@ -29,4 +29,4 @@ def test_bounded_map_tile_store(tmp_path: Path, sanitize: bool) -> None:
|
||||
env["UBSAN_OPTIONS"] = "halt_on_error=1:print_stacktrace=1"
|
||||
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60, env=env)
|
||||
assert ran.returncode == 0, ran.stdout + ran.stderr
|
||||
assert ran.stdout == "map tile store: 25 tests passed\n"
|
||||
assert ran.stdout == "map tile store: 27 tests passed\n"
|
||||
|
||||
Reference in New Issue
Block a user