Snapshot Writer
Snapshot Writer (laredo-snapshotter)
laredo-snapshotter is a standalone process that subscribes to a
fan-out table and continuously writes it to durable storage as a
base snapshot + a stream of diffs, indexed by a manifest, so cold and
cross-account consumers can read the table from object storage on their own
schedule. For the design and the full picture, see
Snapshot Writer — Architecture.
What it produces
For each table, under a key prefix on each destination:
config_document/
manifest.json # the index: latest state + the artifact chain
epoch=1/
snapshot-0_19F000.jsonl # base snapshot (full table at a WAL position)
diff-0_19F000-0_1A2B3C.jsonl
...
epoch=2/
snapshot-0_2B0000.jsonl # a re-base started a new epoch
...
A consumer reads manifest.json, loads the newest snapshot, and applies the
diffs after it to reconstruct the table as of head_position.
Reading the archive
The snapshotter only writes; reconstruction is the consumer's job — but
snapshotter.Reader is the library tool for it. Point it at the same
destination, prefix, and formats the writer used.
Point-in-time reads
ReconstructAsOf materializes the full table as of any source position, by
reading the newest base snapshot at or before it and folding the diffs up to it:
reader, _ := snapshotter.NewReader(dest, "public.events/", jsonl.New())
rec, err := reader.ReconstructAsOf(ctx, "0/1A2B3C0", []string{"id"}, cmp)
// rec.Rows — the full table state
// rec.Position — the effective position it reflects: the latest artifact
// boundary at or before the requested one (diffs are not
// splittable, so a request between boundaries lands on the
// earlier one)
cmp compares two opaque position strings (negative / zero / positive) — supply
it from your source (e.g. WAL-LSN ordering). keyFields must match what the
snapshotter wrote with (defaults to ["id"]). ReconstructAsOf returns nil
when the archive cannot reach the requested position (it predates the oldest
snapshot).
This is the building block for audit ("what did this look like at T?"), point-in-time exports, and historical diffs — all from object storage, with no live system in the loop. For resuming forward from a position (the fan-out cold-replay case) see Cold-tier replay.
From the CLI
laredo archive reconstruct exposes the same thing without writing code. It
reads object storage directly — no laredo-server connection — so it works
offline (forensics, an onboard node). It builds the reader through the same
snapshotter/destwire path the server and snapshotter use:
$ laredo archive reconstruct \
--store local --path /var/lib/laredo/archive/events \
--key-prefix "public.events/" --format jsonl \
--key-fields id --at 0/1A2B3C0
{
"position": "0/1A2B3C0",
"row_count": 1234,
"rows": [ ... ]
}
For an S3 archive, use --store s3 --bucket <b> --prefix <p> --region <r>
(ambient AWS credentials). It prints the effective position (the artifact
boundary at or before --at), and exits non-zero if the archive cannot reach
the requested position. Positions are ordered as PostgreSQL WAL LSNs.
Install
# from source
go build -o laredo-snapshotter ./cmd/laredo-snapshotter
# container image
docker pull ghcr.io/zourzouvillys/laredo-snapshotter:latest
Configure
HOCON. A minimal local config (full example:
examples/snapshotter/):
snapshotter {
source { server = "localhost:4001", schema = public, table = test_users }
diff { interval = 5s }
snapshot { min_interval = 30s, max_interval = 10m, max_churn_records = 1000 }
destinations = [ { type = local, path = "./.laredo-archive" } ]
formats { snapshot = [ jsonl ], diff = [ jsonl ] }
http { port = 8080 }
}
Run it:
laredo-snapshotter --config snapshotter.conf
# or: LAREDO_SNAPSHOTTER_CONFIG=/etc/laredo/snapshotter.conf laredo-snapshotter
Re-base thresholds
A diff is written every diff.interval; a fresh base snapshot is written instead
whenever a threshold fires (any one, subject to min_interval):
| Key | Meaning |
|---|---|
snapshot.min_interval |
Floor — never re-base more often than this |
snapshot.max_interval |
Ceiling — always re-base at least this often |
snapshot.max_diff_bytes |
Re-base when a serialized diff reaches this size |
snapshot.max_diff_fraction |
…or this fraction of the last snapshot's size |
snapshot.max_churn_records |
…or this many changed rows since the snapshot |
snapshot.max_churn_fraction |
…or this fraction of the dataset |
Omit a key to disable that trigger.
Multiple tables
One process can materialize several tables — give a tables array, each entry a
full table block:
snapshotter {
http { port = 8080 }
credentials { s3w { type = ambient } }
tables = [
{ source { server = "laredo:4001", schema = public, table = users }
destinations = [ { type = s3, bucket = b, prefix = "users/", credentials = s3w } ]
formats { snapshot = [ jsonl, protobuf ], diff = [ protobuf ] } },
{ source { server = "laredo:4001", schema = public, table = orders }
destinations = [ { type = local, path = "/var/lib/laredo/orders" } ] }
]
}
Destinations, formats, events, credentials
- Destinations (
type = local | s3) — an artifact is durable only once written to all destinations. S3 destinations name a credentials profile. - Formats (
jsonl,protobuf) — snapshots and diffs may differ, and you may emit several (each a separate object referenced from the manifest). - Events (
sns,sqs,kinesis) — advisory, at-least-once notifications published after the manifest head advances. Consumers must still poll the manifest as the source of truth. - Credentials — named profiles referenced per AWS-backed component, so one process can use different roles for different actions:
credentials {
s3w { type = ambient } # SDK default chain (env, IRSA, task role)
pub { type = assume_role
role_arn = "arn:aws:iam::222233334444:role/laredo-events-pub"
external_id = "laredo" }
}
Operate
laredo-snapshotter serves an HTTP API on http.port:
| Endpoint | Purpose |
|---|---|
GET /health/live |
Process is up |
GET /health/ready |
Every table has written its initial base snapshot |
GET /status |
Per-table position, epoch, buffer depth, churn, last snapshot |
POST /snapshot |
Force an immediate re-base on every table |
GET /metrics |
Prometheus: snapshotter_epoch, snapshotter_buffer_depth, snapshotter_churn_records, snapshotter_snapshot_age_seconds (per table), plus process/Go metrics |
On SIGTERM/SIGINT the writer flushes a final diff so no buffered changes are
lost. See the runbook for incident
procedures.