microsoft/FASTER · error

Number of non-mutable pages (%u) is less than…

Error message

Number of non-mutable pages (%u) is less than 'kNumHeadPages' (4)

What it means

PersistentMemoryMalloc's constructor partitions the log into pages and marks some fraction as mutable. It requires that at least kNumHeadPages (4) pages remain immutable so that when the in-memory log fills up, log data can always be flushed/dumped to disk. If buffer_size_ minus num_mutable_pages_ is below 4, the constructor throws std::invalid_argument.

Solutions

  1. Increase the log size so that at least 4 pages remain immutable: log_size >= (num_mutable_pages + 4) * kPageSize
  2. Lower the mutable fraction to at most 1.0 - (4 * kPageSize) / log_size, as printed by the accompanying log_info message
  3. If the allocator truly needs no disk dump (e.g. compaction scratch), construct it with the no-backing-storage option to skip the check
  4. Double-check kPageSize and requested log size units (bytes vs MiB) to avoid unintentionally tiny logs

Example fix

// before
experimental::LogSettings settings;
settings.epoch = &epoch;
settings.fraction_mutable = 0.9;          // leaves < 4 immutable pages on a small log
f2 = std::make_unique<FasterKv>(...);
// after
size_t log_size = 1ULL << 30;             // or raise log size
double max_fraction = 1.0 - (4.0 * kPageSize) / log_size;
settings.fraction_mutable = std::min(0.9, max_fraction); // guarantee >= 4 immutable pages
Defensive patterns

Strategy: validation

Validate before calling

size_t kPageSize = static_cast<size_t>(Devices::FileSystem::page_size()); // or FASTER kPageSize
size_t min_log_size = 4 * kPageSize; // plus whatever mutable pages you need
if (log_size <= min_log_size || mutable_fraction > 1.0 - (4.0 * kPageSize) / (double)log_size) {
  throw std::invalid_argument("log too small for kNumHeadPages immutable pages");
}

Type guard

bool IsValidLogConfig(size_t log_size, double mutable_fraction, size_t page_size) {
  if (log_size < 4 * page_size) return false;
  return mutable_fraction <= 1.0 - (4.0 * static_cast<double>(page_size)) / static_cast<double>(log_size);
}

Try / catch

try {
  store = std::make_unique<FasterKv>(...);
} catch (const std::invalid_argument& e) {
  // message mentions 'kNumHeadPages immutable pages'
  std::cerr << "Log configuration invalid: " << e.what() << "\n";
  // fall back to a larger log size or lower mutable fraction and retry
}

Prevention

When it happens

Trigger: Constructing PersistentMemoryMalloc (or a FASTER store with a custom log size and mutable_fraction/LogMutableFraction setting) where the log is so small or the mutable fraction so high that fewer than 4 pages of headroom remain: e.g. log_size slightly above 4*page_size with mutable_fraction close to 1.0, or a tiny log (e.g. a few MiB) where rounding to pages yields fewer than 4 non-mutable pages. Skipped entirely when has_no_backing_storage_ is set (compaction).

Common situations: Configuring a small in-memory log for testing with default or near-1.0 mutable fraction; miscalculating log size after changing kPageSize or memory-market settings; compaction-like usage forgetting the no-backing-storage flag is what normally suppresses this check.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of microsoft/FASTER@321d872eab (2026-09-15). Data as JSON: /api/errors/8c5c4034f6ec54f6. Report an issue: GitHub.

Appendix: source

Thrown at cc/src/core/persistent_memory_malloc.h:605

      throw std::invalid_argument{ "Must have at least 2 non-head pages" };
    }
    // The latest N pages should be mutable.
    // If mutable fraction is 0, then allocate minimum size possible (i.e. 2 mutable pages)
    num_mutable_pages_ = (log_mutable_fraction > 0) ? static_cast<uint32_t>(log_mutable_fraction * buffer_size_) : 2;
    log_debug("Num mutable_pages = %u", num_mutable_pages_);

    if(num_mutable_pages_ <= 1) {
      // Need at least two mutable pages: one to write to, and one to open up when the previous
      // mutable page is full.
      throw std::invalid_argument{ "Must have at least 2 mutable pages" };
    }

    // Make sure we have at least 'kNumHeadPages' immutable pages.
    // Otherwise, we will not be able to dump log to disk when our in-memory log is full.
    // If the user is certain that we will never need to dump anything to disk
    // (this is the case in compaction), skip this check.
    if(!has_no_backing_storage_ && buffer_size_ - num_mutable_pages_ < kNumHeadPages) {
      log_error("Number of non-mutable pages (%u) is less than 'kNumHeadPages' (4)", buffer_size_ - num_mutable_pages_, kNumHeadPages);
      log_info("For given log size (%.3lf MiB) set mutable fraction to *no more* than %.3lf.",
        static_cast<double>(log_size) / (1 << 20),
        1.0 - static_cast<double>((kNumHeadPages * kPageSize))/static_cast<double>(log_size));

      throw std::invalid_argument{ "Must have at least 'kNumHeadPages' immutable pages" };
    }

    page_status_ = new FullPageStatus[buffer_size_];

    pages_ = new uint8_t* [buffer_size_];
    for(uint32_t idx = 0; idx < buffer_size_; ++idx) {
      if (pre_allocate_log_) {
        pages_[idx] = reinterpret_cast<uint8_t*>(aligned_alloc(sector_size, kPageSize));
        std::memset(pages_[idx], 0, kPageSize);
        // Mark the page as accessible.
        page_status_[idx].status.store(FlushStatus::Flushed, CloseStatus::Open);
      } else {
        pages_[idx] = nullptr;

View on GitHub (pinned to 321d872eab)