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
- Check handle != nint.Zero before calling CprSnapshotByPtr.
- Ensure the tree's Dispose has not run; gate flush/snapshot behind the same lifecycle that owns the handle.
- If the handle is read from a shared stub, re-read it under the stub's lock and skip the snapshot if zero.
- 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
- Read the handle under the same lock that guards tree disposal.
- Zero out stored handles on disposal and check before use.
- Skip the snapshot rather than throw when the tree is being torn down.
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
- Snapshot path is required.
- Failed to take CPR snapshot of BfTree.
- recoveryPath is required.
- filePath is required for disk-backed trees.
- Failed to create BfTree instance.
AI-assisted analysis of microsoft/garnet@951b0fc683 (2026-08-13).
Data as JSON: /api/errors/fc158481a5aed02a.
Report an issue: GitHub.