Data & Databaselow risk

wal-and-xlog

Add or modify a PostgreSQL WAL / XLOG record — covers choosing built-in rmgr vs Generic WAL (generic_xlog.c) vs custom rmgr (RegisterCustomRmgr), the XLogInsert + XLogRegisterBuffer idiom, writing a correct redo function, FPI / hint-bit (MarkBufferDirtyHint) rules, full-page-write handling, updating rmgrdesc + pg_waldump output, and bumping XLOG_PAGE_MAGIC when format changes. Use whenever a PG patch emits XLogInsert, adds an access method that needs durability, writes or modifies a redo function, registers a custom rmgr via RegisterCustomRmgr, or reviews a patch that changes WAL-record formats. Skip for MySQL binlog / GTID / row-based logging, SQLite WAL journal mode, Kafka log compaction / segment retention, pgbackrest / barman / wal-g operational config, recovery.conf / standby.signal DBA-side configuration, and generic write-ahead-logging theory or ARIES algorithm questions.

matejformanek/postgres-claude·.claude/skills/wal-and-xlog/SKILL.md
31/ 100推荐值

导入这个 Skill

选择你的 coding agent,复制项目级或个人级安装命令。

固定到平台收录的 commit
导入当前项目.agents/skills/wal-and-xlog
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wal-and-xlog -a codex -y
导入个人环境~/.agents/skills/wal-and-xlog
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wal-and-xlog -a codex -g -y
手动放置目录.agents/skills/wal-and-xlogOfficial docs ↗
导入当前项目.claude/skills/wal-and-xlog
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wal-and-xlog -a claude-code -y
导入个人环境~/.claude/skills/wal-and-xlog
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wal-and-xlog -a claude-code -g -y
手动放置目录.claude/skills/wal-and-xlogOfficial docs ↗
导入当前项目.agents/skills/wal-and-xlog
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wal-and-xlog -a github-copilot -y
导入个人环境~/.copilot/skills/wal-and-xlog
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wal-and-xlog -a github-copilot -g -y
手动放置目录.agents/skills/wal-and-xlogOfficial docs ↗
导入当前项目.agents/skills/wal-and-xlog
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wal-and-xlog -a cursor -y
导入个人环境~/.cursor/skills/wal-and-xlog
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wal-and-xlog -a cursor -g -y
手动放置目录.agents/skills/wal-and-xlogOfficial docs ↗
导入当前项目.agents/skills/wal-and-xlog
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wal-and-xlog -a gemini-cli -y
导入个人环境~/.gemini/skills/wal-and-xlog
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/wal-and-xlog -a gemini-cli -g -y
Native Gemini CLIgemini skills install https://github.com/matejformanek/postgres-claude.git --scope workspace --path .claude/skills/wal-and-xlog
手动放置目录.agents/skills/wal-and-xlogOfficial docs ↗
⚠ 安装命令使用开源 skills CLI。执行前请检查来源、脚本和权限。
# WAL & XLOG — operational skill

This is the procedural cookbook. For the conceptual model (LSNs, FPI,
checkpoints, rmgrs) read `knowledge/architecture/wal.md` first.

## When to use what

| Situation | Use |
|---|---|
| In-tree access method, new record type | new builtin rmgr (RmgrId < 128) — needs core patch |
| Extension that just modifies pages (small new index AM, prototype) | **Generic WAL** (`generic_xlog.c`) |
| Extension that needs compact records, logical decoding, or custom replay logic | **Custom rmgr** via `RegisterCustomRmgr` |
| Optimisation hint that can be re-derived from authoritative state | **no WAL** — use `MarkBufferDirtyHint()` |
| Tracking commit/abort of an XID (you almost never need to do this) | not a WAL concern — it's pg_xact |

[from-comment `source/src/backend/access/transam/rmgr.c:97-104`;
from-code `source/src/backend/access/transam/generic_xlog.c:1-12`]

## Custom rmgr vs Generic WAL — decision

Choose **Generic WAL** when:
- You're prototyping or your extension is small.
- Record volume isn't a bottleneck (per-page delta can be larger than a
  hand-rolled record).
- You don't need logical decoding.

Choose **Custom rmgr** when:
- Record volume matters (frequent small changes).
- You need replay-time semantics beyond "apply this page delta" (e.g.
  cross-page invariants, structure-specific recovery).
- You need a `rm_decode` hook for logical decoding.
- You want pretty `pg_waldump` output via `rm_desc` / `rm_identify`.

## Custom rmgr — full checklist

### 1. Reserve an RmgrId

- Valid custom range: `128 ≤ rmid ≤ 255` (`RM_MIN_CUSTOM_ID` ..
  `RM_MAX_CUSTOM_ID`). [verified-by-code
  `source/src/include/access/rmgr.h:35-36`]
