Disagg update-restore eviction with zero surviving entries trips the no-progress assertion in __reconcile

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Unresolved
    • Priority: Major - P3
    • None
    • Affects Version/s: None
    • Component/s: Reconciliation
    • None
    • Storage Engines, Storage Engines - Transactions
    • 0.458
    • SE Transactions - 2026-09-11
    • 3

      Observed

      model-test-long-disagg failed on branch wt-18482-checkpoint-two-phase-sync (commit f6a31e68e6797e113f4398d85dec9b9fdc47d046):
      https://spruce.corp.mongodb.com/task/wiredtiger_feature_ubuntu2004_model_test_long_disagg_f6a31e68e6797e113f4398d85dec9b9fdc47d046_26_09_07_09_15_38/logs?execution=0

      [ERROR]: __reconcile, 428: WiredTiger assertion failed: '!(((btree)->flags & 0x004000u) != 0) ||
        (((r->multi)->flags & 0x1u) != 0) || page->disagg_info->block_meta.page_id == 0 ||
        ((page->type == 3 || page->type == 6) && ... && delta_count == max_consecutive_delta)'
      [ERROR]: __wt_abort, 29: aborting WiredTiger library
      

      This is pre-existing on develop, not a regression from the two-phase-sync branch. The same workload was replayed against a build at the merge-base commit 8a33b297a9 (develop, before any wt-18482 commits) and crashes identically.

      Reproduction

      Build with diagnostics and the model test enabled, then replay the task's model_test.workload artifact:

      cmake -B build -G Ninja -DHAVE_DIAGNOSTIC=1 -DENABLE_MODEL=1 -DCMAKE_BUILD_TYPE=Debug
      ninja model_test wiredtiger_palite
      ./test/model/tools/model_test -w model_test.workload -G disaggregated=1 -R
      

      Crashes deterministically in well under a minute. Use -R to skip the tool's own counterexample reducer — for this failure the reducer's "still fails" check accepts any failure as equivalent, and after round 168 it drifts from the original assertion onto an unrelated "Failed transaction requires rollback" model mismatch. The reduced.workload artifact the task uploaded reproduces that unrelated mismatch, not this crash.

      Root cause

      From gdb inspection of the WTI_RECONCILE/WT_MULTI state at the crash frame: a disaggregated row-store leaf page previously written to disk (valid page_id, e.g. 140) undergoes update-restore eviction where every key's content is restored to the update chain rather than written, leaving chunk->entries == 0.

      src/reconcile/rec_write.c:2691-2693 special-cases chunk->entries == 0 for disagg pages with an early goto copy_image:

      if (r->page->disagg_info != NULL) {
          if (chunk->entries == 0)
              goto copy_image;
      } else if (F_ISSET(multi, WT_MULTI_SUPD_RESTORE))
          goto copy_image;
      

      Unlike the neighboring skip_write path a few lines above (which calls _rec_copy_prev_addr and sets WT_MULTI_SKIP_WRITE), this branch does neither: multi->block_meta is left as the zeroed struct from its _wt_calloc_one allocation.

      The "did reconciliation make progress" check in __reconcile (rec_write.c:418-422) correctly determines no progress was made, and the WT-16244 assertion that follows only recognizes three legitimate no-write cases — WT_MULTI_SKIP_WRITE set, invalid page_id, or an internal page at max consecutive delta count. This fourth case (disagg leaf, zero entries, update-restore) isn't one of them, so it fires.

      Beyond the diagnostic assert: in __rec_write_wrapup's 1-for-1 swap handling (rec_write.c:3318-3320), page->disagg_info->block_meta is unconditionally overwritten from that zeroed multi->block_meta:

      if (F_ISSET(r, WT_REC_IN_MEMORY) || F_ISSET(r->multi, WT_MULTI_SUPD_RESTORE)) {
          if (page->disagg_info != NULL)
              page->disagg_info->block_meta = *r->multi->block_meta;
          ...
      }
      

      In a non-diagnostic build this assert wouldn't fire at all, and the page would silently forget its real on-disk page_id (e.g. 140 → 0/invalid) — a correctness/block-leak risk, not just an overly strict assertion.

      Likely introduced by WT-16244 (PR #12892), which added this early exit and the assertion together but didn't account for the update-restore/zero-entries combination.

      Suggested fix

      In the chunk->entries == 0 disagg branch, mirror the skip_write path: call __rec_copy_prev_addr and F_SET(multi, WT_MULTI_SKIP_WRITE) before goto copy_image, so the page's previous on-disk address/block_meta is preserved and the no-progress check recognizes this as a legitimate skip.

            Assignee:
            Chenhao Qu
            Reporter:
            Chenhao Qu
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated: