From ff141b0a308bc03528ca24084d8822ce645a62be Mon Sep 17 00:00:00 2001 From: Martin Dobrev Date: Tue, 10 Nov 2026 20:04:09 +0000 Subject: [PATCH] Use at least two segments so a single-segment store keeps its data Compaction writes the live records into seg0, and both the rotation and the threshold paths then move new writes to seg1. A store initialised with segment_count 1 therefore writes everything after its first compaction into seg1, outside the range that compaction and index rebuilds scan. That file grows without bound and its records are dropped on the next reload. This predates the recent compaction changes; 0.1.7 behaves the same. init() now raises a segment_count below 2 to 2, which covers counts set through the constructor, init() and USTORE_DEFAULT_SEGMENT_COUNT alike. Adds test_file_store_single_segment_count_keeps_data. --- CHANGELOG.md | 1 + docs/microstore_user_guide.md | 2 +- include/microStore/FileStore.h | 8 ++++++ test/test_file_store/test_file_store.cpp | 36 ++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad1a945..dc35c96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed `FileStore` losing records when configured with a single segment: after the first compaction new writes went to a segment outside the configured range and were dropped on reload. `init()` now uses a minimum of two segments. - Fixed `FileStore` compaction running with the active segment still open, which LittleFS and FatFS refuse to delete, and fixed the store being left without a writable segment (or with a stray out-of-range segment file) after a failed compaction (#6, #11). - Fixed compaction recovery paths that could lose data: a power loss after the compacted segment was moved into place but before the journal was cleared deleted all data on the next boot; a failed rename while finalizing deleted all data and still reported success; recovery from a compaction interrupted mid-way kept a stale index, and a later compaction attempt could truncate the only copy of the interrupted compaction's records. A compaction interrupted by a write failure is now recovered immediately rather than at the next boot. diff --git a/docs/microstore_user_guide.md b/docs/microstore_user_guide.md index f821e13..6ea024d 100644 --- a/docs/microstore_user_guide.md +++ b/docs/microstore_user_guide.md @@ -241,7 +241,7 @@ if (!store.init(fs, "/cfg")) { Optional `init()` arguments: - `clearOnInit` — wipe everything before opening (useful for tests). -- `segment_size`, `segment_count` — override the defaults at runtime (still bounded by the compile-time `USTORE_MAX_*` macros). +- `segment_size`, `segment_count` — override the defaults at runtime (still bounded by the compile-time `USTORE_MAX_*` macros). `segment_count` has a minimum of 2: compaction writes into seg0 and new records then go to seg1, so a smaller value is raised to 2. Always call `store.close()` before destruction if you want a clean flush; the destructor does flush, but `close()` also closes the index and active segment explicitly. diff --git a/include/microStore/FileStore.h b/include/microStore/FileStore.h index 264b275..01af36b 100644 --- a/include/microStore/FileStore.h +++ b/include/microStore/FileStore.h @@ -142,6 +142,14 @@ class BasicFileStore { if (segment_size > 0) _segment_size = segment_size; if (segment_count > 0) _segment_count = segment_count; + // Compaction writes the live records into seg0 and new writes then go to + // seg1, so a store needs at least two segments. With one, every write + // after the first compaction lands in seg1, outside the range that + // compaction and index rebuilds scan, and is lost on the next reload. + if (_segment_count < 2) { + USTORE_LOG("[ustore] init: segment_count %u is below the minimum of 2, using 2\n", _segment_count); + _segment_count = 2; + } USTORE_LOG("[ustore] init: Initializing FileStore with prefix=%s, segment_size=%u, segment_count=%u\n", prefix, _segment_size, _segment_count); _filesystem = filesystem; diff --git a/test/test_file_store/test_file_store.cpp b/test/test_file_store/test_file_store.cpp index f8ce6f4..1d929b8 100644 --- a/test/test_file_store/test_file_store.cpp +++ b/test/test_file_store/test_file_store.cpp @@ -1104,6 +1104,41 @@ void test_dir_prefix_compact() { void setUp() {} void tearDown() {} +// A store needs two segments: compaction fills seg0 and new writes go to seg1. +// Asked for one, init() must use two, or every write after the first +// compaction lands outside the scanned range and is lost on reload. +void test_file_store_single_segment_count_keeps_data() { + reset_ram_fs(); + + uint32_t now = microStore::time() - 10; + char k[8], v[32]; + { + microStore::FileStore store; + auto fs = make_ram_fs(); + store.init(fs, "/p", /*clearOnInit=*/false, /*segment_size=*/256, /*segment_count=*/1); + // Overwrite ten keys six times: enough to fill a 256-byte segment + // several times over and force repeated compaction. + for (int i = 0; i < 60; i++) { + snprintf(k, sizeof(k), "k%02d", i % 10); + snprintf(v, sizeof(v), "value-%02d-xxxxxxxx", i); + TEST_ASSERT_TRUE(store.put(k, v, /*ttl=*/0, now)); + } + } + + microStore::FileStore reader; + auto fs = make_ram_fs(); + reader.init(fs, "/p", /*clearOnInit=*/false, /*segment_size=*/256, /*segment_count=*/1); + TEST_ASSERT_EQUAL(10u, reader.size()); + for (int i = 0; i < 10; i++) { + snprintf(k, sizeof(k), "k%02d", i); + snprintf(v, sizeof(v), "value-%02d-xxxxxxxx", 50 + i); + uint8_t buf[64]; uint16_t sz = sizeof(buf); + TEST_ASSERT_TRUE(reader.get(k, buf, &sz)); + TEST_ASSERT_EQUAL(strlen(v), sz); + TEST_ASSERT_EQUAL(0, memcmp(buf, v, sz)); + } +} + int runUnityTests(void) { UNITY_BEGIN(); // TTL tests @@ -1127,6 +1162,7 @@ int runUnityTests(void) { // Additional edge-case tests RUN_TEST(test_file_store_ttl_exists_expires); RUN_TEST(test_file_store_compact_basic); + RUN_TEST(test_file_store_single_segment_count_keeps_data); RUN_TEST(test_file_store_compact_keeps_store_writable); RUN_TEST(test_file_store_compact_failure_keeps_store_writable); RUN_TEST(test_file_store_threshold_compact_failure_keeps_store_writable);