microsoft/garnet · error · TsavoriteException

Can compact only until Log.SafeReadOnlyAddress

Error message

Can compact only until Log.SafeReadOnlyAddress

What it means

CompactLookup (TsavoriteCompaction.cs:32) compacts the immutable portion of the log, so it requires untilAddress <= hlogBase.SafeReadOnlyAddress (the boundary between the flushed read-only region and the mutable region). Passing an address beyond SafeReadOnlyAddress would try to relocate records that may still be mutating, so it throws 'Can compact only until Log.SafeReadOnlyAddress' at line 36. SafeReadOnlyAddress advances only after pages are marked read-only and flushed.

Source

Thrown at libs/storage/Tsavorite/cs/src/core/Compaction/TsavoriteCompaction.cs:36

        /// <param name="untilAddress">Compact log until this address</param>
        /// <param name="compactionType">Compaction type (whether we lookup records or scan log for liveness checking)</param>
        /// <returns>Address until which compaction was done</returns>
        internal long Compact<TInput, TOutput, TContext, TCompactionFunctions>(TCompactionFunctions cf, long untilAddress, CompactionType compactionType)
            where TCompactionFunctions : ICompactionFunctions
        {
            return compactionType switch
            {
                CompactionType.Scan => CompactScan<TInput, TOutput, TContext, TCompactionFunctions>(cf, untilAddress),
                CompactionType.Lookup => CompactLookup<TInput, TOutput, TContext, TCompactionFunctions>(cf, untilAddress),
                _ => throw new TsavoriteException("Invalid compaction type"),
            };
        }

        private long CompactLookup<TInput, TOutput, TContext, TCompactionFunctions>(TCompactionFunctions cf, long untilAddress)
            where TCompactionFunctions : ICompactionFunctions
        {
            if (untilAddress > hlogBase.SafeReadOnlyAddress)
                throw new TsavoriteException("Can compact only until Log.SafeReadOnlyAddress");

            using var storeSession = NewSession<ITsavoriteScanIterator, TInput, TOutput, TContext, NoOpSessionFunctions<TInput, TOutput, TContext>>(new());
            var storebContext = storeSession.BasicContext;

            using (var iter1 = Log.Scan(Log.BeginAddress, untilAddress))
            {
                long numPending = 0;
                while (iter1.GetNext())
                {
                    var key = iter1.Key;

                    if (!iter1.Info.Tombstone && !cf.IsDeleted(in iter1))
                    {
                        var iter1AsLogSource = iter1 as ISourceLogRecord;   // Can't use 'ref' on a 'using' variable
                        var status = storebContext.CompactionCopyToTail(in iter1AsLogSource, iter1.CurrentAddress, iter1.NextAddress);
                        if (status.IsPending && ++numPending > 256)
                        {
                            _ = storebContext.CompletePending(wait: true);

View on GitHub (pinned to 951b0fc683)

Solutions

  1. Clamp untilAddress to Log.SafeReadOnlyAddress before calling Compact.
  2. Ensure the read-only region has advanced: trigger a flush/checkpoint or wait until SafeReadOnlyAddress >= your target before compacting.
  3. Pass Log.SafeReadOnlyAddress (or a value below it) as the compaction boundary.

Example fix

// before
var addr = store.Log.TailAddress;                  // likely > SafeReadOnlyAddress
store.Log.Compact<TInput, TOutput, TContext>(addr, CompactionType.Lookup); // throws

// after
var addr = Math.Min(requestedAddress, store.Log.SafeReadOnlyAddress);
store.Log.Compact<TInput, TOutput, TContext>(addr, CompactionType.Lookup);
Defensive patterns

Strategy: validation

Validate before calling

// Clamp the compaction address to the safe read-only boundary before Lookup compaction.
long addr = Math.Min(requestedUntilAddress, store.Log.SafeReadOnlyAddress);
if (addr <= store.Log.BeginAddress) return; // nothing to compact
store.Log.Compact<TInput, TOutput, TContext>(addr, CompactionType.Lookup);

Type guard

static bool WithinSafeRange(long untilAddress, long safeReadOnlyAddress, long beginAddress)
    => untilAddress > beginAddress && untilAddress <= safeReadOnlyAddress;
// Guard the Compact call with WithinSafeRange(addr, store.Log.SafeReadOnlyAddress, store.Log.BeginAddress).

Prevention

When it happens

Trigger: Calling Compact with CompactionType.Lookup and an untilAddress greater than Log.SafeReadOnlyAddress, e.g. compacting up to Log.TailAddress before a flush has advanced the read-only boundary.

Common situations: Using Log.TailAddress or Log.ReadOnlyAddress as the compaction target instead of SafeReadOnlyAddress; compacting immediately after heavy writes with no checkpoint/flush; or racing a compaction against ongoing writes that keep the safe boundary below the requested address.

Related errors


AI-assisted analysis of microsoft/garnet@951b0fc683 (2026-08-13). Data as JSON: /api/errors/ce7407e55bc236d3. Report an issue: GitHub.