Pre-1.0 hoardDB is pre-1.0. Expect breaking changes.
Backup and Restore
This page has not been rewritten for ring mode yet. Its examples use buckets with a type: and verbs that ring mode does not have, so read it as a description of the feature and not as commands that run. Getting started has the statements that run today, and Limitations lists what is not available yet.
hoarddb dump captures every database and bucket except blob payloads,
which no dump contains — see Blobs are not restored at
all and Store fields; both
dump and restore print a warning naming each blob bucket and each
blobStore field affected, on every run. Everything else is
written to a directory of plain BSON files — the same shape standard dump
tooling uses
— and hoarddb restore reads it back into a running server. No second
binary, no proprietary archive format: the .bson files this produces are
ordinary BSON, readable by any BSON-aware tool, on the
day you take the dump and after.
This page is the how-to drill, the guarantees the commands actually make, and
the caveats worth knowing before you rely on this for something that matters.
Every transcript below is copied from a real session: a server started with
no configuration, real inserts, a real dump, real destruction of the data,
and a real restore.
The Drill
Start a server the normal way (see Getting Started) and
connect the CLI. Create a database with a hash bucket and a fifo bucket so
the drill covers both an engine that dumps in native storage order and one
where order is the data:
[h]oardDB> create database shop
OK created database shop
Next: use shop
Then: create bucket <name> {type: hash | btree | fifo | lifo | heap | blob} (hash is the default)
create bucket Post {Photos lifoStore, Comments fifoStore}
a store field is <name> <type>Store — hashStore, btreeStore, fifoStore, lifoStore, heapStore, blobStore
[h]oardDB> use shop
Switched to database 'shop'
[h]oardDB [shop]> create bucket Customers { type: hash, indices: ["Email"] }
OK created bucket shop.Customers (type hash, whole)
Indexes: [Email]
[h]oardDB [shop]> db.Customers.insert({_id_: "c1", Name: "Ada Lovelace", Email: "ada@example.com"})
OK id=c1
[h]oardDB [shop]> db.Customers.insert({_id_: "c2", Name: "Grace Hopper", Email: "grace@example.com"})
OK id=c2
[h]oardDB [shop]> create bucket Orders { type: fifo }
OK created bucket shop.Orders (type fifo)
[h]oardDB [shop]> db.Orders.push({item: "widget", qty: 3})
OK id=668a4412-09e0-4f80-965e-91cbaa795914
[h]oardDB [shop]> db.Orders.push({item: "gadget", qty: 1})
OK id=1e6d644a-5d95-43f2-beef-eeaf13fc76df
[h]oardDB [shop]> db.Orders.push({item: "gizmo", qty: 7})
OK id=7f8c1e3b-80a1-4c18-a251-29be5e5811b0
dump — the standalone form
dump and restore are verbs of the same hoardDB binary, not a separate
tool — dump and restore are also REPL statements sharing the same
implementation (see below), because a database admin tool that splits
“connect and query” from “back up and restore” into two programs is asking
you to remember which binary does what:
hoarddb dump -address 127.0.0.1:17433 -user admin -password ****** -insecure --out ./dump
dumping shop/Customers...
shop/Customers: 2 documents, 138 bytes, sha256:e6e192eaed90562ab4a7dbd1421902a85ab332bd7964fff001157ba6c8204d31
dumping shop/Orders...
shop/Orders: 3 documents, 245 bytes, sha256:16fe3a1e5ca88e9030f70d4219762c07861175ef3e2c174cd881332d53aa5ad5
dump: 1 database(s), 2 bucket(s), 5 document(s), 383 byte(s)
-insecure is for a local development server, as in Getting Started —
against anything else, pass the server’s certificate fingerprint instead.
dump, restore and ingest never stop to ask whether to trust a server they
have not met: when it is not in ~/.hoarddb/known_servers they refuse, so a
backup script cannot hang on a question nobody answers. Connect once with
hoarddb and confirm the fingerprint to record the server, or pass
-insecure against a development server. This is a change for dump, restore
and ingest, which used to ask on a terminal. With no credentials to send they
exit 1.
dump exits 1 when the command line or the --out directory is unusable,
and 2 when the server refuses the scope, for example a database that does not
exist. 0 is a clean dump, 3 a dump where some buckets failed, 4 an
interrupted one.
The output directory:
dump/
manifest.json
shop/
Customers.bson
Customers.metadata.json
Orders.bson
Orders.dead.bson
Orders.metadata.json
manifest.json:
{
"format_version": 3,
"hoarddb_version": "devel",
"created_at": "2026-09-19T09:59:35Z",
"source_node": "node1",
"databases": ["shop"],
"buckets": 2,
"documents": 5,
"bytes": 383,
"blobs_included": false,
"credentials_included": false,
"globally_consistent": false,
"incomplete": false
}
shop/Customers.metadata.json — one of these per bucket, carrying the store
type, indexes, the replication factor the bucket declared (when it declared one)
and a digest of the .bson file next to it:
{
"format_version": 3,
"hoarddb_version": "devel",
"protocol_major": 1,
"created_at": "2026-09-19T09:59:35Z",
"source_node": "node1",
"database": "shop",
"bucket": "Customers",
"store_type": "hash",
"indexes": ["Email"],
"index_specs": [{"id": 0, "name": "Email_1", "fields": ["Email"]}],
"documents": 2,
"bytes": 138,
"digest": "sha256:e6e192eaed90562ab4a7dbd1421902a85ab332bd7964fff001157ba6c8204d31",
"blobs_included": false,
"replication_factor": 3,
"placement": "whole"
}
A hash bucket’s placement (whole or spread) is recorded too, and restore creates the bucket
with it, so a bucket with a unique index comes back whole and keeps the index. A dump written
before the field has none, and its buckets restore as the create’s default, whole in this build.
A restore into a bucket that already exists restores documents, not placement.
Every dump this build writes is format_version 3. restore accepts format 2
and format 3 dumps, and refuses any other version, naming the one it found and
the ones it restores; a newer dump needs a newer build.
A queue or heap bucket can also have a <bucket>.dead.bson file, holding the
bucket’s dead entries as records rather than live documents.
Its count, byte size and digest are in metadata.json as dead, dead_bytes
and dead_digest, and a restore writes those records back as dead entries,
not as live documents. A bucket with no dead entry has no such file and none of
those three members. A client older than format 3 refuses to dump a bucket with
dead entries rather than write them into <bucket>.bson, where a restore would
revive them; upgrade that client and dump again.
A dump also refuses a bucket that holds an index written by a newer version of hoardDB (after a rollback), naming the index: this version cannot describe it, so a dump would restore a unique index as a plain one or leave it out. Dump that bucket with the newer version instead.
shop/Customers.bson is a plain concatenation of BSON documents — no length
prefix, no wrapper — which is what lets any BSON tooling read it unmodified.
Note there is no users.json: see
Account recreation is not implemented
below.
The dump is plaintext — protecting it is your part
There is no password on these files and no --encrypt flag, deliberately: a
dump is the artifact you carry to another machine, another version, another
host, and sealing it under this server’s keys would make it restorable only
where those keys exist, which is the opposite of a backup. That means the dump
directory is exactly as sensitive as the database it came from — and a little
more so, because it is one directory you can copy in a single command.
Protect it with tools you already trust: write it onto an encrypted volume,
pipe the directory through gpg or age before it leaves the host, or give it
the same permissions, retention and off-site policy as any other copy of the
data. manifest.json holds no credentials (see
Account recreation)
but it does list every database and bucket, and the .bson files beside it are
your documents, verbatim.
Destroy it
[h]oardDB [shop]> drop bucket Customers
OK bucket "Customers" dropped from "shop" (data removed from disk)
[h]oardDB [shop]> drop bucket Orders
OK bucket "Orders" dropped from "shop" (data removed from disk)
[h]oardDB [shop]> show buckets
BUCKETS in shop
---
(no buckets)
restore
hoarddb restore -address 127.0.0.1:17433 -user admin -password ****** -insecure --dir ./dump
restoring shop/Customers (2 document(s))...
shop/Customers: 2 applied, 0 skipped
restoring shop/Orders (3 document(s))...
shop/Orders: 3 applied, 0 skipped
restore: 1 database(s), 2 bucket(s), 5 document(s) applied, 0 skipped
While a node joins the cluster, a restore is refused with retry. Starting a bucket’s restore creates or drops it, and schema changes pause until the join settles. A restore already under way finishes the bucket it is on; the next bucket’s start is refused, and the summary names the buckets that landed and the one refused, with the server’s reason:
restore: 1 database(s), 1 bucket(s), 2 document(s) applied, 0 skipped (incomplete: 1 bucket(s) failed)
restored shop/Customers
FAILED shop/Orders: Schema changes are paused while n4 joins the cluster ... Run this again once 'nodes' shows the join settled - it settles on its own. ...
Run the restore again once nodes shows the join settled. A restore --dry-run
is answered during a join, and adds that a real restore would be refused now.
Verify — counts and contents
[h]oardDB [shop]> show buckets
NAME TYPE ENTRIES
Customers hash 2
Orders fifo 3
[h]oardDB [shop]> db.Customers.get("c1")
{"Email": "ada@example.com", "Name": "Ada Lovelace", "_id_": "c1"}
[h]oardDB [shop]> db.Customers.get("c2")
{"Email": "grace@example.com", "Name": "Grace Hopper", "_id_": "c2"}
[h]oardDB [shop]> db.Orders.length()
3
[h]oardDB [shop]> db.Orders.pop()
{"_id_": "668a4412-09e0-4f80-965e-91cbaa795914", "item": "widget", "qty": 3}
[h]oardDB [shop]> db.Orders.pop()
{"_id_": "1e6d644a-5d95-43f2-beef-eeaf13fc76df", "item": "gadget", "qty": 1}
[h]oardDB [shop]> db.Orders.pop()
{"_id_": "7f8c1e3b-80a1-4c18-a251-29be5e5811b0", "item": "gizmo", "qty": 7}
Values round-tripped exactly, and the queue popped back out in the same order it was pushed in (widget, gadget, gizmo) — order is the data for a FIFO bucket, and the dump preserves it.
The REPL form
The exact same grammar works as a statement inside an interactive session,
so you can dump one bucket without leaving the shell. Flags come before the
positional database bucket arguments, same as the standalone form:
[h]oardDB [shop]> dump --out ./dump-repl shop Customers
dumping shop/Customers...
shop/Customers: 2 documents, 138 bytes, sha256:4892e5983f3f67eb93462c22a2cf2c9de4223f83cc0fb02af93677f4e4273920
dump: 1 database(s), 1 bucket(s), 2 document(s), 138 byte(s)
Scripting it
--json prints exactly one JSON object to stdout — progress, byte counts
and rate go to stderr — so a pipeline can parse the result without the
human-readable noise mixed in:
hoarddb restore -address 127.0.0.1:17433 -user admin -password ****** -insecure \
--dir ./dump --drop --json 2>restore.log
{"ok":true,"databases":["shop"],"buckets":1,"documents_applied":2,"documents_skipped":0,"elements_skipped":0,"users_recreated":0,"users_awaiting_password":0,"warnings":[],"errors":[]}
Exit codes: 0 everything was restored, 1 usage error (a bad flag, or a
--dir that is missing, unreadable or not a dump), 2 refused before
anything was applied (the server declined the restore), 3 partial (a bucket failed — check errors — or
data was skipped: documents_skipped or elements_skipped above zero, which
is what a blob bucket or a blobStore field gives you), 4 interrupted. A
script that restores a database with blob buckets or blob fields should expect
3, not 0, and read the two skipped counts to see what was left behind. restore --drop replaces each target bucket instead of merging
into it; without --drop, restore merges — and whether a re-run is safe
depends on the bucket’s store type, because the underlying write is
different per engine:
- hash and btree restore through an upsert (
OpUpsert), which replaces the document stored under a key and adds it when the key is new. Re-running the same restore twice converges: the second run replaces the same keys rather than adding to them. - FIFO and LIFO restore through a push (
OpFIFOPush, or a reverse-push for LIFO — seeserver/restore.go’sapplyRestoreDocument). A dump lists a LIFO bucket newest first, so its restore pushes nothing until the last element has arrived: the elements wait in a spill file in the data directory, sealed under a key held only in memory and removed when the restore ends, and are pushed oldest first when the restore finishes. The node needs free disk for that file, about the dump’s size, beyond the usual headroom; memory stays small whatever the bucket’s size. A queue element has no key to converge on, so re-running the same restore against one of these buckets delivers every element again, on top of what the first run already added. This is the specified, intentional behavior for a push-based engine, not a bug — but it means a cron job or retry loop that blindly re-runsrestoremust not merge into a FIFO/LIFO bucket that already received a previous run; use--dropto replace the bucket outright instead of merging into it a second time. - A heap restores through a push too (
OpHeapPush, seeserver/restore.go’sapplyRestoreHeapDocument), but it converges like a keyed engine: a restored element keeps the_id_it was dumped with, and a push under an_id_already present at the same priority overwrites it. Re-running the same restore twice leaves the heap at the same count. The one caveat is an element whose priority moved between the two runs: the entry key is the id and the priority, so the restored copy lands beside the element’s current self rather than on top of it. - Blob buckets restore nothing at all, on the first run or any subsequent one — see Blobs are not restored at all below.
hoarddb dump --out /backups/$(date +%F) is the whole cron line: it needs no
prompt, no TTY, and no flag to confirm when the destination is a fresh
directory.
What the dump promises about consistency
Per bucket, a dump is a consistent point in time. Across buckets, it is
not. This is how a logical dump normally behaves, for the
same reason, and it is worth stating as plainly: a dump of shop reads
Customers as of one instant and Orders as of a slightly later instant,
because each bucket is read independently while the rest of the server keeps
serving writes. manifest.json carries "globally_consistent": false so
nothing downstream has to guess. If you need every bucket frozen at exactly
the same instant, that is not what a logical dump gives you — stop writes for
the duration, or dump during a quiet window.
Within one bucket, though, the guarantee is real: a hash/btree/heap bucket’s dump is read from a single consistent snapshot, and writers are not blocked while it runs. A FIFO/LIFO bucket’s dump is the queue as of the moment it started capturing, and the queue keeps taking pushes and pops throughout — see the next section for what that means for elements popped mid-dump.
One dump covers the whole cluster
On a multi-node cluster, a dump with no database/bucket argument backs up
every bucket in the cluster, whichever member you connect to. The bucket
listing is answered for the whole cluster, not for the node you dialled, and each
bucket is then read from the node that owns it — so a bucket held by several
replicas is dumped once, from its owner, not once per copy. There is no need to
run dump against each node, and no reason to: every such dump would hold the
same buckets.
A scoped dump behaves the same way: hoarddb dump shop Orders reaches Orders
even when the node you are connected to is not the one that holds it.
The one thing that stops a scopeless dump is a member that is down or silent.
Producing the complete bucket list needs every member’s answer, and a short list
would read as a complete backup, so the dump is refused with a message naming the
member that did not answer and asking you to retry — run nodes to see member
state. That refusal is deliberate: a dump that quietly left out one node’s
buckets is the one failure a backup cannot have. A dump that names a database and
bucket does not need the cluster listing, so it does not care which members are down
as such. What it needs is a node that holds the bucket: the node you are connected to
answers it when that node holds a copy, and otherwise the request goes to the
bucket’s owner. It fails, naming the owner, only when the owner is the member that is
down and the node you are connected to holds no copy of the bucket. At replication
factor 1 each bucket has one holder, so it fails whenever that holder is down; with
more copies, connect to a member that holds one. For a bucket recorded spread, the
holder the request must reach is its router; see Spread
buckets.
Restore is unaffected by any of this: it always goes through a single connection
to a single server (-address names it), the same as any other write.
Spread buckets
A bucket recorded spread is dumped as one bucket, under its own name. Its
router asks every part, and the dump streams their documents into one file; no
part’s name appears in the dump, the manifest or an error. metadata.json
records "placement": "spread", so restore creates the bucket with that
placement and writes each document back to its part, through the bucket’s
router.
A spread bucket’s dump needs its router, the node that owns the bucket’s name,
because every document is read through it. A scoped dump therefore works from
any node you can reach while the router answers, and cannot run while the router
is down, even if the node you dialled holds a copy of the bucket’s name. A part
owner that does not answer refuses the bucket’s dump; the dump reports that
bucket as failed and exits 3, marking "incomplete": true in
manifest.json. It never writes part of a spread bucket as a complete one.
A second dump running at the same time is refused when it reaches a part the first dump is reading, naming the first bucket: a spread dump holds the dump slot of each part only while it reads that part, not one slot on the router. Wait for the first dump to finish, or dump another collection. A second dump of the same collection while the first runs is refused when it starts, with the “a dump is already running” refusal that names the first dump.
Queues: what you get back may be a superset
Dumping a FIFO or LIFO bucket does not pause it. Pushes during the dump simply land after the captured range and are not included — nothing surprising there. Pops are the case worth knowing: a pop advances the queue’s head, but the bytes the dump already captured stay readable until the dump finishes with that bucket, so an element popped during the dump can still end up in the dump file.
The practical effect: a restore can re-deliver an element your application
already consumed. For an at-least-once consumer this is exactly the cost of
being at-least-once and is harmless. For an exactly-once consumer it is
data — and this release does not tell you when it happened: the design
this feature was specified against calls for a "concurrent_mutations_detected"
manifest flag, but no such field is written by dump or read anywhere in
this release (verified: cli/dump.go’s topLevelManifest has no such
field, and neither concurrent_mutation nor ConcurrentMutations appears
anywhere in the codebase outside the specification document). There is
currently no flag to check. If you dump a FIFO or LIFO bucket that is being
actively popped from, assume the restored queue may contain elements your
application already processed — there is no way to confirm this after the
fact from the dump’s own output. The way to get an exact, no-redelivery
snapshot of a queue today is to dump it from a period with no concurrent
pops.
Elements that were deleted (tombstoned) before the dump captured them are never emitted — those do not come back. LIFO is dumped in pop order (newest-first), so a restore reproduces the stack correctly rather than reversing it.
The compaction-pin ceiling on FIFO/LIFO dumps
Dumping a queue holds a pin that blocks that bucket’s background compaction for as long as the dump is reading it — pushes and pops are unaffected, but disk space used by consumed entries cannot be reclaimed until the pin releases. The pin is released the moment the dump finishes the bucket, the connection closes, or a ceiling is hit — whichever comes first.
The ceiling defaults to 5 minutes and is configurable:
| Setting | Default | Meaning |
|---|---|---|
HOARDDB_DUMP_PIN_CEILING_SECONDS (YAML: server.dump_pin_ceiling_seconds) | 300 | Maximum time a queue dump may hold compaction pinned before the dump fails loudly with a named error, rather than pinning an unbounded amount of time on a busy queue |
A FIFO or LIFO bucket under continuous, heavy pop traffic can outrun this ceiling before its dump finishes. If that happens to you, dump that bucket during a maintenance window, or raise the ceiling for the duration of the dump and lower it again afterward — a pin held indefinitely against a high-throughput queue is a disk-exhaustion risk, which is exactly what the default keeps bounded.
The 16 MiB portability caveat
Most third-party BSON tooling assumes every document is at most 16 MiB — a
convention, not a limit of the format. hoardDB’s own
limits are different: a FIFO/LIFO entry may be up to the segment size (64
MiB by default), so a .bson file this tool produces from a legitimate
bucket can exceed that 16 MiB convention. That is not a bug in the dump —
it is a faithful copy of data that was already legally in the bucket — but it
means a .bson file from a queue-heavy hoardDB installation is not
guaranteed to open in tooling that hard-codes the 16 MiB assumption. hoardDB’s
own restore has no such limit; the caveat is specifically about handing the
file to something else.
Oversized documents are refused, not truncated
There is a real ceiling on what dump will write for you, and it is lower
than the 64 MiB segment size above: ~31.94 MiB (33,488,896 bytes exactly
— the 32 MiB wire-frame limit, minus headroom reserved for framing overhead).
A document that cannot fit inside one dump chunk is refused outright:
dump names the bucket and the key (or “queue element” for a FIFO/LIFO
entry with no key) and points at blob buckets as the place for a payload
that large. The bucket’s dump fails; the rest of the run continues, and the
failure is recorded in manifest.json.
If you store payloads anywhere near this size, use a blob bucket instead
of hash/btree/heap/fifo/lifo — blob payloads are designed for large
binary content, and no dump chunk ceiling applies to them.
That trade is not free, and it is the opposite of a backup guarantee: a
payload moved into a blob bucket leaves the dump entirely. A document that
is too large for dump fails loudly and is recorded in manifest.json; the
same bytes in a blob bucket are simply not captured, and restore brings
the bucket back empty (next section). Until that changes, back blob payloads
up out of band — dump and restore both warn on every run and name each
blob bucket, so a scripted backup can fail on the warning rather than
discover it during a recovery.
Blobs are not restored at all
Blob payloads are excluded from every dump, deliberately —
manifest.json and each blob bucket’s metadata.json carry
"blobs_included": false. The dump side does capture each blob’s metadata
(key, filename, content type, size, checksum, creation time) into that
bucket’s .bson file — a dump of a blob bucket is not empty on disk, and
this is a deliberate default, not a current limitation: a dump that silently
balloons into a full copy of every payload you store is the kind of thing
that gets run once and never trusted again.
restore, however, does not write any of that back. In this release,
every document in a blob bucket’s dump — metadata included — is counted into
the restore’s skipped total and otherwise discarded (verified:
server/restore.go’s applyRestoreDocument, the BucketTypeBlob case,
increments a skip counter and returns with no write of any kind).
A restored blob bucket in this release ends up completely empty — not
“metadata present, payload missing.” restore does report the count of
blob documents it skipped, so this is not silent, but do not expect a
restored blob bucket to hold anything: no key, no filename, no size, no
timestamp comes back. If you need a blob bucket’s metadata after a restore,
read it from the dump directory’s .bson/.metadata.json files directly —
dump captured it — rather than from the restored bucket, which restore
does not populate.
Store fields
A bucket can declare store fields, so each document owns a list, a map, a
queue or a blob store of its own (Comments fifoStore, Photos blobStore).
A dump records them in two extra files per bucket, <bucket>.stores.bson for
the elements and the stores list in <bucket>.metadata.json for the
declaration, and restore writes them back.
| Field engine | What restore brings back |
|---|---|
hashStore, btreeStore | every element, by its key |
fifoStore, lifoStore, heapStore | every element, in the same pop order (positions are assigned afresh) |
blobStore | nothing: the field is declared again and is empty |
A blobStore field’s elements are not restored for the same reason a blob
bucket’s are not: the dump carries no payload. Nothing about this is silent.
Both dump and restore warn, before anything is written, and name the
bucket and field (app/Post (blobStore field Photos)); the closing summary
counts the elements left behind, and --json carries the count as
elements_skipped and the warning in warnings. The exit code is 3, and
ok is false, because data was left behind:
restore: 1 database(s), 1 bucket(s), 1 document(s) applied, 0 skipped, 2 store-field element(s) skipped
{"ok":false,"databases":["app"],"buckets":1,"documents_applied":1,"documents_skipped":0,"elements_skipped":2,"users_recreated":0,"users_awaiting_password":0,"warnings":["blob payloads are not captured by any hoardDB dump yet: 1 blob bucket(s) in this dump have no payload bytes to restore — app/Post (blobStore field Photos). ..."],"errors":[]}
restore --drop refuses to run against a bucket that declares a blobStore
field, and against a dump that declares one, because dropping the bucket would
destroy elements the restore cannot put back. The bucket and its elements are
left as they were:
FAILED app/Post: restore start: server error (5): restore --drop would destroy blob payloads dump never captured: the bucket declares the blobStore field(s) Photos of app/Post — dump never captures blob field elements, so --drop would permanently destroy them recreating this bucket; run 'drop bucket Post' yourself first if this is really what you want, or restore into a different bucket
Copy the blob field’s data out of band before you rely on the backup, as you
would for a blob bucket. If you really do want the bucket replaced, run
drop bucket Post yourself and then restore.
A bucket’s replication factor
A bucket created with { replication_factor: N } keeps that factor through a
dump and a restore. The dump writes "replication_factor": N into the bucket’s
metadata.json; a bucket that declared no factor, and inherits the node’s, has
no such field, and a restore creates it inheriting the target’s default. A dump
taken before dumps recorded the factor reads the same way.
To see it, start the three-node local cluster from Replication (client ports 7433 to 7435), create a bucket at factor 2, dump it, and read the field back:
printf 'create database shop;\nuse shop\ncreate bucket Orders {type: hash, replication_factor: 2};\ndb.Orders.insert({_id_: "o1", total: 12})\nexit\n' | \
./hoarddb --insecure --user admin --password "$HOARDDB_ROOT_PASSWORD" --address localhost:7433
./hoarddb dump --insecure --user admin --password "$HOARDDB_ROOT_PASSWORD" --address localhost:7433 \
--out ./dump-rf shop Orders
grep replication_factor ./dump-rf/shop/Orders.metadata.json
Restore it over the same bucket with --drop, and it is re-created at factor 2;
restore it into a standalone server (one hoarddb-server started on its own,
here on 17433), and the summary ends with the block shown in the first case
below, because one server keeps one copy:
./hoarddb restore --insecure --user admin --password "$HOARDDB_ROOT_PASSWORD" --address localhost:7433 \
--drop --dir ./dump-rf
./hoarddb restore --insecure --user admin --password "$HOARDDB_ROOT_PASSWORD" --address localhost:17433 \
--dir ./dump-rf
restore creates each bucket with the factor its dump records, both when the
bucket is new and when --drop re-creates it. A bucket that does not end up
with that factor is never silent: the summary ends with one block per case, on
stderr, naming at most ten buckets and then “and N more”, and --json lists
every one as database/bucket in factor_not_restored, factor_kept or
factor_lowered (each present only when it has a bucket in it). None of them
changes the exit code: the documents were restored.
The target cannot grant the factor. A standalone server keeps one copy, and a cluster cannot place more copies than it has members. The bucket is created inheriting the node default, and the data is restored:
restore: 2 bucket(s) were restored WITHOUT the replication factor their dump records shop.Events (3), shop.Orders (3) This server cannot place that many copies (it is standalone, or has fewer members than the factor), so they use the node default. Add members ('join' on each new node, then 'nodes' to count them), then run this restore again with --drop.The bucket already exists and the restore merges into it. A merge restores documents, not placement, so the bucket keeps its own factor (
shop.Orders (kept 1; the dump records 3)). Run the restore with--dropto take the dump’s factor.--dropover a bucket that declared a factor, from a dump that records none. The bucket is re-created inheriting the node default, and the summary says its factor was LOWERED (shop.Audit (was 3; the dump records none)). To keep the factor, drop the bucket, create it with{ replication_factor: 3 }, and restore without--drop.
restore --dry-run reports the same three cases without creating anything,
in the conditional: restore --dry-run: 2 bucket(s) would be restored without the replication factor their dump records, would keep their own replication factor, would have their replication factor LOWERED by --drop.
Account recreation is not implemented in this release
hoardDB dumps and restores data and bucket definitions. It does not dump or restore user accounts.
Concretely: dump never writes a users.json file, and restore never
reads one. If a dump directory happens to contain a users.json — hand-
written, copied in from a different tool, or left over from a future
version — restore notices it, prints a warning, and does not act on it:
warning: users.json present but account restore is not yet implemented; accounts were not recreated
{"warnings":["users.json present but account restore is not yet implemented; accounts were not recreated"]}
The warning appears in both the text summary and --json output, never only
as a log line, so a script that only checks the exit code still has
somewhere to see it if it parses warnings.
This is a real limitation of this release: user accounts are managed on the
server (see User management), but dump does not
capture them yet and restore does not recreate them.
A future release that adds persistent, mutable user accounts is expected to
add account recreation to dump/restore at the same time; until then, back
up your credential configuration (HOARDDB_ROOT_USER/HOARDDB_ROOT_PASSWORD
or your config file) the way you already do for any other secret.
Upgrading the storage layout
This build keeps every bucket of a node in one shared store. Earlier builds kept each bucket in a directory of its own. The server does not convert a data directory from one layout to the other; it refuses to start on the old one and says so:
Data directory ./data uses the per-bucket storage layout (catalog version 1).
This server writes the shared layout (version 2) and will not convert data in place.
Back up with the previous release's 'hoarddb dump', then restore into a fresh
directory with this release — see docs/user/backup-restore.md#upgrading-the-storage-layout.
The refusal comes from what is in the directory, not from its catalog alone. A
data directory that has a catalog.json is refused, and so is one with no
catalog.json that still holds bucket directories (<database>/<bucket>/ with
a badger/, segments/, blobs/ or indexes/ directory, or a
key.wrapped file, inside). The check is the first thing the server does, before
it writes a key, a token or a store, and it only lists directory names, so the
old directory is exactly as you left it.
To move to the shared layout, dump with the build that wrote the data and restore into a new, empty data directory with this one:
Start the earlier build on the old data directory and take a dump, as in The Drill:
hoarddb dump -address 127.0.0.1:17433 -user admin -password ****** -insecure --out ./dumpStop it. Start this build on a new data directory (a fresh
./data, or another path given with the data directory setting). Do not point it at the old one.Restore into the new server:
hoarddb restore -address 127.0.0.1:17433 -user admin -password ****** -insecure --dir ./dumpCheck the counts with
show bucketsanddb.<bucket>.count()before you delete the old directory. Keep the old directory and the dump until you have.
A cluster has one data directory per node, and each is refused the same way. Take one dump of the cluster before you stop any of the nodes (see One dump covers the whole cluster); it holds every bucket whichever member you connect to, each bucket once, from its owner. Then start this build on a fresh data directory for each node and restore through one node. Restore one bucket once: running the same restore into a queue bucket twice would deliver its entries twice.
What a dump carries is what it carries on any build, and the sections above still apply:
- Hash, btree, fifo, lifo and heap buckets come back with their documents and index declarations. A queue comes back in order.
- Blob buckets do not. Blob payloads are not in a dump and
restorewrites nothing into a blob bucket (see Blobs are not restored at all). A data directory with blob buckets cannot be upgraded with a dump alone: copy the files out of the old directory yourself first, and upload them again after the restore. - Users are not in a dump (see Account recreation is not implemented in this release). The new server starts with its own root credential.
- A dump is plaintext, even from an encrypted node (see The dump is plaintext). Delete it when the restore is verified. Whether the new node encrypts its data is decided by its own configuration when its directory is first started; see Configuration.
When a store file is damaged
A node keeps its buckets in table files under <data dir>/store, and every
block of those files carries a checksum that is checked when the block is read
from disk. A block that fails it — a bad disk, a torn copy — is never answered
as data and never as “not found”. A read of a document in it fails:
bucket "users" in database "app" could not be read: a file of the node store
at /var/lib/hoardDB/store is damaged, so the read failed and returned nothing
(a table block could not be read: table /var/lib/hoardDB/store/000042.sst: ...
checksum mismatch). Reads of the damaged part keep failing rather than answer
wrongly. Stop this node and move /var/lib/hoardDB/store aside ...
A search, a dump or a page that walks across the block fails the same way
instead of leaving the block’s documents out, so a dump of a damaged node is
refused rather than taken incomplete. The rest of the node keeps answering.
The node logs the first such read at ERROR and counts every one in the
Prometheus counter hoarddb_store_damaged_reads_total. Alert on any increase:
the damage does not heal, and a node with damaged files is one disk failure
away from losing data its replicas do not hold.
To repair it, replace the node’s store:
- If the node’s buckets have replicas on other nodes, remove the node with
remove-node <node-id>, stop it, move its data directory aside (keep it until the node is back: its undamaged files may hold the only copy of something), start it with an empty data directory andjoinit again. It is seeded from the other holders. See Replication. - Otherwise restore the node from your last dump, as in The Drill, into a fresh data directory. Writes since that dump are lost.
Summary of flags
| Flag | Applies to | Meaning |
|---|---|---|
--out <dir> | dump | Destination directory (default ./dump). Refused if non-empty and not itself a previous dump |
--dir <dir> | restore | Source directory (default ./dump) |
--drop | restore | Drop each target bucket before restoring it. Without it, restore merges — see the per-engine re-run behavior above before relying on merge for FIFO/LIFO (a heap merges safely) |
--allow-incomplete | restore | Restore from a dump manifest marked "incomplete": true (one or more buckets failed to dump). Refused by default so an automated pipeline does not silently apply a partial backup |
--dry-run | restore | Validate every file and report what would change; write nothing |
--json | both | One JSON result object on stdout; progress on stderr |
-address, -user, -password, -insecure | both | The same connection flags the CLI itself takes |
dump [database [bucket]] and restore (source is always --dir) both
work as REPL statements with the same flags, minus the connection ones —
the REPL is already connected.
Source: docs/user/backup-restore.md in the repository.