template, json and hstore transformers no longer corrupt their shared random generator under multi-worker snapshots; batches discarded by ignore_send_errors are counted and exported as metrics; snapshot failures are logged as errors instead of rendering as {}; snapshot request status updates are retried; and a failed restore now carries the output that explains it, with credentials redacted. No migration is required.
Shared Template State
PR #1045, by @kvch. Thetemplate, json and hstore transformers build their templates from greenmask’s toolkit function map, which closes over a single *rand.Rand shared by every random* and noise* function. pgstream builds one transformer per column and calls it from all snapshot workers, so a template as simple as {{ randomInt 1 1000 }} had several goroutines drawing from one generator: the lagged-Fibonacci source’s indices were corrupted and the call failed with index out of range [-1]. With the batch writer’s error handling this surfaced as a per-row DATALOSS — the transformed value dropped or nulled — rather than a crash, so it was easy to miss.
Templates are now pooled, so each execution gets an instance with function map state of its own. This is the same mechanism applied to the greenmask generators in PR #884, which covered the string transformer and the byte generators but not the template function map. Pooling also covers the other shared state in that map: the pgtype.Map behind tsModify and noiseDatePgInterval.
Accounting for Dropped Batches
PR #1042, by @kvch.ignore_send_errors: true turns a failed send into a dropped batch and keeps the pipeline running. Until now nothing counted that: each drop produced a log line, with no total anywhere and no metric at all, so the only way to size the loss was to count log lines after the fact.
Every writer that can drop — the Postgres batch and bulk-ingest writers, the Kafka writer and the search indexer — now shares one counter with the batch senders it builds, exported as two observable counters:
pgstream.batch.sender.dropped_batches— batches discarded becauseignore_send_errorsis enabledpgstream.batch.sender.dropped_messages— messages lost with those batches
writer_type attribute — postgres_batch_writer, postgres_bulk_ingest_writer, kafka_batch_writer or search_batch_indexer — so a run with several writers can be told apart. Each drop is logged with severity: DATALOSS, the batch’s message count and size, and the running totals; the totals are reported once more at shutdown.
Suppression is also stated at startup rather than being visible only in hindsight: a writer configured with ignore_send_errors warns that failing batches will be dropped, and the Postgres batch writer says when strict_mode is off, since that governs the per-query drop-and-continue path.
Relatedly, pgstream.postgres.writer.dropped_queries is now registered only for the batch writer, the only writer that can increment it. It was previously exported for the bulk-ingest writer as well, where it published a permanent zero under a writer_type label naming a component that does drop data, by whole batches.
Snapshot Reporting
PR #1039 and PR #1041, by @kvch. A snapshot that failed loggedsnapshot generation completed at info level with the error rendered as {}, because the error was passed as a log field rather than as an error. Failures are now logged as errors with the message snapshot generation failed.
Recording the outcome of a snapshot request is also no longer a single attempt: the status update is retried (3 attempts, 500ms apart). If it still cannot be persisted, the failure is logged with severity: DATALOSS and the status that remains stored, because a request left in a non-terminal state is never picked up again.
Restore Errors Carry Their Output
PR #1040, by @kvch. Whenpg_restore or psql exited non-zero without a parseable error, pgstream reported only the exit status, discarding the output that said what actually went wrong. The error now carries the tail of that output (up to 4KiB), with credential material removed first: PASSWORD '...' literals in role statements and the row payload psql appends to a failed COPY are both redacted.
Upgrade
No configuration changes are required. ⚠️ If you use thetemplate, json or hstore transformers with greenmask’s random*/noise* functions, snapshots run with more than one worker before this release may have written incomplete data: affected values were dropped or nulled per row rather than failing the run. Check the affected columns and re-snapshot if needed. The workaround for earlier versions — sprig’s randInt in place of randomInt, or table_workers/schema_workers set to 1 — is no longer needed.
Alerting on drop logs. When ignore_send_errors is enabled, the per-drop line changed from failed to send batch to failed to send batch: dropping it and continuing, and now carries severity: DATALOSS. Alerts keyed on the old message text need updating; keying on the severity field covers this and the other silent-loss sites. The message is unchanged when ignore_send_errors is off, where the error still stops the pipeline.
Metrics. The two new counters are exported only when instrumentation is enabled. pgstream.postgres.writer.dropped_queries no longer appears with writer_type: postgres_bulk_ingest_writer; that series was always zero.
Contributors. make lint now installs golangci-lint v2.10.1 to match CI, which reports findings the previously pinned v2.5.0 did not.
Support
- Issues: https://github.com/xataio/pgstream/issues
- Documentation: https://github.com/xataio/pgstream/tree/main/docs
- Discussions: https://github.com/xataio/pgstream/discussions