microsoft/garnet · error · ArgumentException

Native handle is null.

Error message

Native handle is null.

What it means

Thrown by the static CprSnapshotByPtr when the supplied native handle is IntPtr.Zero. This is a pure caller-contract violation: the method exists precisely for callers (e.g. RangeIndex's OnFlush path) that hold a raw tree pointer, and a null pointer cannot be snapshotted. It is an ArgumentException, not an InvalidOperationException, because the argument itself is the defect.

Source

Thrown at libs/native/bftree-garnet/BfTreeService.cs:528

        /// Take a CPR snapshot of a tree given only its native handle (no managed wrapper).
        /// Used by RangeIndex's <c>OnFlush</c> path which has direct access to the stub's
        /// TreeHandle but not the managed <see cref="BfTreeService"/> instance.
        ///
        /// <para><b>Caller contract:</b> this method does NOT self-serialize. bftree's internal
        /// <c>snapshot_in_progress</c> flag makes a <c>cpr_snapshot</c> that races another
        /// snapshot on the same tree <b>silently no-op</b> (no file written) while this method
        /// still returns success. Callers MUST hold external per-tree serialization (e.g.
        /// RangeIndex's per-tree snapshot claim) around this call so concurrent snapshots of the
        /// same handle cannot race.</para>
        /// </summary>
        /// <param name="handle">Native BfTree pointer.</param>
        /// <param name="snapshotPath">Destination path for the snapshot file. The snapshot
        /// destination is supplied at call time; the caller supplies the path it
        /// configured for this tree.</param>
        public static void CprSnapshotByPtr(nint handle, string snapshotPath)
        {
            if (handle == nint.Zero)
                throw new ArgumentException("Native handle is null.", nameof(handle));
            if (string.IsNullOrEmpty(snapshotPath))
                throw new ArgumentException("Snapshot path is required.", nameof(snapshotPath));

            var snapBytes = Encoding.UTF8.GetBytes(snapshotPath);
            int result;
            fixed (byte* sp = snapBytes)
            {
                result = NativeBfTreeMethods.bftree_cpr_snapshot(handle, sp, snapBytes.Length);
            }
            if (result != 0)
                throw new InvalidOperationException("Failed to take CPR snapshot of BfTree.");
        }

        /// <summary>
        /// Recover a BfTree from a CPR snapshot file. Unified API for disk-backed and
        /// memory-backed (cache_only) trees — the storage backend is recorded in the
        /// snapshot and inferred by the native library.
        /// </summary>

View on GitHub (pinned to 951b0fc683)

Solutions

  1. Check handle != nint.Zero before calling CprSnapshotByPtr.
  2. Ensure the tree's Dispose has not run; gate flush/snapshot behind the same lifecycle that owns the handle.
  3. If the handle is read from a shared stub, re-read it under the stub's lock and skip the snapshot if zero.
  4. Log and skip rather than throw when the tree is known to be going away.

Example fix

// before
BfTreeService.CprSnapshotByPtr(stub.TreeHandle, snapPath);

// after
if (stub.TreeHandle != nint.Zero)
    BfTreeService.CprSnapshotByPtr(stub.TreeHandle, snapPath);
else
    logger.LogWarning("Skipping snapshot: tree handle is null");
Defensive patterns

Strategy: validation

Validate before calling

if (handle == nint.Zero) throw new ArgumentException("Handle is null.", nameof(handle));
// or skip: if (handle == nint.Zero) return;

Type guard

static bool IsValidTreeHandle(nint handle) => handle != nint.Zero;

Try / catch

try { BfTreeService.CprSnapshotByPtr(handle, path); }
catch (ArgumentException ex) when (ex.ParamName == nameof(handle)) { /* handle stale, skip snapshot */ }

Prevention

When it happens

Trigger: Calling CprSnapshotByPtr(nint.Zero, path), or passing a handle whose tree has already been dropped (bftree_drop set the pointer to zero on the managed side). Passing a disposed stub's TreeHandle that was zeroed out.

Common situations: A RangeIndex flush path racing with tree disposal, or a recovery/migration routine that snapshots a handle obtained from a structure that has since been torn down.

Related errors


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