{"record":{"id":"ce7407e55bc236d3","repo":"microsoft/garnet","slug":"can-compact-only-until-log-safereadonlyaddress","errorCode":null,"errorMessage":"Can compact only until Log.SafeReadOnlyAddress","messagePattern":"Can compact only until Log\\.SafeReadOnlyAddress","errorType":"exception","errorClass":"TsavoriteException","httpStatus":null,"severity":"error","filePath":"libs/storage/Tsavorite/cs/src/core/Compaction/TsavoriteCompaction.cs","lineNumber":36,"sourceCode":"        /// <param name=\"untilAddress\">Compact log until this address</param>\n        /// <param name=\"compactionType\">Compaction type (whether we lookup records or scan log for liveness checking)</param>\n        /// <returns>Address until which compaction was done</returns>\n        internal long Compact<TInput, TOutput, TContext, TCompactionFunctions>(TCompactionFunctions cf, long untilAddress, CompactionType compactionType)\n            where TCompactionFunctions : ICompactionFunctions\n        {\n            return compactionType switch\n            {\n                CompactionType.Scan => CompactScan<TInput, TOutput, TContext, TCompactionFunctions>(cf, untilAddress),\n                CompactionType.Lookup => CompactLookup<TInput, TOutput, TContext, TCompactionFunctions>(cf, untilAddress),\n                _ => throw new TsavoriteException(\"Invalid compaction type\"),\n            };\n        }\n\n        private long CompactLookup<TInput, TOutput, TContext, TCompactionFunctions>(TCompactionFunctions cf, long untilAddress)\n            where TCompactionFunctions : ICompactionFunctions\n        {\n            if (untilAddress > hlogBase.SafeReadOnlyAddress)\n                throw new TsavoriteException(\"Can compact only until Log.SafeReadOnlyAddress\");\n\n            using var storeSession = NewSession<ITsavoriteScanIterator, TInput, TOutput, TContext, NoOpSessionFunctions<TInput, TOutput, TContext>>(new());\n            var storebContext = storeSession.BasicContext;\n\n            using (var iter1 = Log.Scan(Log.BeginAddress, untilAddress))\n            {\n                long numPending = 0;\n                while (iter1.GetNext())\n                {\n                    var key = iter1.Key;\n\n                    if (!iter1.Info.Tombstone && !cf.IsDeleted(in iter1))\n                    {\n                        var iter1AsLogSource = iter1 as ISourceLogRecord;   // Can't use 'ref' on a 'using' variable\n                        var status = storebContext.CompactionCopyToTail(in iter1AsLogSource, iter1.CurrentAddress, iter1.NextAddress);\n                        if (status.IsPending && ++numPending > 256)\n                        {\n                            _ = storebContext.CompletePending(wait: true);","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/microsoft/garnet/blob/951b0fc6838721f89d102c2bbe1b914e8d39d700/libs/storage/Tsavorite/cs/src/core/Compaction/TsavoriteCompaction.cs#L18-L54","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Clamp untilAddress to Log.SafeReadOnlyAddress before calling Compact.","Ensure the read-only region has advanced: trigger a flush/checkpoint or wait until SafeReadOnlyAddress >= your target before compacting.","Pass Log.SafeReadOnlyAddress (or a value below it) as the compaction boundary."],"exampleFix":"// before\nvar addr = store.Log.TailAddress;                  // likely > SafeReadOnlyAddress\nstore.Log.Compact<TInput, TOutput, TContext>(addr, CompactionType.Lookup); // throws\n\n// after\nvar addr = Math.Min(requestedAddress, store.Log.SafeReadOnlyAddress);\nstore.Log.Compact<TInput, TOutput, TContext>(addr, CompactionType.Lookup);","handlingStrategy":"validation","validationCode":"// Clamp the compaction address to the safe read-only boundary before Lookup compaction.\nlong addr = Math.Min(requestedUntilAddress, store.Log.SafeReadOnlyAddress);\nif (addr <= store.Log.BeginAddress) return; // nothing to compact\nstore.Log.Compact<TInput, TOutput, TContext>(addr, CompactionType.Lookup);","typeGuard":"static bool WithinSafeRange(long untilAddress, long safeReadOnlyAddress, long beginAddress)\n    => untilAddress > beginAddress && untilAddress <= safeReadOnlyAddress;\n// Guard the Compact call with WithinSafeRange(addr, store.Log.SafeReadOnlyAddress, store.Log.BeginAddress).","tryCatchPattern":null,"preventionTips":["Use Log.SafeReadOnlyAddress as the compaction ceiling, never TailAddress/ReadOnlyAddress.","Flush/checkpoint first so SafeReadOnlyAddress advances to your intended point.","Compact below or at SafeReadOnlyAddress; re-check the live value immediately before the call."],"tags":["tsavorite","compaction","safe-read-only-address","lookup","csharp"],"backgroundTag":null,"analyzedSha":"951b0fc6838721f89d102c2bbe1b914e8d39d700","analyzedAt":"2026-08-13T19:01:32.939Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}