]> git.hungrycats.org Git - bees/commitdiff
checkpoint: defer beespoint.ini writes behind hash writeback
authorZygo Blaxell <bees@furryterror.org>
Thu, 23 Apr 2026 21:54:02 +0000 (17:54 -0400)
committerZygo Blaxell <bees@furryterror.org>
Sat, 5 Sep 2026 04:03:49 +0000 (00:03 -0400)
Add a deferred-checkpoint queue so that crawl checkpoints do not advance
past hash table contents that are still buffered in memory.  Without
this, a crash could leave beespoint.ini claiming progress beyond what
beeshash.dat has persisted, causing missed deduplication opportunities on
the next run.

BeesRoots changes:

  * Split save_point() into snapshot_point() (mutate m_point_ini while
    holding m_mutex) and flush_pending_checkpoints() (drain the queue
    outside the lock).  save_point() now orchestrates both.

  * Snapshot takes hash_table()->writeback_extent_count() at enqueue
    time; flush releases entries only once the writeback counter has
    advanced by at least extent_count() entries, guaranteeing every
    extent modified since the snapshot is on disk.  force=true (used
    at shutdown) drains unconditionally.

  * writeback_thread() now polls at m_checkpoint_interval seconds
    instead of the BEES_WRITEBACK_INTERVAL compile-time constant, so
    the state.point.interval config key takes effect.

  * Add load_crawl_state() and print_crawl_state() as the public
    BeesRoots entry points that BeesContext::{load,print}_state()
    will call.

  * Add flush_deferred_checkpoints() for the shutdown path; BeesContext
    invokes it after hash_table()->stop_wait() so pending queue entries
    are flushed to disk before exit.

Config-driven behaviour matrix:

  state.point.defer=no  (default)  - snapshot writes inline, queue unused
  state.point.defer=yes            - snapshot enqueues; writer drains
                                     lazily in save_point() and again
                                     at shutdown via the BeesContext
                                     stop() path.

Assisted-by: Claude-Code:claude-opus-4-7
Signed-off-by: Zygo Blaxell <bees@furryterror.org>
src/bees-context.cc
src/bees-hash.cc
src/bees-roots.cc
src/bees.h

index 92d2c107ba8acd9cbd755a93f7f8e38de3f97646..a6f7999eecf4ff5f00b82f29bb917b71b8a7bb1c 100644 (file)
@@ -1308,6 +1308,13 @@ BeesContext::stop()
                m_hash_table->stop_wait();
        }
 
+       // Flush any deferred checkpoints that were waiting for hash writeback
+       if (m_roots) {
+               BEESNOTE("flushing deferred crawl checkpoints");
+               BEESLOGDEBUG("Flushing deferred crawl checkpoints");
+               catch_all([&]() { m_roots->flush_deferred_checkpoints(); });
+       }
+
        // Stop the status reporter
        BEESNOTE("stopping reporter at " << stop_timer << " sec");
        BEESLOGDEBUG("Stopping reporter");
index 25890dd10f582ada3f3db831330d5d45387c6532..7181fbf0f2d1d10c31840e686a5e31d69b58bec9 100644 (file)
@@ -226,8 +226,10 @@ BeesHashTable::writeback_loop()
        // some hash table pages during the second flush.  These updates
        // will be lost.  The Tasks will be repeated on the next run because
        // they were not completed prior to the stop request, and the
