USAGE / STORAGE LIFECYCLE

Keep storage moving in the background

The standard maintainer schedules merges after segment movement and periodically cleans inactive read caches. Its controls and events let an application adapt that policy to its workload.

Run the standard maintainer

Create one maintainer alongside the tree. This complete program writes a record, requests a merge of current in-memory data, and waits for tracked background work:

using ZoneTree;

using var tree = new ZoneTreeFactory<int, string>()
    .SetDataDirectory("data/maintenance-example")
    .OpenOrCreate();
using var maintainer = tree.CreateMaintainer();

tree.Upsert(42, "Ada");

maintainer.EvictToDisk();
await maintainer.WaitForBackgroundThreadsAsync();

Console.WriteLine($"Read-only segments: {tree.Maintenance.ReadOnlySegmentsCount}");

For normal service operation, keep the tree and maintainer alive and let the event-driven policy schedule merges. Explicit eviction and waiting are useful at controlled maintenance points; they are not needed after every write.

Optional coordinator, application-owned policy

ZoneTree can operate without CreateMaintainer(). Without it, you own merge scheduling, retries, cache cleanup, and background-work coordination through tree.Maintenance.

Maintainer and maintenance APIs

Surface Responsibility
tree.CreateMaintainer() Standard merge policy, merge-thread tracking, periodic cache cleanup, and disposal coordination. Returns a caller-owned IMaintainer.
tree.Maintenance Segment state, counters, lifecycle events, and lower-level operations for custom policy.

The maintainer schedules normal merges when mutable segments move forward. Bottom-segment compaction remains an explicit operation. Read LSM-tree architecture for the distinction between mutable, read-only, disk, and bottom layers.

Tune merge triggers

After the mutable segment moves forward, the default maintainer starts a normal merge when either limit is exceeded:

Setting Default Trigger
ThresholdForMergeOperationStart 0 Read-only record count exceeds the threshold.
MaximumReadOnlySegmentCount 64 Read-only segment count exceeds the limit.

With the default record threshold, any non-empty read-only layer can trigger a merge after movement. To collect larger batches or bound the number of queued segments:

maintainer.ThresholdForMergeOperationStart = 500_000;
maintainer.MaximumReadOnlySegmentCount = 32;

Higher thresholds can reduce the frequency of small merges, but allow more records to remain in memory. Choose them alongside record size, write rate, and available memory; a record count is not a byte budget.

Request a merge and wait for it

EvictToDisk() moves the active mutable segment forward and starts a normal merge:

maintainer.EvictToDisk();
maintainer.WaitForBackgroundThreads();

StartMerge() requests a normal merge without explicitly moving the current mutable segment. WaitForBackgroundThreads() waits for the merge threads tracked by this maintainer; it does not stop application writers or create an application-wide checkpoint by itself.

For asynchronous code, use await maintainer.WaitForBackgroundThreadsAsync().

Merge completion and durability are different decisions

Eviction reorganizes storage. It does not replace choosing WAL and failure-recovery guarantees. To establish a controlled maintenance boundary, coordinate application writers as well as background threads. See WAL modes and live backups for those separate workflows.

Compact bottom segments

Run a bottom merge when your policy wants to compact persistent bottom-layer segments:

maintainer.StartBottomSegmentsMerge();
maintainer.WaitForBackgroundThreads();

To request a selected range in the current bottom-segment order:

maintainer.StartBottomSegmentsMerge(fromIndex: 0, toIndex: 4);
maintainer.WaitForBackgroundThreads();

The default bounds are 0 and int.MaxValue, asking the engine to merge as much of the current bottom layer as possible. The indexes refer to segment order, not to record keys. Schedule this work around the CPU, I/O, and latency budget of your service.

Control inactive-cache cleanup

The standard maintainer starts periodic cleanup automatically:

Setting Default Effect
EnableJobForCleaningInactiveCaches true Enables the periodic cleanup job.
BlockCacheLifeTime 1 minute Controls how long inactive decompressed disk blocks remain eligible for retention.
InactiveBlockCacheCleanupInterval 30 seconds Sets the timer period when the cleanup job starts. Restart the job to apply a changed interval.
maintainer.EnableJobForCleaningInactiveCaches = false;
maintainer.BlockCacheLifeTime = TimeSpan.FromMinutes(2);
maintainer.InactiveBlockCacheCleanupInterval = TimeSpan.FromSeconds(15);
maintainer.EnableJobForCleaningInactiveCaches = true;

