Backups

The backups plugin copies committed transactions to local folders and S3-compatible storage. It can restore a missing or older database from a backup target.

Enable backups in the plugins option and provide its settings when you open the database. An absent plugin selection enables no plugins. Release packages include backups as a plugin library and in the static archive. The core library does not contain it.

The history and backups plugins cannot share a database. History retention changes the transaction history that backups must preserve.

Settings

edn1.1 kB
{:backups {:targets           {:local   {:file {:path "backups/main"}}
                               :offsite {:s3               {:endpoint           "https://s3.eu-central-1.amazonaws.com"
                                                            :region             "eu-central-1"
                                                            :bucket             "team-backups"
                                                            :prefix             "main"
                                                            :accessKeyIdEnv     "BACKUP_KEY_ID"
                                                            :secretAccessKeyEnv "BACKUP_SECRET"}
                                         :compression      {:algorithm "zstd"
                                                            :level     3}
                                         :encryptionKeyEnv "BACKUP_ENCRYPTION_KEY"
                                         :maximumLag       {:transactions 1000
                                                            :duration     {:#dur "PT5M"}}}}
           :bypassUnreachable false}}
SettingValueEffect
targetsObjectEach member defines a named target.
bypassUnreachableBoolean, default falsePermit writes during a target outage. Lag limits and recovery checks still apply.

Each target requires exactly one storage member: file or s3. These members are optional:

MemberValueEffect
compressionObjectSelect algorithm: none or zstd. Zstandard accepts an optional integer level. Compression applies to transaction runs.
encryptionKeyEnvTextName an environment variable containing a 64-digit hexadecimal AES-256 key. Runs, payload chunks and payload manifests use AES-256-GCM encryption.
maximumLagObjectRefuse further data writes when the target exceeds any specified lag limit.
Lag limitValueMeasurement
transactionsPositive integerTransactions the target has not confirmed
logicalBytesPositive integerStored transaction-row bytes the target has not confirmed
binaryBytesPositive integerPayload bytes the target has not confirmed
durationPositive #durAge of the oldest unconfirmed transaction

The file member requires path. A relative path resolves against the working directory when the database opens.

S3 memberRequiredValue
endpointYesS3 service URL
regionYesSigning region
bucketYesBucket name
accessKeyIdEnvYesEnvironment variable containing the access key ID
secretAccessKeyEnvYesEnvironment variable containing the secret access key
prefixNoObject key prefix
sessionTokenEnvNoEnvironment variable containing the session token
addressingStyleNopath or virtual

On Linux and macOS, S3 requires libcurl 7.84.0 or newer with thread-safe initialization and asynchronous DNS. The plugin loads libcurl on the first S3 connection. Windows uses WinHTTP. If libcurl is unavailable, S3 targets report Backup_Operating_System. Local file targets remain usable.

Unknown members, invalid values and duplicate storage locations make the database open fail. The plugin reads secret values from the environment when needed. It does not store them in the database.

Stored options

Startup settings are authoritative on every open. The plugin records them as read-only definitions of the backup type:

DefinitionContents
backups/targets/<name>Settings for the named target
backups/settingsPlugin-wide settings

Different startup settings replace these definitions in one transaction. Definitions for removed targets are deleted. Identical settings produce no transaction.

Attempts to write, edit, move or delete these definitions fail with definition_read_only. To change the settings, close the database and open it with the new settings.

Format

Each target stores immutable runs and a mutable head file, manifest.json. A run contains one JSON line per transaction. Each line is exactly the transaction's merge patch. It includes the transaction ID, stardust/committedAt and stardust/transaction.

A SHA-256 chain links the transaction lines. The head identifies the runs, newest transaction and chain digest. The plugin merges small runs into larger runs to limit the number of stored files.

Open and recovery

On open, the plugin compares the local history with each readable target. It selects the newest history only if every other compared history is an exact prefix of it. Missing local transactions are restored with their original transaction IDs, commit times, entity IDs and field names. Older targets receive the transactions they lack.

Data writes remain held until recovery and target catch-up finish. This also applies with bypassUnreachable true. A source target reports restoring while the database restores from it. A source failure holds writes until recovery can continue. After a crash or database close, the next open continues after the last restored transaction. Reactors exclude restored transactions because the history already contains their live-mutation results.

These conditions make the open fail without changing the database:

ConditionStatusCode
Histories diverge after a common transactionconflictbackup_history_divergent
The newest target cannot supply history needed to compare another copyunavailablebackup_target_unreadable
The database is new or empty and every target is unreachableunavailablebackup_targets_unreachable
Recovery needs object payloads but objects is disabledunavailablebackup_objects_required

Error messages name the affected targets. Divergence errors use local for the database. Unreachable storage includes network or operating-system failures and a target held by another database. A new database cannot safely start a history while unreachable targets might contain an existing one.

A missing storage location or unavailable encryption key does not itself make the open fail. The target reports the failure in its status. The plugin retries targets it cannot compare during open. A later divergence reports Backup_History_Divergent in the target status.

After an empty open or a restore, an unexamined target might contain newer history. It reports comparisonPending, and data writes remain held until comparison and any further recovery finish. bypassUnreachable true does not bypass this check.

Write admission

Initial comparison and recovery hold data writes and the stored-options transaction. After recovery, a target that exceeds its maximumLag holds further data writes. A held data write returns conflict with code write_held. Retry the write later, or call sync first. Maintenance, restore transactions and plugin progress records remain permitted.

An unreachable target or Backup_Memory_Exhausted failure triggers retries with increasing delays, up to five minutes. bypassUnreachable true permits writes during retries except when recovery or comparison requires that target. Lag limits still apply. Each commit immediately increases the measured lag. A target within its limits permits the next write.

Runtime status

Call status to inspect backup progress without contacting a target.

FieldContents
enabledWhether backups are enabled
headNewest committed transaction
writesAdmittedWhether data writes are permitted
optionsStoredWhether stored definitions match startup settings
optionsCodeLast stored-options commit failure, or ""
targetsPer-target status entries
Target fieldContents
nameTarget name
phaseCurrent phase, listed below
cursorNewest confirmed transaction, or null
synchronizedWhether the target has confirmed the current history
restoringWhether the database is restoring history from this target
comparisonPendingWhether comparison is still required after an open
codeLast failure, or ""
lagCurrent transactions, logicalBytes and binaryBytes, when measured
PhaseMeaning
startingOpening the target
validatingReading and checking stored history
reconcilingRestoring or comparing transactions
synchronizingStoring new transactions
synchronizedAll committed transactions confirmed
retryingWaiting to retry after a failure
failedStopped until the database opens again
stoppedThe database is closing

When to call sync

Backups run automatically after commits. A successful write can return before a target stores the transaction. Call sync when an operation requires every target to confirm the current history, such as a recovery test.

Sync waits for the transaction current at the call, or the stored-options transaction if that commits later. It succeeds after the options are stored and every target confirms that transaction. Targets with maximumLag must also report current lag measurements. A failed target or refused stored-options commit makes sync report failed. Use maxWait to set the wait duration. Omitting it, or using zero, checks once without waiting.

Object payloads

The objects plugin identifies file payloads through stardust/object/digest. Backups store referenced payloads before the transaction run that names them:

LocationContents
binaries/chunks/Shared payload chunks
binaries/manifests/Payload manifests

A shared chunk is stored once. Targets retain stored payloads permanently. encryptionKeyEnv encrypts chunks and manifests. Payloads are not compressed.

Recovery copies missing payloads into the object store. Before each referencing transaction commits, recovery checks every chunk and the whole payload against their digests. These checks also apply to payloads already present locally. Each payload digest is published with its referencing transaction.

A missing or corrupt payload stops recovery before that transaction and reports Backup_Corrupted. Earlier restored transactions remain committed, and data writes remain held.

Recovery of payload references requires objects. If this requirement is discovered during open, it fails with backup_objects_required, as listed under open and recovery. A target examined later reports Backup_Objects_Required before restoring any transactions. The same status applies when a target must store payloads that the database cannot read. Each pending payload contributes its size to the binaryBytes lag.

Operations

OperationReturnsSummary
backups_statusdocumentbackups.status() returns an owned document describing backup progress.
backups_syncnonebackups.sync(options) waits for every target to confirm the current transaction history.

backups_status

odin99 B
backups_status :: proc(
	db: Database,
	allocator := context.allocator,
) -> (
	Document,
	Error,
)

Parameters

ParameterTypeOwnershipDefaultMeaning
dbDatabaseBorrowed for the callRequiredThe backups owner for this operation. Keep it alive during the call.
allocatormem.AllocatorValuecontext.allocatorAllocator for copied return values and diagnostics. The caller releases them with the same allocator.

Errors

StatusWhen
.Unavailablethe database does not enable the backups plugin
.Closedthe database is closed
.Out_Of_Memorynative memory is exhausted

Notes

See runtime status for the result fields. This call does not contact targets.

Examples

Describe the backup runtime

odin105 B
failed, result := stardust.backups_status(db)
defer stardust.close(&failed)
defer stardust.close(&result)
Result / 199 B
{:value {:code    "unavailable"
         :message "the plugin is not enabled"
         :path    nil
         :field   nil
         :entity  nil
         :offset  nil
         :status  "unavailable"}}

C exports: stardust_db_plugin_call, stardust_result_take_document, stardust_result_error, stardust_result_release

backups_sync

odin105 B
backups_sync :: proc(
	db: Database,
	options: Document = {},
	allocator := context.allocator,
) -> Error

Parameters

ParameterTypeOwnershipDefaultMeaning
dbDatabaseBorrowed for the callRequiredThe backups owner for this operation. Keep it alive during the call.
optionsDocumentBorrowed for the call{}The borrowed call options, or none. Optional maxWait is a nonnegative native duration, such as {maxWait {#dur PT5S}}. Without it, or with zero, the call checks once.
allocatormem.AllocatorValuecontext.allocatorAllocator for copied return values and diagnostics. The caller releases them with the same allocator.

Errors

StatusWhen
.Invalid_Argumentoptions is not an object, or an option is invalid, duplicated or unsupported
.Failedthe plugin's work failed; status describes the failure
.TimeoutmaxWait passed before every target confirmed
.Unavailablethe database does not enable the backups plugin
.Closedthe database is closed

Notes

Database close interrupts an active wait. See when to call sync.

Examples

Wait until every target stored the history

odin188 B
sync_options := stardust.parse(`{maxWait {#dur PT5S}}`, .DUST) or_return
defer stardust.close(&sync_options)
result := stardust.backups_sync(db, sync_options)
defer stardust.close(&result)
Result / 199 B
{:value {:code    "unavailable"
         :message "the plugin is not enabled"
         :path    nil
         :field   nil
         :entity  nil
         :offset  nil
         :status  "unavailable"}}

C exports: stardust_db_plugin_call, stardust_result_take_document, stardust_result_error, stardust_result_release