Configuration and Failure Reference¶
Everything about pg_relay.file_spool in one place. The other pages explain why. This page lists what.
Registration¶
SELECT pgrelay.register('exports', '<options JSON, or empty>', p_action_type := 'pg_relay.file_spool');
A file_spool channel is an ordinary channel. All of register()'s options apply: p_notes, p_active, p_max_retries, p_concurrency_mode, p_node_restricted. The functions update(), enable(), disable(), unregister(), get(), and list() work as they do for any channel. See Function Reference: Channels and Events.
Channel options (the action column)¶
| Option | Type | Default | What it does |
|---|---|---|---|
filename |
string | {channel}-{event_id}.json |
File name template. Tokens: {channel}, {event_id}, {queued_at}, {payload.<key>}. |
extension_tmp |
string | .tmp |
Suffix while writing. Letters, digits, ., _, - only. Not empty. |
mode |
octal digits as a string, or a number | 0640 |
File permissions after writing. Linux and macOS only. |
replace |
boolean | false |
Overwrite an existing file instead of skipping it. A JSON-object payload can override it with a top-level "replace" key. |
An empty action means all defaults. Unknown keys are ignored. A wrong value makes every event on the channel fail without retry.
Environment variables¶
| Variable | Default | What it does |
|---|---|---|
PG_RELAY_SPOOL_DIR |
not set | Base folder. A channel without its own folder writes to <base>/<channel>/. |
PG_RELAY_SPOOL_<CHANNEL>_DIR |
not set | Folder for one channel. Overrides the base. |
PG_RELAY_SPOOL_MAX_BYTES |
no limit | Largest total size of the files in a channel's folder, temporary files included. A write that would reach or pass this size is held back. |
PG_RELAY_SPOOL_<CHANNEL>_MAX_BYTES |
not set | The same limit for one channel. Overrides the global one. |
PG_RELAY_SPOOL_MAX_FILE_BYTES |
16777216 (16 MiB) |
Largest single payload. Exactly this size is allowed. Bigger fails without retry. |
<CHANNEL> is the channel name in upper case, with every character that is not a letter or digit changed to _.
Sizes are whole numbers of bytes. If a size variable is set but is not a whole number, the Processor refuses to start. If a channel has no folder of its own and no base folder is set, its events are held back, and the log names both variables. The Processor reads the variables for every event. To change one, edit the environment file and restart.
File name tokens and the safe name rule¶
| Token | Value |
|---|---|
{channel} |
The channel name, as stored. |
{event_id} |
The queue row id. A bigint. |
{queued_at} |
When the event was queued, UTC, in the form 20260907T041200Z. |
{payload.<key>} |
A top-level string or number from a JSON-object payload. Numbers appear as sent: 4471, 1.50. |
The finished name must contain only letters, digits, ., _, and -. It must not contain ... It must not be empty. It must be shorter than 200 bytes.
These cases fail the event without retry:
- The
{payload.<key>}key is missing from the payload. - The value is
null, a boolean, an object, or a list. - The payload is empty, or is not a JSON object, and the template uses
{payload.<key>}.
The name is never written to the error text.
The write, step by step¶
- Check the folder exists and is a folder. If not, hold the event back.
- Check whether the final file already exists. If it does and
replaceis off, report success and stop. If it does andreplaceis on, carry on. The rename will replace it. A symbolic link counts as "exists" and is never followed. - Check the size limit. Add up the regular files in the folder. Subtract the size of a file about to be replaced. Add the payload size. If the total reaches the limit, hold the event back.
- Remove any leftover temporary file from an earlier crashed attempt. A symbolic link is unlinked, not followed. Then create the temporary file, and fail if it already exists. If permission is denied, or the folder has vanished, hold the event back. Any other error is a disk error.
- Write the bytes. Flush them to disk. Set the permissions (Linux and macOS). Close the file.
- Rename the temporary file to its final name. This is a single step on every platform. Then flush the folder to disk (Linux and macOS). Any error here is a disk error, and the temporary file is removed if possible.
What happens when things go wrong¶
| What happened | Outcome | Audit row | Uses a retry? |
|---|---|---|---|
| The folder is not set, does not exist, or cannot be written to | Held back. The event stays in the queue and is tried again every second. | none | no |
| The folder has reached its size limit | Held back. | none | no |
The payload is bigger than MAX_FILE_BYTES |
Failed, no retry. | error, naming the limit |
— |
The file name is unsafe or empty, a channel option is wrong, or the payload's "replace" is not true/false |
Failed, no retry. Nothing is written. | error |
— |
| A disk error during the write or rename | Retried on the normal retry schedule: 3 s, then 5 s, then 10 s, up to the channel's max_retries. Then it fails. The temporary file is removed if possible. |
error for each attempt, with retry_scheduled in the log |
yes |
The file already exists and replace is off |
Success. Skipped. | ok, with the note file already exists; write skipped (…) |
— |
The file already exists and replace is on |
Success. Replaced. | ok, with the note file already existed; replaced (replace=true) |
— |
| The channel is disabled or unregistered | invalid |
invalid |
— |
The event's expire_at has passed |
expired, row deleted |
expired |
— |
"Held back" is different from "retried", on purpose. A held-back event gets no audit row, uses none of its retries, and keeps the same queue row and {event_id}. It is simply offered again on the next tick. A folder that stays full for an hour produces one warning in the Processor's log, not thousands of audit rows. Use pgrelay.queue_stats() to see the backlog: pending grows and oldest_pending gets older.
Disk errors use the retry schedule. If a channel's folder is on unreliable storage, give the channel a higher p_max_retries.
Log lines¶
Every line carries "instance": N, like all Processor log lines. The payload and the file name never appear. The folder path does, because it is part of the operator's own environment.
| Message | Level | Shown at the default level (warn)? | Fields |
|---|---|---|---|
spool_config_invalid |
error | yes | error. A PG_RELAY_SPOOL_* size is not a whole number. The Processor stops. |
spool_dir_unavailable |
warn | yes | variable, path, error. At startup, a folder named in the environment does not exist. |
spool_unavailable |
warn | yes | channel, reason. Logged once when a channel's events start being held back. |
spool_available |
info | no | channel. Logged once when the first write after that succeeds. |
spool_withheld: id: <id> |
debug | no | One event held back. |
spool_exists: <ch>, id: <id> |
info | no | log_id. The file already existed and the write was skipped. |
spool_replaced: <ch>, id: <id> |
info | no | log_id. The file already existed and was overwritten. |
ok, error, retry_scheduled, invalid, expired, skipped |
as for every action type | See Log Levels and Format. |
Monitoring¶
-- Backlog and outcomes per channel over the last minute (the default window)
SELECT * FROM pgrelay.queue_stats() WHERE action_type = 'pg_relay.file_spool';
-- Recent outcomes, newest first. The skipped/replaced notes are in the error column.
SELECT actioned_at, channel, status, error, elapsed_ms
FROM pgrelay.log_report(p_channel => 'exports', p_within => interval '1 hour');
Grants and upgrades¶
pg_relay.file_spool needs no new grants. It uses the same internal functions every action type already uses, and the pgrelay role already has them. preflight() already checks them. A Processor on pg_relay 1.1 or later gets the feature when you upgrade to 1.6. The only setup is PG_RELAY_SPOOL_DIR and a restart.
The upgrade script sql/pg_relay--1.5--1.6.sql adds the action type and updates the version number. That is all it does. Run ALTER EXTENSION pg_relay UPDATE; on a normal install. On a managed cloud database, run the script directly.
Old Processor binary? A Processor older than 1.6 does not know this action type. It hands the event to process_one(), which records unsupported_action_type in the audit log and marks the event done. It never loops. Upgrade the binary and the files start flowing.
Platform summary¶
| Linux and macOS | Windows | |
|---|---|---|
| Rename into place | rename(2), one step |
MoveFileEx, replaces the target in one step |
| Flush the file to disk | yes | yes, FlushFileBuffers |
| Flush the folder to disk | yes | not available, skipped |
mode option |
applied | not applied; the file gets the folder's permissions |
| Symbolic links | never followed | never followed |
Back to the Overview. For notify()'s delay, expiry, and deduplication options, which all work with file_spool channels, see Sending Events.