- During development use `RM_EXPERIMENTAL_ID = 128`. [verified-by-code
  `rmgr.h:60`]
- For release: register your unique ID at
  <https://wiki.postgresql.org/wiki/CustomWALResourceManagers> so it
  doesn't collide with another extension. [from-comment `rmgr.c:99-104`]

### 2. Define the `RmgrData` struct

In your extension's headers/source, define a static `RmgrData` filling at
least:

```c
static const RmgrData my_rmgr = {
    .rm_name      = "my_extension",
    .rm_redo      = my_redo,
    .rm_desc      = my_desc,
    .rm_identify  = my_identify,
    .rm_startup   = NULL,         /* optional */
    .rm_cleanup   = NULL,         /* optional */
    .rm_mask      = my_mask,      /* needed for wal_consistency_checking */
    .rm_decode    = NULL,         /* optional; only if records should be
                                     visible to logical decoding plugins */
};
```

[verified-by-code `source/src/include/access/xlog_internal.h:351-362`]

### 3. Register from `_PG_init`

```c
void
_PG_init(void)
{
    if (!process_shared_preload_libraries_in_progress)
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                 errmsg("my_extension must be loaded via shared_preload_libraries")));
    RegisterCustomRmgr(RM_EXPERIMENTAL_ID, &my_rmgr);
}
```

`RegisterCustomRmgr` will `ereport(ERROR)` if:
- the name is empty,
- the rmid is outside the custom range,
- you're not in `shared_preload_libraries`,
- the rmid is already taken,
- the name (case-insensitive) collides with another loaded rmgr.

[verified-by-code `source/src/backend/access/transam/rmgr.c:107-146`]

### 4. Emit records (writer side)

The five-step write rule from `transam/README:437-470` is mandatory:

```c
/* 1. Pin & exclusive-lock all buffers we'll modify */
LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);

START_CRIT_SECTION();           /* 2. errors from here on = PANIC */

/* 3. Apply changes to the page in shared buffers */
modify_page(buf);

MarkBufferDirty(buf);           /* 4. BEFORE the WAL insert */

/* 5. Build & insert the WAL record */
XLogBeginInsert();
XLogRegisterBuffer(0, buf, REGBUF_STANDARD);
XLogRegisterData(&xlrec, SizeOfMyXlog);          /* main data */
XLogRegisterBufData(0, payload, payload_len);    /* per-block data */
recptr = XLogInsert(MY_RMGR_ID, XLOG_MY_OP);
PageSetLSN(BufferGetPage(buf), recptr);

END_CRIT_SECTION();             /* 6 */

UnlockReleaseBuffer(buf);       /* 7 */
```

Pre-flight checks (page-full, etc.) must happen *before*
`START_CRIT_SECTION()`. [from-README `transam/README:441-446`]

`REGBUF_*` choice cheatsheet:
- `REGBUF_STANDARD` — standard page (hole stripped from FPI). Use this
  whenever you can.
- `REGBUF_WILL_INIT` — redo will recreate the page from scratch (use for
  page-initialising records like btree split's right page).
- `REGBUF_FORCE_IMAGE` — force FPI even if not first-after-checkpoint;
  useful when you rewrite most of the page anyway.
- `REGBUF_KEEP_DATA` — keep the registered block data in the record even
  when an FPI is taken.
- `REGBUF_NO_IMAGE` — never take an FPI. Rarely correct; only if torn-page
  risk is genuinely zero. [verified-by-code
  `source/src/include/access/xloginsert.h:31-41`]

Default working-area limits: 5 block refs, 20 data chunks. To exceed, call
`XLogEnsureRecordSpace(max_block_id, ndatas)` **before** `XLogBeginInsert()`
and **outside** the critical section. [from-README `transam/README:542-553`]

### 5. Write the redo function

Signature: `void my_redo(XLogReaderState *record)`. Use the decoding
helpers from `xlogreader.h` to extract data and buffer references.

```c
static void
my_redo(XLogReaderState *record)
{
    uint8       info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
    xl_my_op   *xlrec = (xl_my_op *) XLogRecGetData(record);
    Buffer      buf;
    XLogRedoAction action;

    switch (info)
    {
        case XLOG_MY_OP:
            action = XLogReadBufferForRedo(record, 0, &buf);
            if (action == BLK_NEEDS_REDO)
            {
                Page page = BufferGetPage(buf);
                /* apply the change exactly as the writer did */
                apply_change(page, xlrec);
                PageSetLSN(page, record->EndRecPtr);
                MarkBufferDirty(buf);
            }
            if (BufferIsValid(buf))
                UnlockReleaseBuffer(buf);
            break;
        default:
            elog(PANIC, "my_redo: unknown op code %u", info);
    }
}
```

`XLogReadBufferForRedo` return values you will hit
[verified-by-code `source/src/include/access/xlogutils.h:74-78`]:

| return | meaning | what to do |
|---|---|---|
| `BLK_NEEDS_REDO` | page LSN < record LSN; apply the change | apply, `PageSetLSN(page, record->EndRecPtr)`, `MarkBufferDirty` |
| `BLK_DONE` | page LSN ≥ record LSN; already applied | skip body, but still `UnlockReleaseBuffer` if `BufferIsValid` |
| `BLK_RESTORED` | FPI was just applied | skip body, page already at correct LSN |
| `BLK_NOTFOUND` | block doesn't exist (e.g. truncated) | nothing to do; buffer is invalid |

Use `record->EndRecPtr` (end of the record being replayed) for
`PageSetLSN` so the page LSN advances *past* the record once it's fully
applied — using the start LSN would leave the page LSN equal to the
record's start and falsely re-admit `BLK_NEEDS_REDO` on a re-scan that
restarts exactly at this record.

Hard rules for redo:
- **Idempotent**: must produce the right result regardless of how many
  times it's replayed, by virtue of the LSN check (`BLK_NEEDS_REDO` is
  only returned when the page's LSN is older than the record's).
