Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9deb02c
Tsavorite: add PostCopyToTail, OnTruncate triggers; OnFlush(addr); RM…
badrishc May 7, 2026
2c444ca
RangeIndex: refactor for compaction lifecycle (Guid keying, two roots…
badrishc May 7, 2026
daab095
Tests: trigger ext tests; update RecordLifecycleTests + RangeIndex di…
badrishc May 7, 2026
9894bfa
Tests: add Defect 1 reproduction (flush files immutable) and OnTrunca…
badrishc May 7, 2026
edc3b70
doc: update range-index-resp-api.md for compaction lifecycle integration
badrishc May 7, 2026
e40e322
RangeIndex: drop StorageBackend==Disk guard from DisposeTreeUnderLock…
badrishc May 8, 2026
e2dd298
RangeIndex: restore xmldoc + inline comments on ComputeLeafPageSize/R…
badrishc May 8, 2026
b5ee80f
RangeIndex: simplify pre-stage paths to direct File.Copy (drop .tmp +…
badrishc May 8, 2026
d77ef97
RangeIndex: fix race in PreStageAndRegisterPending — acquire per-key …
badrishc May 8, 2026
001309f
RangeIndex: address GPT-5.5 PR review (3 issues; FlagTransferred, OnF…
badrishc May 8, 2026
ff1f9d2
RangeIndex: DEL preserves per-flush snapshot files (LOG-tied lifetime)
badrishc May 8, 2026
0f0feeb
RangeIndex: remove .tmp dead-code distraction
badrishc May 8, 2026
be9789b
RangeIndex: consolidate stub flag accessors (RecordInfo style)
badrishc May 8, 2026
d32c8e4
RangeIndex: comment cleanup + Tsavorite format fix
badrishc May 8, 2026
1d7142b
ci: point setup-rust-toolchain at the bftree workspace
badrishc May 8, 2026
1387162
RangeIndex: restore per-key exclusive lock around OnFlush snapshot
badrishc May 8, 2026
7c62750
RangeIndex: address dev-vs-PR design audit (3 findings)
badrishc May 8, 2026
2d045ea
RangeIndex: migrate to bf-tree 0.5 CPR snapshot, fix OnFlush deadlocks
badrishc May 12, 2026
6c59dc2
RangeIndex: drop redundant X-lock from PreStageAndRegisterPending
badrishc May 13, 2026
6784595
Revert "RangeIndex: drop redundant X-lock from PreStageAndRegisterPen…
badrishc May 13, 2026
86c73f3
RangeIndex: propagate RecordType in PostCopyToTail (CTT bug fix)
badrishc May 13, 2026
e7e6c88
RangeIndex: dedicated Read_RangeIndex API to suppress CTT per-op
badrishc May 13, 2026
8e232cb
RangeIndex: address PR review feedback (cleanups + doc fixes)
badrishc May 13, 2026
85b5c59
RangeIndex: fail-fast init, atomic activation, strict recovery checks
badrishc May 13, 2026
9220576
Tests: migrate RecordTriggersExtTests off Allure (post-rebase fixup)
badrishc May 14, 2026
54a85ee
update bftree
badrishc May 14, 2026
b723b6e
ci: drop Rust toolchain setup step
badrishc May 14, 2026
d4c3699
GarnetServer: drop stray blank line after EnableRangeIndexPreview clu…
badrishc May 14, 2026
23331c5
RangeIndex: gate trigger Call* properties on IsEnabled (perf)
badrishc May 14, 2026
517a853
RangeIndex: construct manager only when enabled; drop IsEnabled flag
badrishc May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions libs/host/GarnetServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,30 @@ private GarnetDatabase CreateDatabase(int dbId, GarnetServerOptions serverOption
CustomCommandManager customCommandManager)
{
var removeOutdated = !serverOptions.EnableCluster;
var rangeIndexDataDir = Path.Combine(serverOptions.CheckpointBaseDirectory, GarnetServerOptions.GetCheckpointDirectoryName(dbId));
var rangeIndexManager = new RangeIndexManager(serverOptions.EnableRangeIndexPreview, rangeIndexDataDir,
removeOutdatedCheckpoints: removeOutdated, loggerFactory?.CreateLogger("RangeIndexManager"));

// Two-roots layout for RangeIndex files:
// riLogRoot — log-tied (working file + per-flush snapshots), co-located with hlog.
// Falls back through LogDir → CheckpointDir → cwd, mirroring Tsavorite's
// CheckpointBaseDirectory chain so RangeIndex works without storage tier.
// cprDir — checkpoint-tied (per-token snapshots live under <token>/rangeindex/),
// alongside Tsavorite's cpr-checkpoints/<token>/info.dat etc.
// Construct the manager only when the feature is enabled. When disabled, the
// store wrapper / triggers / functions hold a null reference, and Tsavorite's
// record-trigger gates (CallOnFlush etc.) return false → zero per-op overhead.
RangeIndexManager rangeIndexManager = null;
if (serverOptions.EnableRangeIndexPreview)
{
var logRootBase = serverOptions.LogDir
?? serverOptions.CheckpointDir
?? Directory.GetCurrentDirectory();
var riLogRoot = Path.Combine(logRootBase ?? string.Empty, "Store", "rangeindex");
var cprDir = Path.Combine(serverOptions.GetStoreCheckpointDirectory(dbId), "cpr-checkpoints");

rangeIndexManager = new RangeIndexManager(
riLogRoot: riLogRoot, cprDir: cprDir,
storeEpoch: storeEpoch,
logger: loggerFactory?.CreateLogger("RangeIndexManager"));
}
var store = CreateStore(dbId, clusterFactory, customCommandManager, storeEpoch, rangeIndexManager, out var stateMachineDriver, out var sizeTracker, out var kvSettings);
var aof = CreateAOF(dbId);

Expand Down
171 changes: 71 additions & 100 deletions libs/native/bftree-garnet/BfTreeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,15 @@ public readonly struct ScanRecord
/// <summary>
/// High-level managed wrapper for the native bftree-garnet library.
/// Provides safe C# access to BfTree lifecycle, point operations, scans,
/// and snapshot/recovery.
/// and CPR snapshot/recovery.
/// </summary>
public sealed unsafe class BfTreeService : IDisposable
{
private nint _tree;
private int _disposed;
private readonly StorageBackendType _storageBackend;
private readonly string _filePath;
private readonly string _snapshotFilePath;

/// <summary>
/// Gets the native tree pointer for storage in stubs and direct P/Invoke.
Expand All @@ -117,12 +118,20 @@ public sealed unsafe class BfTreeService : IDisposable
/// </summary>
public StorageBackendType StorageBackend => _storageBackend;

/// <summary>
/// Gets the CPR snapshot scratch file path configured at construction. Null if the
/// tree was created without snapshot support.
/// </summary>
public string SnapshotFilePath => _snapshotFilePath;

/// <summary>
/// Creates a new BfTree with the given configuration.
/// Pass 0 for any numeric parameter to use the bf-tree default.
/// </summary>
/// <param name="storageBackend">Disk (default, file-backed) or Memory (bounded in-memory).</param>
/// <param name="filePath">Data file path for disk-backed trees. Ignored for memory-only.</param>
/// <param name="snapshotFilePath">Scratch path for CPR snapshot output. Required if
/// <see cref="CprSnapshot"/> will be called later. Null disables snapshots (legacy behavior).</param>
/// <param name="cbSizeByte">Circular buffer size in bytes (hot-data cache for Disk; total capacity for Memory).</param>
/// <param name="cbMinRecordSize">Minimum record size.</param>
/// <param name="cbMaxRecordSize">Maximum record size.</param>
Expand All @@ -131,6 +140,7 @@ public sealed unsafe class BfTreeService : IDisposable
public BfTreeService(
StorageBackendType storageBackend = StorageBackendType.Disk,
string filePath = null,
string snapshotFilePath = null,
ulong cbSizeByte = 0,
uint cbMinRecordSize = 0,
uint cbMaxRecordSize = 0,
Expand All @@ -139,14 +149,18 @@ public BfTreeService(
{
_storageBackend = storageBackend;
_filePath = filePath;
_snapshotFilePath = snapshotFilePath;
if (storageBackend == StorageBackendType.Disk && string.IsNullOrEmpty(filePath))
throw new ArgumentException("filePath is required for disk-backed trees.", nameof(filePath));
byte[] pathBytes = filePath != null ? Encoding.UTF8.GetBytes(filePath) : null;
byte[] snapBytes = snapshotFilePath != null ? Encoding.UTF8.GetBytes(snapshotFilePath) : null;
fixed (byte* pp = pathBytes)
fixed (byte* sp = snapBytes)
{
_tree = NativeBfTreeMethods.bftree_create(
cbSizeByte, cbMinRecordSize, cbMaxRecordSize, cbMaxKeyLen, leafPageSize,
(byte)storageBackend, pp, pathBytes?.Length ?? 0);
(byte)storageBackend, pp, pathBytes?.Length ?? 0,
sp, snapBytes?.Length ?? 0);
}
if (_tree == 0)
throw new InvalidOperationException("Failed to create BfTree instance.");
Expand All @@ -156,13 +170,14 @@ public BfTreeService(
/// Creates a BfTreeService wrapping an existing native tree pointer (e.g. from snapshot restore).
/// Takes ownership of the pointer.
/// </summary>
internal BfTreeService(nint treePtr, StorageBackendType storageBackend, string filePath = null)
internal BfTreeService(nint treePtr, StorageBackendType storageBackend, string filePath = null, string snapshotFilePath = null)
{
if (treePtr == 0)
throw new ArgumentException("Tree pointer must not be null.", nameof(treePtr));
_tree = treePtr;
_storageBackend = storageBackend;
_filePath = filePath;
_snapshotFilePath = snapshotFilePath;
}

// ---------------------------------------------------------------
Expand Down Expand Up @@ -467,119 +482,75 @@ public List<ScanRecord> ScanAll(
}

/// <summary>
/// Snapshot the BfTree to a file.
/// For disk-backed trees: drains the circular buffer and writes the index
/// structure to the tree's own data file. <paramref name="snapshotPath"/> is ignored.
/// For memory-only trees: serializes the tree to the given path.
/// Currently throws <see cref="NotSupportedException"/> until bf-tree adds
/// cache_only snapshot support.
/// Take a CPR (Concurrent Prefix Recovery) snapshot of this tree. Synchronous;
/// non-blocking to concurrent insert/read/delete callers. Writes the snapshot to
/// the path configured at construction (<see cref="SnapshotFilePath"/>).
///
/// <para>To produce snapshots at multiple destination paths, the caller is expected
/// to <c>File.Move</c> / copy the configured snapshot file to the final destination
/// after each call.</para>
///
/// <para>Internal <c>snapshot_in_progress</c> AtomicBool serializes concurrent calls;
/// losers no-op silently. Callers that need both snapshots to succeed must serialize
/// externally.</para>
/// </summary>
/// <param name="snapshotPath">Target snapshot file path (used for memory-only trees; ignored for disk).</param>
public void Snapshot(string snapshotPath = null)
public void CprSnapshot()
{
ObjectDisposedException.ThrowIf(_disposed != 0, this);
if (_storageBackend == StorageBackendType.Disk)
{
int result = NativeBfTreeMethods.bftree_snapshot(_tree);
if (result != 0)
throw new InvalidOperationException("Failed to snapshot disk-backed BfTree.");
}
else
{
if (string.IsNullOrEmpty(snapshotPath))
throw new ArgumentException("snapshotPath is required for memory-only trees.", nameof(snapshotPath));
var pathBytes = Encoding.UTF8.GetBytes(snapshotPath);
int result;
fixed (byte* pp = pathBytes)
{
result = NativeBfTreeMethods.bftree_snapshot_memory(_tree, pp, pathBytes.Length);
}
if (result != 0)
throw new NotSupportedException(
"Snapshot is not yet supported for memory-only trees. Pending bf-tree cache_only snapshot support.");
}
if (string.IsNullOrEmpty(_snapshotFilePath))
throw new InvalidOperationException("CprSnapshot requires the tree to be constructed with a snapshotFilePath.");
int result = NativeBfTreeMethods.bftree_cpr_snapshot(_tree);
if (result != 0)
throw new InvalidOperationException("Failed to take CPR snapshot of BfTree.");
}

/// <summary>
/// Snapshot the BfTree to a specified target file for flush persistence.
/// For disk-backed trees: performs in-place snapshot, then copies the data file to <paramref name="targetPath"/>.
/// For memory-only trees: serializes directly to <paramref name="targetPath"/>.
/// 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.
/// Snapshot is written to the path configured at the tree's construction time.
/// </summary>
/// <param name="targetPath">Target file path for the snapshot.</param>
public void SnapshotToFile(string targetPath)
/// <param name="handle">Native BfTree pointer.</param>
public static void CprSnapshotByPtr(nint handle)
{
ObjectDisposedException.ThrowIf(_disposed != 0, this);
if (string.IsNullOrEmpty(targetPath))
throw new ArgumentException("targetPath is required.", nameof(targetPath));

if (_storageBackend == StorageBackendType.Disk)
{
// Snapshot in-place first (drains circular buffer to data file)
int result = NativeBfTreeMethods.bftree_snapshot(_tree);
if (result != 0)
throw new InvalidOperationException("Failed to snapshot disk-backed BfTree before file copy.");

// Copy the data file to the target path
System.IO.File.Copy(_filePath, targetPath, overwrite: true);
}
else
{
// For memory-only trees, serialize directly to the target path
var pathBytes = Encoding.UTF8.GetBytes(targetPath);
int result;
fixed (byte* pp = pathBytes)
{
result = NativeBfTreeMethods.bftree_snapshot_memory(_tree, pp, pathBytes.Length);
}
if (result != 0)
throw new NotSupportedException(
"Snapshot to file is not yet supported for memory-only trees. Pending bf-tree cache_only snapshot support.");
}
if (handle == nint.Zero)
throw new ArgumentException("Native handle is null.", nameof(handle));
int result = NativeBfTreeMethods.bftree_cpr_snapshot(handle);
if (result != 0)
throw new InvalidOperationException("Failed to take CPR snapshot of BfTree.");
}

/// <summary>
/// Recover a BfTree from its snapshot.
/// For disk-backed trees: reopens the existing data file and resumes.
/// For memory-only trees: loads the snapshot from disk into a new cache_only tree.
/// Currently throws <see cref="NotSupportedException"/> for memory-only until
/// bf-tree adds cache_only recovery support.
/// 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>
public static BfTreeService RecoverFromSnapshot(
string filePath,
StorageBackendType storageBackend = StorageBackendType.Disk,
ulong cbSizeByte = 0,
uint cbMinRecordSize = 0,
uint cbMaxRecordSize = 0,
uint cbMaxKeyLen = 0,
uint leafPageSize = 0)
/// <param name="recoveryPath">Source CPR snapshot file path.</param>
/// <param name="newSnapshotPath">Scratch path for the recovered tree's future cpr_snapshot
/// calls. Pass null to disable snapshots on the recovered tree (legacy behavior).</param>
/// <param name="storageBackend">Storage backend of the recovered tree (for managed tracking).</param>
public static BfTreeService RecoverFromCprSnapshot(
string recoveryPath,
string newSnapshotPath,
StorageBackendType storageBackend)
{
var pathBytes = Encoding.UTF8.GetBytes(filePath);
nint treePtr;
if (string.IsNullOrEmpty(recoveryPath))
throw new ArgumentException("recoveryPath is required.", nameof(recoveryPath));

if (storageBackend == StorageBackendType.Disk)
{
fixed (byte* pp = pathBytes)
{
treePtr = NativeBfTreeMethods.bftree_new_from_snapshot(
pp, pathBytes.Length,
cbSizeByte, cbMinRecordSize, cbMaxRecordSize, cbMaxKeyLen, leafPageSize);
}
if (treePtr == 0)
throw new InvalidOperationException($"Failed to recover disk-backed BfTree from '{filePath}'.");
}
else
var recoveryBytes = Encoding.UTF8.GetBytes(recoveryPath);
var newSnapBytes = newSnapshotPath != null ? Encoding.UTF8.GetBytes(newSnapshotPath) : null;
nint treePtr;
fixed (byte* rp = recoveryBytes)
fixed (byte* sp = newSnapBytes)
{
fixed (byte* pp = pathBytes)
{
treePtr = NativeBfTreeMethods.bftree_recover_memory(
pp, pathBytes.Length,
cbSizeByte, cbMinRecordSize, cbMaxRecordSize, cbMaxKeyLen, leafPageSize);
}
if (treePtr == 0)
throw new NotSupportedException(
"Recovery is not yet supported for memory-only trees. Pending bf-tree cache_only recovery support.");
treePtr = NativeBfTreeMethods.bftree_new_from_cpr_snapshot(
rp, recoveryBytes.Length,
sp, newSnapBytes?.Length ?? 0,
null, 0);
}
return new BfTreeService(treePtr, storageBackend, filePath);
if (treePtr == 0)
throw new InvalidOperationException($"Failed to recover BfTree from CPR snapshot '{recoveryPath}'.");
return new BfTreeService(treePtr, storageBackend, filePath: null, snapshotFilePath: newSnapshotPath);
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions libs/native/bftree-garnet/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion libs/native/bftree-garnet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ description = "C FFI wrapper over bf-tree for Garnet P/Invoke interop"
crate-type = ["cdylib"]

[dependencies]
bf-tree = "0.4"
bf-tree = "0.5"

[profile.release]
opt-level = 3
Expand Down
Loading
Loading