-       // Crawl progress was already flushed out before the Hash table
-       // started writing, so nothing is really lost here.
+       // Crawl progress checkpoint was captured before the Hash table
+       // started writing (with checkpoint deferral it is written out after
+       // this flush, but its contents are the pre-flush scan position),
+       // so nothing is really lost here.
 
        catch_all([&]() {
                // trigger writeback on our way out
index b766b837d35d8eac5613178ab067fedf5b00876b..d8a06a56091b7a6e969656fb001ae28cbbeef017 100644 (file)
@@ -1518,7 +1518,7 @@ BeesRoots::set_poll_bounds(double min_seconds, double max_seconds)
 void
 BeesRoots::set_checkpoint_interval(double seconds)
 {
-       THROW_CHECK1(invalid_argument, seconds, seconds >= 0);
+       THROW_CHECK1(invalid_argument, seconds, seconds > 0);
        m_checkpoint_interval = seconds;
        BEESLOGINFO("checkpoint interval = " << m_checkpoint_interval << "s [state.point.interval]");
 }
@@ -1533,10 +1533,16 @@ BeesRoots::set_persistent(bool v)
 void
 BeesRoots::set_checkpoint_defer(bool v)
 {
-       BEESLOGINFO("state defer = " << (v ? "yes" : "no") << " [state.point.defer]");
+       BEESLOGINFO("defer checkpoint = " << (v ? "yes" : "no") << " [state.point.defer]");
        m_checkpoint_defer = v;
 }
 
+void
+BeesRoots::flush_deferred_checkpoints()
+{
+       flush_pending_checkpoints(true);
+}
+
 uint64_t
 BeesRoots::effective_transid_max()
 {
@@ -1785,15 +1791,9 @@ BeesRoots::import_legacy()
 }
 
 void
-BeesRoots::save_point()
+BeesRoots::snapshot_point()
 {
-       BEESNOTE("saving beespoint.ini");
-       BEESLOGINFO("Saving beespoint.ini");
-       BEESTOOLONG("Saving beespoint.ini");
-
-       Timer save_time;
-
-       unique_lock<mutex> lock(m_mutex);
+       // Must already be holding m_mutex.
 
        if (m_crawl_clean == m_crawl_dirty) {
                BEESLOGINFO("Nothing to save");
@@ -1822,7 +1822,7 @@ BeesRoots::save_point()
                                m_point_ini.remove_section(section);
                        }
                } else {
-                       BEESLOGWARN("save_point: unrecognised root " << ibcs.m_root << ", skipping");
+                       BEESLOGWARN("snapshot_point: unrecognised root " << ibcs.m_root << ", skipping");
                }
        }
 
@@ -1834,15 +1834,94 @@ BeesRoots::save_point()
 
        const string ini_string = m_point_ini.write();
        const auto crawl_saved = m_crawl_dirty;
-       lock.unlock();
 
-       m_point_file.write(ini_string);
+       if (!m_checkpoint_defer) {
+               // Immediate write (current behaviour): write while lock is temporarily released.
+               // We must re-acquire the lock before updating m_crawl_clean.
+               // Use a raw unlock/lock here since snapshot_point() is called with lock held.
+               m_mutex.unlock();
+               m_point_file.write(ini_string);
+               m_mutex.lock();
+               m_crawl_clean = crawl_saved;
+       } else {
+               // Deferred write: enqueue with current writeback counter.
+               const auto wc = m_ctx->hash_table()->writeback_extent_count();
+               m_pending_checkpoints.push_back({ wc, ini_string });
+               m_crawl_clean = crawl_saved;
+       }
+}
 
-       lock.lock();
-       m_crawl_clean = crawl_saved;
+void
+BeesRoots::flush_pending_checkpoints(bool force)
+{
+       while (true) {
+               string last_ini;
+               {
+                       unique_lock<mutex> lock(m_mutex);
+                       if (m_pending_checkpoints.empty()) return;
+
+                       const auto extent_count  = m_ctx->hash_table()->extent_count();
+                       const auto writeback_now = m_ctx->hash_table()->writeback_extent_count();
+
+                       // Drain all entries eligible under writeback_now (or all if forced),
+                       // keeping only the last one's content to write.
+                       while (!m_pending_checkpoints.empty()) {
+                               const auto &entry = m_pending_checkpoints.front();
+                               if (!force && writeback_now - entry.writeback_count < extent_count) {
+                                       break;
+                               }
+                               last_ini = std::move(entry.ini_string);
+                               m_pending_checkpoints.pop_front();
+                       }
+               }  // lock released here
+
+               if (last_ini.empty()) return;   // nothing became eligible
+               m_point_file.write(last_ini);   // slow I/O outside the lock
+               // loop: more entries may be eligible now
+       }
+}
+
+void
+BeesRoots::save_point()
+{
+       BEESNOTE("saving beespoint.ini");
+       BEESLOGINFO("Saving beespoint.ini");
+       BEESTOOLONG("Saving beespoint.ini");
+
+       if (!m_roots_persistent) {
+               return;
+       }
+
+       Timer save_time;
+
+       m_mutex.lock();
+       snapshot_point();   // enqueue or write immediately (may unlock/relock m_mutex)
+       m_mutex.unlock();
+
+       if (m_checkpoint_defer) {
+               flush_pending_checkpoints(false);
+       }
        BEESLOGINFO("Saved beespoint.ini in " << save_time << "s");
 }
 