- **No new WAL**: redo never emits WAL.
- **Lock order matters when multi-page**: if a record touches several
  buffers, acquire them in the same order the writer did, and don't
  release until all modifications are done — Hot Standby readers must
  not see intermediate states. [from-README `transam/README:614-619`]
- **Only the Startup process modifies pages during recovery**, so
  `PageSetLSN` without exclusive lock is fine there — but not from a
  hot-standby reader. [from-README `transam/README:620-626`]
- **No `MarkBufferDirtyHint`** in redo — there is no WAL to emit during
  recovery. [from-README `transam/README:638-642`]

### 6. Write `rm_desc` and `rm_identify`

Format conventions documented in
`source/src/backend/access/rmgrdesc/README:17-61`:

- JSON-style key/value, no top-level `{}`.
- Comma + space between fields. Spaces around colons.
- Length of an array appears *before* the array.
- Nested objects use `{}`. Use `rmgrdesc_utils.c` helpers for arrays.

Example:

```c
static void
my_desc(StringInfo buf, XLogReaderState *record)
{
    xl_my_op *xlrec = (xl_my_op *) XLogRecGetData(record);

    appendStringInfo(buf, "off: %u, flags: 0x%02X",
                     xlrec->offnum, xlrec->flags);
}

static const char *
my_identify(uint8 info)
{
    switch (info & ~XLR_INFO_MASK)
    {
        case XLOG_MY_OP: return "MY_OP";
        default:         return NULL;
    }
}
```

These show up in `pg_waldump` output and in `pg_get_wal_resource_managers()`.

### 7. Write `rm_mask` (for wal_consistency_checking)

`wal_consistency_checking` re-applies the record on a copy of the original
page and compares to the in-memory page. Some bits (hint bits, free-space
counters) legitimately differ between the two. `rm_mask` zeroes out those
bits on a page copy before comparison. Look at any existing
`*_mask` in `src/backend/access/*` for examples (e.g. `heap_mask`).

## Generic WAL — short form

```c
GenericXLogState *state = GenericXLogStart(rel);
Page  copy = GenericXLogRegisterBuffer(state, buf, GENERIC_XLOG_FULL_IMAGE);
modify_page(copy);          /* in the WAL workspace, not the real buffer */
XLogRecPtr lsn = GenericXLogFinish(state);
```

`GenericXLogFinish` computes the delta between the original and modified
page images, emits a `Generic` WAL record, and updates the real buffer.
Redo applies the delta. No custom redo function needed. [verified-by-code
`source/src/backend/access/transam/generic_xlog.c:1-44`]

Caveats:
- Up to `MAX_GENERIC_XLOG_PAGES` = `XLR_NORMAL_MAX_BLOCK_ID` = **4**
  pages per call. [verified-by-code
  `source/src/include/access/generic_xlog.h:23`,
  `source/src/include/access/xloginsert.h:28`]
- One transaction may emit many Generic records.
- Buffers are dirtied for you on `GenericXLogFinish`.

## Hint bits — when *not* to write WAL

Some changes are recoverable from authoritative state and can be skipped:
the canonical case is setting `HEAP_XMIN_COMMITTED` on a tuple after
checking pg_xact. Rules: [from-README `transam/README:629-665`]

