Back up a live tree. Restore a complete generation.
Keep reads and writes running while ZoneTree captures a backup. Choose a destination, restore and verify a generation, then add a schedule and retention policy.
Include memory records and hold immutable disk files while they are copied.
Every generation is complete. Its physical disk files can be shared with earlier generations.
Publish the catalog after its data is committed. Retention protects files still in use.
Live backup supports built-in plain and transactional trees, including Shared and AsyncShared WALs. It captures database records; restore does not need the original production WAL files.
Choose a destination
Local directory
Use LocalLiveBackupProvider to write to a filesystem directory. ZoneTree manages durable publication, shared files, restore leases, and retention.
Try the example belowObject storage
Use ObjectStorageLiveBackupProvider with an IBackupObjectStorage adapter. ZoneTree owns the format, integrity checks, conditional publication, and restore pins.
Configure object storageBoth destinations use the same coordinator API for manual requests, schedules, and status. Retention belongs to the provider: configure KeepLastGenerations in LocalLiveBackupOptions or ObjectStorageLiveBackupOptions. Choose a backup location with the storage separation your deployment needs; a sibling directory on the same device shares that device's lifetime.
Restore and verify a generation
These examples back up a running tree, restore the exact generation just created, and verify its records. The restore directory is unique so the example can be run again. Select the API that matches your source tree.
using ZoneTree;
using ZoneTree.Backup;
using var tree = new ZoneTreeFactory<int, string>()
.SetDataDirectory("data/catalog")
.OpenOrCreate();
using var maintainer = tree.CreateMaintainer();
tree.Upsert(42, "Ada Lovelace");
using var backup = tree.CreateLiveBackup(new LiveBackupOptions
{
Store = new LocalLiveBackupProvider("backups/catalog"),
InMemoryMode = LiveBackupInMemoryMode.Snapshot
});
await backup.CreateGenerationAsync();
using var restored = await new ZoneTreeFactory<int, string>()
.SetDataDirectory(Path.Combine("restore-checks", Guid.NewGuid().ToString("N")))
.RestoreFromLiveBackupGeneration("backups/catalog", backup.CurrentGenerationId);
if (!restored.TryGet(42, out var value) || value != "Ada Lovelace")
throw new InvalidOperationException("Restored records did not match."); using ZoneTree;
using ZoneTree.Backup;
using var tree = new ZoneTreeFactory<int, string>()
.SetDataDirectory("data/people")
.OpenOrCreateTransactional();
using var maintainer = tree.CreateMaintainer();
tree.Execute(tx =>
{
tx.Upsert(42, "Ada");
tx.Upsert(43, "Grace");
});
using var backup = tree.CreateLiveBackup("backups/people");
await backup.CreateGenerationAsync();
using var restored = await new ZoneTreeFactory<int, string>()
.SetDataDirectory(Path.Combine("restore-checks", Guid.NewGuid().ToString("N")))
.RestoreTransactionalFromLiveBackupGeneration("backups/people", backup.CurrentGenerationId);
using var read = restored.BeginReadOnlyTransaction();
if (!read.TryGet(42, out var ada) || ada != "Ada" ||
!read.TryGet(43, out var grace) || grace != "Grace")
throw new InvalidOperationException("Restored transaction did not match."); CreateGenerationAsync() creates one generation and does not require Start(). Factory restore methods return an already opened tree. Use RestoreFromLatestLiveBackup() or RestoreTransactionalFromLatestLiveBackup() to choose the latest published generation instead of supplying an ID. Each restore overload accepts a local directory, local options, or an ILiveBackupSource, including the object-storage provider.
Restore into an unused data directory and, when configured separately, an unused WAL directory. Keep WAL enabled in the target and preserve the source's data contracts: types, serializers, comparer, deletion behavior, and compatible storage options. Plain and transactional restore methods reject the wrong tree layout.
Transaction visibility
A transactional backup preserves the latest published commit snapshot at capture. Active and rolled-back transactions stay invisible after restore. A durable commit still waiting to publish is excluded; an ordinary commit already published is included even if the source has not synced it yet.
Commits pause while ZoneTree captures the segment set and transaction statuses, then continue during copying. Transactional backups always freeze the memory view and require IncludeInMemoryRecords = true, regardless of InMemoryMode.
For plain trees, the example explicitly selects Snapshot. The default Live mode can observe concurrent writes during its memory pass. A frozen storage view does not make several separate application writes atomic; use transactions for that requirement.
Schedule backups and retention
This service helper creates an initial generation, requests another every 15 minutes, and keeps the newest seven generations. runApplicationAsync represents the application's lifetime, so the tree and coordinator remain alive while scheduled work runs.
static async Task RunWithBackupsAsync(
IZoneTree<int, string> tree,
Func<Task> runApplicationAsync)
{
using var backup = tree.CreateLiveBackup(new LiveBackupOptions
{
Store = new LocalLiveBackupProvider(new LocalLiveBackupOptions
{
Directory = "backups/catalog",
KeepLastGenerations = 7
}),
InMemoryMode = LiveBackupInMemoryMode.Snapshot,
Schedule = LiveBackupSchedule.Every(TimeSpan.FromMinutes(15)),
BackupAfterMerge = false
});
await backup.CreateGenerationAsync();
backup.Start();
try
{
await runApplicationAsync();
}
finally
{
backup.Stop();
await backup.WaitForLiveBackupAsync();
}
} Use ITransactionalZoneTree<int, string> for a transactional service; the coordinator body is the same. For object storage, replace Store with an ObjectStorageLiveBackupProvider configured with KeepLastGenerations = 7. See the combined object-storage schedule and retention example.
Start() enables configured triggers. It does not create the first generation. The example creates that generation explicitly.
Seven generations is not seven days. More frequent schedules or additional merge triggers can shorten the history covered by that count.
Choose a trigger
| Schedule | Requests |
|---|---|
LiveBackupSchedule.None | Manual requests and enabled merge triggers only. |
LiveBackupSchedule.Every(TimeSpan.FromMinutes(15)) | One request every 15 minutes. |
LiveBackupSchedule.Daily(new TimeOnly(2, 0)) | Daily at 02:00 UTC. |
LiveBackupSchedule.Weekly(LiveBackupSchedule.On(DayOfWeek.Sunday, new TimeOnly(2, 0))) | Sunday at 02:00 UTC. |
BackupAfterMerge defaults to true. After Start(), successful normal merges can request generations in addition to the schedule; bottom-segment merges do not. The example disables merge triggers to use the interval alone. A timer remains useful when changes stay in memory without triggering a merge.
Keep shared files and active restores safe
Each retained generation is logically complete. You do not replay earlier backup generations to restore it, but several catalogs may reference the same physical files. Retention deletes a file only when no retained or protected generation needs it. Keep catalogs, referenced files, and record batches together when copying a backup elsewhere.
KeepLastGenerations = null retains all published generations. A positive limit keeps the newest requested count plus generations protected by active restores, so the count can temporarily exceed the limit.
Factory restore releases the lease automatically. A later successful backup can reclaim released generations and abandoned files.
Pins do not expire. Inspect and explicitly release an abandoned pin after its consumer stops, then run collection. Read the pin lifecycle.
If you consume ILiveBackupSource directly, use await using for the returned LiveBackupGeneration and finish reading its streams before releasing it. Factory restore manages this lifetime for you.
Poll coordinator status
ILiveBackup.GetStatus() returns a consistent in-memory snapshot for either destination and either tree type. Polling takes the coordinator's lock and performs no storage I/O or clock reads.
var status = backup.GetStatus();
var running = status.AttemptState == LiveBackupAttemptState.Running;
Console.WriteLine($"State: {status.AttemptState}");
Console.WriteLine($"Last success: {status.LastSuccessfulGenerationId}");
Console.WriteLine($"Completed at (Unix seconds): {status.LastCompletedAtUnixSeconds}");
if (status.AttemptState == LiveBackupAttemptState.Failed)
Console.Error.WriteLine(status.LastError); An accepted attempt becomes Running before capture or allocation. Busy, disposed, and already-cancelled requests leave its status unchanged. Success history survives a later failure or cancellation. A new coordinator starts with None, zero IDs and timestamps, and an empty error; it does not discover existing backups. Status remains readable after disposal.
Backup requests and completion
Use one coordinator and one normal writer per backup destination. A coordinator runs at most one generation at a time; it does not build a backlog.
| Request while busy | Result |
|---|---|
Manual CreateGenerationAsync() | Throws InvalidOperationException; an accepted failure propagates to the caller. |
| Schedule or normal-merge trigger | Skips the new request; failures of accepted attempts go to the tree's logger. |
Stop() disables automatic triggers and cancels scheduler waiting. It does not cancel a running generation or create a final one. Stop before calling WaitForLiveBackupAsync() while automatic operation is enabled; waiting drains active work and scheduler shutdown, but does not certify backup success.
Dispose() stops and waits too. Keep the coordinator inside the source tree's lifetime. If you need a final application checkpoint, finish application writers, stop triggers, await active work, then explicitly await CreateGenerationAsync(). CreateGeneration() and WaitForLiveBackup() are blocking equivalents; prefer their async forms in services.
Capture, integrity, and storage reference
Object storage backups
Implement a storage adapter, understand conditional publication, and manage persistent restore pins.
Read the object-storage guideRecovery and data contracts
Understand startup recovery, capture diagnostics, and preserve the types and settings needed to read stored data.
Read the recovery guide