+void
+BeesRoots::load_crawl_state()
+{
+       if (!m_roots_persistent) {
+               return;
+       }
+       if (!load_point()) {
+               import_legacy();
+       }
+}
+
+void
+BeesRoots::print_crawl_state(ostream &os)
+{
+       unique_lock<mutex> lock(m_mutex);
+       os << m_point_ini.write();
+}
+
 void
 BeesRoots::crawl_state_set_dirty()
 {
@@ -2156,7 +2235,7 @@ BeesRoots::writeback_thread()
                        });
                        return;
                }
-               m_stop_condvar.wait_for(lock, chrono::duration<double>(BEES_WRITEBACK_INTERVAL));
+               m_stop_condvar.wait_for(lock, chrono::duration<double>(m_checkpoint_interval));
        }
 }
 
index b6e49a84324872fe78bfc5a7a6e08e63bd52fb0d..c91847f43d73c21b5f949a2330cf0cc7573b34fd 100644 (file)
@@ -27,6 +27,7 @@
 
 #include <array>
 #include <atomic>
+#include <deque>
 #include <functional>
 #include <list>
 #include <mutex>
@@ -1006,6 +1007,12 @@ class BeesRoots : public enable_shared_from_this<BeesRoots> {
        double                          m_checkpoint_interval = BEES_WRITEBACK_INTERVAL;  ///< Seconds between checkpoint writes.
        bool                            m_roots_persistent = true;      ///< If false, skip load/save of beespoint.ini.
        bool                            m_checkpoint_defer = false;     ///< If true, defer writes until hash writeback catches up.
+       /// A checkpoint snapshot waiting to be written to disk.
+       struct PendingCheckpoint {
+               uint64_t writeback_count;  ///< m_hash_table->writeback_extent_count() at snapshot time.
+               string   ini_string;       ///< Serialised beespoint.ini content.
+       };
+       deque<PendingCheckpoint>        m_pending_checkpoints;   ///< Queue of deferred checkpoints.
 
        vector<shared_ptr<BeesScanMode>>        m_scanners;  ///< Active scan-mode strategy objects.
 
@@ -1030,8 +1037,6 @@ class BeesRoots : public enable_shared_from_this<BeesRoots> {
        /// state.  If beescrawl.dat is also absent, both maps are left empty (first run).
        /// beescrawl.dat is never modified.
        void import_legacy();
-       /// Write current m_extent_crawl_map and m_root_crawl_map to beespoint.ini.
-       void save_point();
        void crawl_state_set_dirty();
        void crawl_state_erase(const BeesCrawlState &bcs);
        bool all_crawlers_finished();
@@ -1050,6 +1055,8 @@ class BeesRoots : public enable_shared_from_this<BeesRoots> {
        /// Return true if @p bcs.m_max_transid is already at or beyond the
        /// configured transid bound.
        bool up_to_date(const BeesCrawlState &bcs);
+       void snapshot_point();
+       void flush_pending_checkpoints(bool force);
 
 friend class BeesCrawl;
 friend class BeesScanMode;
@@ -1065,6 +1072,13 @@ public:
        /// Block until background threads have stopped.
        void stop_wait();
 
+       /// Load beespoint.ini (or migrate from beescrawl.dat on first run).
+       void load_crawl_state();
+       /// Write current m_extent_crawl_map and m_root_crawl_map to beespoint.ini.
+       void save_point();
+       /// Serialize the current crawl state and write it to @p os in INI format.
+       void print_crawl_state(ostream &os);
+
        /// Return true if the subvolume @p root is mounted read-only.
        bool is_root_ro(uint64_t root);
 
@@ -1108,6 +1122,8 @@ public:
        void set_persistent(bool v);
        /// Defer checkpoint writes until the hash table writeback has lapped past the snapshot point.
        void set_checkpoint_defer(bool v);
+       /// Write any remaining deferred checkpoints (call after hash table stop_wait() at shutdown).
+       void flush_deferred_checkpoints();
 
        /// Return the lowest transaction ID seen across all active crawls.
        uint64_t transid_min();