- Use `MarkBufferDirtyHint()`, never `MarkBufferDirty()`.
- If the buffer is clean **and** (`data_checksums` or `wal_log_hints` is on),
  `MarkBufferDirtyHint()` itself emits `XLOG_FPI_FOR_HINT` to protect the
  page from torn writes.
- Recovery skips hint-only writes (no WAL is emitted during recovery anyway).

Replacing a `MarkBufferDirty()` call with `MarkBufferDirtyHint()` to "save
WAL" is only correct if the change is genuinely re-derivable. Otherwise
you've introduced silent corruption under torn-page conditions.

## Two-phase rmgr is special — skip unless you're rewriting twophase.c

`twophase.c` records PREPARE/COMMIT-PREPARED/ROLLBACK-PREPARED via its own
record types in the `Transaction` rmgr, and also stores per-prepared-xact
state files in `pg_twophase/` for crash recovery. You almost never want to
emit these directly. [from-comment
`source/src/backend/access/transam/twophase.c:12-19`]

## Pre-commit checklist for any WAL-touching patch

- [ ] All page modifications inside `START_CRIT_SECTION` / `END_CRIT_SECTION`.
- [ ] `MarkBufferDirty()` before `XLogInsert()`.
- [ ] `PageSetLSN()` after `XLogInsert()` on every modified buffer.
- [ ] Every modified buffer is `XLogRegisterBuffer`'d (else torn-page hazard).
- [ ] If using `REGBUF_NO_IMAGE`, an explicit comment justifies why.
- [ ] Multi-page records: writer and redo use the same buffer lock order.
- [ ] Redo function is idempotent and only modifies pages when
      `XLogReadBufferForRedo` returns `BLK_NEEDS_REDO`.
- [ ] **Writer ↔ redo symmetry**: for every field the writer reads from
      the in-memory state, the corresponding bytes are written into the
      record via `XLogRegisterData` / `XLogRegisterBufData` and read back
      via `XLogRecGetData` / `XLogRecGetBlockData` in redo. This bug
      class survives `wal_consistency_checking=all` (the writer never
      produces a diverging on-disk page) — only crash recovery exposes it.
- [ ] `rm_desc` and `rm_identify` implemented (and follow the
      `rmgrdesc/README` formatting).
- [ ] `rm_mask` implemented if the record can leave hint-bit-like
      differences.
- [ ] If running under `wal_level=minimal`, the access method either
      respects `RelationNeedsWAL()` or implements one of the two
      exception protocols (`FlushRelationBuffers + smgrimmedsync`, or
      unconditional WAL). [from-README `transam/README:746-775`]
- [ ] Tested under `wal_consistency_checking = 'all'` (or your rmgr name).
- [ ] Tested under `wal_compression` ∈ {off, pglz, lz4, zstd} if
      applicable.

## Useful greps for finding examples

- All redo entry points: `grep -RIn '_redo(' src/backend/access`
- All `rm_mask` implementations: `grep -RIn '_mask(' src/backend/access`
- `XLogInsert` callers (writer-side patterns):
  `grep -RIn 'XLogInsert(' src/backend/access | head`
- Existing custom-rmgr extensions (contrib): `ls contrib/` and look at
  `bloom/` — it uses Generic WAL, not a custom rmgr, but is a good
  minimal-extension example.

## Open questions to verify on first real use

- `rm_decode` is **optional**: `decode.c` checks `if (rmgr.rm_decode != NULL)`
  before invoking it. Set it only if you want your rmgr's records to be
  visible to logical decoding plugins. [verified-by-code
  `source/src/backend/replication/logical/decode.c:116-117`]
- `[unverified]` Behaviour of `XLogRegisterBlock` (the lower-level variant
  of `XLogRegisterBuffer`) — used by recovery-friendly paths that don't
  have a Buffer yet; signature in `xloginsert.h:51-53`.

## Cross-references

- `.claude/skills/locking/SKILL.md` — buffer-content lock ordering around modified pages; WAL-before-data rule.
- `.claude/skills/error-handling/SKILL.md` — `ereport(PANIC, …)` inside redo; redo functions can't `ereport(ERROR)`.
- `.claude/skills/replication-overview/SKILL.md` — walsender / walreceiver consumers of WAL records; logical decoding `rm_decode`.
- `.claude/skills/access-method-apis/SKILL.md` — AM-specific rmgrs for index / table AMs.
- `.claude/skills/catalog-conventions/SKILL.md` — `XLOG_PAGE_MAGIC` bump rules; `pg_control` version bumps.
- `.claude/skills/coding-style/SKILL.md` — backend C style for redo functions, rmgrdesc structures.
- `knowledge/architecture/wal.md` — long-form WAL design discussion.
- `source/src/backend/access/transam/README` — canonical XLOG-record format discussion.