]> git.hungrycats.org Git - bees/commit
roots: checkpoint on content change, not a dirty counter
authorZygo Blaxell <bees@furryterror.org>
Mon, 24 Aug 2026 17:18:33 +0000 (13:18 -0400)
committerZygo Blaxell <bees@furryterror.org>
Sat, 5 Sep 2026 04:03:58 +0000 (00:03 -0400)
commit1980df21b8a9c1fec470aa1fffb68b7f42030c31
tree0538e6eb128bc33a0844bea1f66371601625206d
parenta31615760df82cebd736cd3de0ca68d62dcc6c3d
roots: checkpoint on content change, not a dirty counter

snapshot_point() gated the checkpoint write on a generation counter pair,
m_crawl_dirty/m_crawl_clean, bumped only by BeesCrawl::set_state() (via
crawl_state_set_dirty()), crawl_state_erase(), insert_root(), and
insert_extent_crawler().

In extent scan mode the crawl position never passes through set_state().
find_next_extent() and scan_one_ref() advance the cursor exclusively with
hold_state(), and the value snapshot_point() serialises is
get_state_begin() -- the ProgressTracker low-water mark, which advances as
holders retire.  Holder retirement is deliberately decoupled from
BeesRoots::m_mutex, so it cannot bump a counter guarded by that mutex.
The counter therefore tracked a different quantity than the one being
written, and snapshot_point() logged "Nothing to save" and returned even
though the position had moved.

The only remaining dirty events in extent scan mode were tier insertion at
startup and restart_crawl() at the end of a pass.  beespoint.ini was thus
rewritten roughly once per scan pass, and every restart resumed from the
last pass boundary, discarding all progress since.

The end-of-pass trigger made sense when a scan task began and ended within
a short interval.  It no longer holds: resolving lock contention can keep
a task open for days.

Replace the counter pair with m_point_saved, the serialised INI of the
last checkpoint written (immediate mode) or enqueued (deferred mode).
snapshot_point() now builds ini_string first and returns early only when
it is byte-identical.  Comparing the bytes that would be written against
the bytes that were cannot drift out of sync the way a side counter can,
and it is self-correcting: a new progress path changes the serialised
state without having to remember to signal anything.  At one serialisation
per checkpoint interval the cost is immaterial.

m_point_saved is updated after a successful write in immediate mode, so a
throw leaves it stale and the next tick retries.  In deferred mode it is
updated at enqueue, because the pending queue already owns the write
obligation and retries on failure; re-enqueueing an identical copy would
only create an entry for the coalescer to discard.

Under state.point.defer this restores continuous occupancy of the pending
queue, which was previously fed only at pass boundaries.  In the steady
state one entry is queued and one written per checkpoint interval, with a
depth of roughly (hash table sweep time / checkpoint interval).  Each
entry carries its own writeback_count, so an older entry becomes safe to
write sooner, and flush_pending_checkpoints() always writes the most
advanced position that is currently safe.

The writeback thread's idle status note now reports that queue depth
instead of the removed dirty/clean flag, so the backlog is visible in
BEESSTATUS.  It is read under m_mutex before m_stop_mutex is acquired,
introducing no new lock nesting.

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