The cleanup interval is captured when the periodic timer is created; assigning the property alone does not reconfigure an already running timer. BlockCacheLifeTime is read on each cleanup pass.

Longer retention can help repeated reads and also retain more memory. Cleanup releases inactive block buffers and expired circular key/value cache entries. The .NET runtime may retain released memory for reuse rather than immediately returning it to the operating system.

See memory usage and read-path caching when interpreting process memory or changing the cache policy.

Observe progress and failures

Subscribe to merge completion to record the outcome:

tree.Maintenance.OnMergeOperationEnded += (_, result) =>
{
    Console.WriteLine($"Merge ended: {result}");
};
Result Interpretation
SUCCESS The merge completed.
NOTHING_TO_MERGE There were no eligible read-only segments.
ANOTHER_MERGE_IS_RUNNING A merge of the same kind was already active.
RETRY_READONLY_SEGMENTS_ARE_NOT_READY Read-only segments were still preparing.
CANCELLED_BY_USER A cancellation request was observed.
FAILURE The merge failed; inspect the configured logger.

The standard maintainer retries RETRY_READONLY_SEGMENTS_ARE_NOT_READY. After ANOTHER_MERGE_IS_RUNNING, it schedules follow-up work after the active normal merge finishes.

Useful counters on tree.Maintenance include:

Counter What it measures
MutableSegmentRecordCount Records in the active mutable segment.
ReadOnlySegmentsCount Queued read-only in-memory segments.
ReadOnlySegmentsRecordCount Records across those read-only segments.
InMemoryRecordCount Mutable and read-only records combined.
TotalRecordCount Physical records across memory and disk layers.
IsMerging / IsBottomSegmentsMerging Whether each merge kind is active.

Physical records can include multiple versions and deletion markers. Use Count() or CountFullScan() for live-record counts, with the cost and visibility requirements of your workload in mind.

Maintainer shutdown and disposal

Stop accepting application work and finish scans before closing storage. Short-lived iterators let old segments and files be reclaimed; long-lived iterators can pin them after a merge.

Maintainer disposal waits for tracked merge threads, requests cancellation of periodic cleanup, and detaches event handlers. The wait methods join tracked merge threads; they do not join the cache-cleanup task or arbitrary application work. For a cancellation-first shutdown:

maintainer.TryCancelBackgroundThreads();
maintainer.WaitForBackgroundThreads();

Cancellation is a request. The threads still need to observe it and finish. After the wait, dispose the maintainer and then the tree. The using declaration order in the first example handles that disposal sequence.

Build a custom policy when needed

Use tree.Maintenance when integrating with your own scheduler or storage orchestrator. A direct merge returns a thread, or null if a merge of the same kind is already active:

tree.Maintenance.MoveMutableSegmentForward();
var mergeThread = tree.Maintenance.StartMergeOperation();
mergeThread?.Join();

A null return is not proof that maintenance is finished. Observe the existing work through the appropriate state and events. Directly started threads are outside the standard maintainer's thread tracking, so your custom policy must coordinate them.

Lower-level operations
Operation Purpose
MoveMutableSegmentForward() Freeze the current mutable segment into the read-only layer.
StartMergeOperation() Start a normal merge thread.
StartBottomSegmentsMergeOperation(fromIndex, toIndex) Start a bottom-layer merge thread.
TryCancelMergeOperation() Request cancellation of the active normal merge.
TryCancelBottomSegmentsMergeOperation() Request cancellation of the active bottom merge.
SaveMetaData() Refresh the JSON metadata file and clear pending metadata records.
ReleaseReadBuffers(ticks) Release inactive decompressed disk blocks.
ReleaseCircularKeyCacheRecords() / ReleaseCircularValueCacheRecords() Release expired circular-cache entries.
Lifecycle events
  • OnMutableSegmentMovedForward: observe mutable-segment movement.
  • OnMergeOperationStarted / OnMergeOperationEnded: observe normal merges.
  • OnBottomSegmentsMergeOperationStarted / OnBottomSegmentsMergeOperationEnded: observe bottom merges.
  • OnDiskSegmentCreated / OnDiskSegmentActivated: observe disk-segment publication.
  • OnCanNotDropReadOnlySegment, OnCanNotDropDiskSegment, and OnCanNotDropDiskSegmentCreator: investigate failed cleanup of obsolete files or unfinished merge output.

For failed-drop events, log the exception and investigate the file-system or storage-provider error.