c
复制代码
/*
* xact_redo() -- 事务类WAL记录的重做入口函数
*
* 在恢复(recovery)过程中,每当遇到 RM_XACT_ID 类型的WAL记录时,
* 都会调用此函数。它根据操作码(info)分派到不同的处理逻辑。
*
* 参数:
* record -- XLogReaderState,包含已读取并解码的WAL记录
*/
void
xact_redo(XLogReaderState *record)
{
/*
* 从WAL记录头中提取操作码。
* XLOG_XACT_OPMASK (0x70) 用于屏蔽掉额外标志位(如 XLOG_XACT_HAS_INFO 等),
* 仅保留操作类型部分:
* 0x00 = COMMIT
* 0x10 = PREPARE
* 0x20 = ABORT
* 0x30 = COMMIT_PREPARED
* 0x40 = ABORT_PREPARED
* 0x50 = ASSIGNMENT
* 0x60 = INVALIDATIONS
*/
uint8 info = XLogRecGetInfo(record) & XLOG_XACT_OPMASK;
/*
* 事务类WAL记录不使用backup block机制(FPW全页写)。
* 事务操作修改的是 pg_xact (CLOG)、pg_subtrans 等专用结构,
* 不走标准的缓冲区管理器,因此这里断言不应有关联的数据块引用。
*/
Assert(!XLogRecHasAnyBlockRefs(record));
/* ================================================================
* 分支1: XLOG_XACT_COMMIT --- 普通事务提交
* ================================================================
*
* 这是最常见的事务提交路径。对于单语句事务和显式 COMMIT,
* 主库都会写入此类型的WAL记录。
*/
if (info == XLOG_XACT_COMMIT)
{
xl_xact_commit *xlrec = (xl_xact_commit *) XLogRecGetData(record);
xl_xact_parsed_commit parsed;
/*
* ---- ParseCommitRecord() 详解 ----
* 源码: src/backend/access/rmgrdesc/xactdesc.c:36
*
* 功能: 将WAL中紧凑的可变长提交记录解析为结构化的
* xl_xact_parsed_commit,方便后续各模块使用。
*
* WAL中 xl_xact_commit 是一个变长记录,固定部分只有 xact_time,
* 后面根据 xinfo 标志位依次追加多个可选段:
*
* xl_xact_commit 在WAL中的布局:
* +------------------+
* | xact_time | 固定: 提交时间戳
* +------------------+
* | xl_xact_xinfo | 可选: xinfo标志位 (若 XLOG_XACT_HAS_INFO)
* +------------------+
* | xl_xact_dbinfo | 可选: 数据库ID和表空间ID (若 XACT_XINFO_HAS_DBINFO)
* +------------------+
* | xl_xact_subxacts | 可选: 子事务XID列表 (若 XACT_XINFO_HAS_SUBXACTS)
* | + TransactionId[]|
* +------------------+
* | xl_xact_relfile- | 可选: 需要删除的关系文件列表 (若 XACT_XINFO_HAS_RELFILELOCATORS)
* | locators |
* +------------------+
* | xl_xact_stats_ | 可选: 需要丢弃的统计信息 (若 XACT_XINFO_HAS_DROPPED_STATS)
* | items |
* +------------------+
* | xl_xact_invals | 可选: 缓存失效消息 (若 XACT_XINFO_HAS_INVALS)
* +------------------+
* | xl_xact_twophase | 可选: 两阶段事务XID (若 XACT_XINFO_HAS_TWOPHASE)
* | + GID字符串 | 可选: 全局事务标识符 (若 XACT_XINFO_HAS_GID)
* +------------------+
* | xl_xact_origin | 可选: 复制源信息 (若 XACT_XINFO_HAS_ORIGIN)
* +------------------+
*
* ParseCommitRecord 按顺序逐段解析上述内容,将指针/计数存入 parsed 结构体:
* parsed->xact_time = 提交时间戳
* parsed->xinfo = 标志位集合
* parsed->dbId, tsId = 数据库/表空间OID
* parsed->nsubxacts = 子事务数量
* parsed->subxacts = 子事务XID数组指针 (指向WAL缓冲区)
* parsed->nrels = 需删除的关系文件数
* parsed->xlocators = RelFileLocator数组指针
* parsed->nstats = 需丢弃的统计项数
* parsed->stats = 统计项数组指针
* parsed->nmsgs = 失效消息数量
* parsed->msgs = SharedInvalidationMessage数组指针
* parsed->twophase_xid = 两阶段事务XID (仅2PC)
* parsed->twophase_gid = 全局事务ID字符串 (仅2PC)
* parsed->origin_lsn = 复制源LSN
* parsed->origin_timestamp = 复制源时间戳
*/
ParseCommitRecord(XLogRecGetInfo(record), xlrec, &parsed);
/*
* ---- xact_redo_commit() 详解 ----
* 源码: src/backend/access/transam/xact.c:5953
*
* 功能: 在恢复过程中重放一个事务提交操作。
* 这是事务redo中最关键、最复杂的函数,执行顺序至关重要。
*
* 参数:
* parsed -- 已解析的提交记录
* xid -- 事务ID (普通提交用WAL记录的xid)
* lsn -- WAL记录的结束位置 (EndRecPtr)
* origin_id -- 复制源ID
*
* 执行步骤(顺序严格):
*
* ┌─────────────────────────────────────────────────────────────┐
* │ 步骤1: AdvanceNextFullTransactionIdPastXid(max_xid) │
* │ 确保 ShmemVariableCache->nextXid 超过本记录涉及的所有 │
* │ XID (包括子事务),保证XID分配器不会分配已用过的ID │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤2: TransactionTreeSetCommitTsData(...) │
* │ 设置事务提交时间戳(commit_ts),包括主事务和所有子事务 │
* │ 如果有复制源信息,使用origin_timestamp作为提交时间 │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤3: 标记事务在 pg_xact (CLOG) 中为已提交 │
* │ - standbyState == STANDBY_DISABLED (非Hot Standby): │
* │ TransactionIdCommitTree() -- 同步标记 │
* │ - 否则 (Hot Standby 模式): │
* │ a) RecordKnownAssignedTransactionIds(max_xid) │
* │ 登记新发现的XID到KnownAssignedXids │
* │ b) TransactionIdAsyncCommitTree() -- 异步标记 │
* │ (异步是为了避免提前设hint bits导致崩溃后可见性错误) │
* │ c) ExpireTreeKnownAssignedTransactionIds() │
* │ 从KnownAssignedXids中移除已提交的事务 │
* │ d) ProcessCommittedInvalidationMessages() │
* │ 发送缓存失效消息(保持先失效再释放锁的顺序) │
* │ e) StandbyReleaseLockTree() -- 释放AccessExclusive锁 │
* │ (如果 XACT_XINFO_HAS_AE_LOCKS 标志设置) │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤4: replorigin_advance() (若有复制源信息) │
* │ 推进复制源的重放进度 │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤5: DropRelationFiles() (若有需要删除的关系文件) │
* │ 先 XLogFlush(lsn) 确保WAL已持久化,再删除物理文件 │
* │ (先刷WAL是因为删除不可逆,必须保证可以重放) │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤6: pgstat_execute_transactional_drops() (若有统计项) │
* │ 丢弃统计信息 │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤7: XLogFlush() (若 XACT_COMPLETION_FORCE_SYNC_COMMIT) │
* │ 对于需要同步提交的事务(如 CREATE DATABASE), │
* │ 强制刷WAL以更新 minRecoveryPoint │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤8: XLogRequestWalReceiverReply() │
* │ 如果需要 synchronous_commit = remote_apply 级别反馈, │
* │ 通知 walreceiver 立即发送回复 │
* └─────────────────────────────────────────────────────────────┘
*
* 图示: xact_redo_commit 在Hot Standby模式下的关键执行顺序
*
* AdvanceNextXid
* │
* v
* SetCommitTs ──> RecordKnownAssigned
* │
* v
* AsyncCommitTree (标记CLOG)
* │
* v
* ExpireKnownAssigned (从KnownAssigned移除)
* │
* v
* ProcessInvalidations (发送失效消息)
* │
* v
* ReleaseLocks (释放AE锁)
* │
* v
* DropFiles / DropStats (删除文件/统计)
* │
* v
* [可选] ForceSync / ReplyFeedback
*/
xact_redo_commit(&parsed, XLogRecGetXid(record),
record->EndRecPtr, XLogRecGetOrigin(record));
}
/* ================================================================
* 分支2: XLOG_XACT_COMMIT_PREPARED --- 两阶段提交的最终提交
* ================================================================
*
* 当执行 COMMIT PREPARED 'gid' 时,主库写入此WAL记录。
* 与普通 COMMIT 的区别:
* 1. 使用 parsed.twophase_xid 而非 XLogRecGetXid(record) 作为事务ID,
* 因为执行COMMIT PREPARED的后端可能有不同的xid
* 2. 提交完成后需要清理共享内存中的预备事务条目
*/
else if (info == XLOG_XACT_COMMIT_PREPARED)
{
xl_xact_commit *xlrec = (xl_xact_commit *) XLogRecGetData(record);
xl_xact_parsed_commit parsed;
/* 解析提交记录(同上) */
ParseCommitRecord(XLogRecGetInfo(record), xlrec, &parsed);
/*
* 执行提交重做,注意使用 parsed.twophase_xid
* 这是PREPARE TRANSACTION时原始事务的XID
*/
xact_redo_commit(&parsed, parsed.twophase_xid,
record->EndRecPtr, XLogRecGetOrigin(record));
/*
* ---- PrepareRedoRemove() 详解 ----
* 源码: src/backend/access/transam/twophase.c:2563
*
* 功能: 从共享内存的 TwoPhaseState 中删除对应的预备事务条目(gxact)。
* 如果该预备事务之前被checkpoint写入了磁盘(pg_twophase/目录),
* 还需要删除对应的2PC文件。
*
* 流程:
* 1. 在 TwoPhaseState->prepXacts[] 数组中查找匹配 xid 的 gxact
* 2. 如果找不到,静默返回(恢复中可能重复重放)
* 3. 如果 gxact->ondisk == true,调用 RemoveTwoPhaseFile() 删除磁盘文件
* 4. 调用 RemoveGXact() 从 prepXacts 数组中移除,归还到 freelist
*
* TwoPhaseState 共享内存结构:
* ┌──────────────────────────────────────┐
* │ TwoPhaseState │
* │ ├─ freeGXacts ──> gxact ──> gxact │ 空闲链表
* │ ├─ numPrepXacts = N │
* │ └─ prepXacts[0..N-1] │ 活跃预备事务数组
* │ ├─ [0] ──> gxact {xid, gid, │
* │ │ inredo, ondisk, ...} │
* │ ├─ [1] ──> gxact {...} │
* │ └─ ... │
* └──────────────────────────────────────┘
*
* PrepareRedoRemove 把目标 gxact 从 prepXacts[] 移走,
* 放回 freeGXacts 链表。
*/
LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);
PrepareRedoRemove(parsed.twophase_xid, false);
LWLockRelease(TwoPhaseStateLock);
}
/* ================================================================
* 分支3: XLOG_XACT_ABORT --- 普通事务回滚
* ================================================================
*
* 当事务执行 ROLLBACK 或异常中止时写入此WAL记录。
*/
else if (info == XLOG_XACT_ABORT)
{
xl_xact_abort *xlrec = (xl_xact_abort *) XLogRecGetData(record);
xl_xact_parsed_abort parsed;
/*
* ---- ParseAbortRecord() 详解 ----
* 源码: src/backend/access/rmgrdesc/xactdesc.c:173
*
* 功能: 与 ParseCommitRecord 类似,解析WAL中的回滚记录。
* 回滚记录比提交记录更简单,不包含缓存失效消息(invals)。
*
* xl_xact_abort 在WAL中的布局:
* +------------------+
* | xact_time | 固定: 回滚时间戳
* +------------------+
* | xl_xact_xinfo | 可选: xinfo标志位
* +------------------+
* | xl_xact_dbinfo | 可选: 数据库/表空间ID
* +------------------+
* | xl_xact_subxacts | 可选: 子事务XID列表
* +------------------+
* | xl_xact_relfile- | 可选: 需删除的关系文件 (DDL回滚场景)
* | locators |
* +------------------+
* | xl_xact_stats_ | 可选: 需丢弃的统计信息
* | items |
* +------------------+
* | xl_xact_twophase | 可选: 两阶段事务XID
* +------------------+
* | xl_xact_origin | 可选: 复制源信息
* +------------------+
*
* 注意: 回滚记录没有 XACT_XINFO_HAS_INVALS,因为
* 回滚不需要发送缓存失效消息。
*/
ParseAbortRecord(XLogRecGetInfo(record), xlrec, &parsed);
/*
* ---- xact_redo_abort() 详解 ----
* 源码: src/backend/access/transam/xact.c:6108
*
* 功能: 在恢复过程中重放一个事务回滚操作。
* 与 xact_redo_commit 相似但更简单。
*
* 重要区别: abort 可以是针对子事务及其子孙的,不一定是顶层事务。
* 因此 topxid != xid 是可能的(而 commit 中 topxid 总等于 xid,
* 因为子事务提交不会单独写WAL)。
*
* 执行步骤:
*
* ┌─────────────────────────────────────────────────────────────┐
* │ 步骤1: AdvanceNextFullTransactionIdPastXid(max_xid) │
* │ 与commit相同,确保nextXid足够大 │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤2: 标记事务在 pg_xact (CLOG) 中为已回滚 │
* │ - standbyState == STANDBY_DISABLED: │
* │ TransactionIdAbortTree() -- 直接标记 │
* │ - 否则 (Hot Standby): │
* │ a) RecordKnownAssignedTransactionIds(max_xid) │
* │ b) TransactionIdAbortTree() -- 标记CLOG │
* │ c) ExpireTreeKnownAssignedTransactionIds() │
* │ 从KnownAssignedXids中移除 │
* │ d) StandbyReleaseLockTree() -- 释放AE锁 │
* │ (注意: 回滚不需要发送缓存失效消息) │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤3: replorigin_advance() (若有复制源信息) │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤4: DropRelationFiles() (若有需要删除的关系文件) │
* │ 场景: DDL操作在abort时需要清理已创建的文件 │
* │ 例如 CREATE TABLE 后 ROLLBACK 需要删除已创建的数据文件 │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤5: pgstat_execute_transactional_drops() (若有统计项) │
* └─────────────────────────────────────────────────────────────┘
*
* xact_redo_commit vs xact_redo_abort 对比:
*
* ┌────────────────────┬──────────────┬──────────────┐
* │ 操作 │ redo_commit │ redo_abort │
* ├────────────────────┼──────────────┼──────────────┤
* │ AdvanceNextXid │ ✓ │ ✓ │
* │ SetCommitTs │ ✓ │ ✗ │
* │ MarkCLOG │ Commit │ Abort │
* │ ProcessInvalidation│ ✓ │ ✗ │
* │ ReleaseLocks │ ✓ │ ✓ │
* │ DropFiles │ ✓ │ ✓ │
* │ DropStats │ ✓ │ ✓ │
* │ ForceSyncCommit │ ✓ │ ✗ │
* │ ReplyFeedback │ ✓ │ ✗ │
* └────────────────────┴──────────────┴──────────────┘
*/
xact_redo_abort(&parsed, XLogRecGetXid(record),
record->EndRecPtr, XLogRecGetOrigin(record));
}
/* ================================================================
* 分支4: XLOG_XACT_ABORT_PREPARED --- 两阶段提交的最终回滚
* ================================================================
*
* 当执行 ROLLBACK PREPARED 'gid' 时写入此WAL记录。
* 逻辑与 XLOG_XACT_COMMIT_PREPARED 对称:
* 1. 使用 parsed.twophase_xid 执行回滚重做
* 2. 从共享内存清理预备事务条目
*/
else if (info == XLOG_XACT_ABORT_PREPARED)
{
xl_xact_abort *xlrec = (xl_xact_abort *) XLogRecGetData(record);
xl_xact_parsed_abort parsed;
ParseAbortRecord(XLogRecGetInfo(record), xlrec, &parsed);
xact_redo_abort(&parsed, parsed.twophase_xid,
record->EndRecPtr, XLogRecGetOrigin(record));
/* 删除TwoPhaseState中的gxact条目和/或2PC磁盘文件 */
LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);
PrepareRedoRemove(parsed.twophase_xid, false);
LWLockRelease(TwoPhaseStateLock);
}
/* ================================================================
* 分支5: XLOG_XACT_PREPARE --- 两阶段提交的PREPARE阶段
* ================================================================
*
* 当执行 PREPARE TRANSACTION 'gid' 时写入此WAL记录。
* PREPARE阶段不提交也不回滚事务,而是将事务状态"冻结",
* 等待后续的 COMMIT PREPARED 或 ROLLBACK PREPARED。
*/
else if (info == XLOG_XACT_PREPARE)
{
/*
* ---- PrepareRedoAdd() 详解 ----
* 源码: src/backend/access/transam/twophase.c:2461
*
* 功能: 在恢复过程中,将预备事务的信息存入共享内存的
* TwoPhaseState 结构中。这样后续的 COMMIT PREPARED /
* ROLLBACK PREPARED 重做时能找到对应的条目。
*
* 参数:
* buf -- WAL记录的数据部分(包含TwoPhaseFileHeader + GID等)
* start_lsn -- WAL记录的开始位置 (ReadRecPtr)
* end_lsn -- WAL记录的结束位置 (EndRecPtr)
* origin_id -- 复制源ID
*
* 执行步骤:
*
* ┌─────────────────────────────────────────────────────────────┐
* │ 步骤1: 解析 TwoPhaseFileHeader │
* │ 从buf中获取 hdr->xid, hdr->owner, hdr->prepared_at 等 │
* │ 跳过header获取 GID 字符串 │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤2: 检查重复 │
* │ 如果 start_lsn 有效,检查 pg_twophase/{xid} 文件是否存在│
* │ 如果已存在(checkpoint期间崩溃导致),跳过以避免重复 │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤3: 从 freeGXacts 链表获取一个空闲 gxact │
* │ 填充字段: │
* │ gxact->xid = hdr->xid │
* │ gxact->prepare_start_lsn = start_lsn │
* │ gxact->prepare_end_lsn = end_lsn │
* │ gxact->inredo = true (标记为恢复阶段添加的) │
* │ gxact->ondisk = (start_lsn无效时为true) │
* │ gxact->valid = false │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤4: 插入 prepXacts[] 活跃数组 │
* │ TwoPhaseState->prepXacts[numPrepXacts++] = gxact │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤5: replorigin_advance() (若有复制源) │
* └─────────────────────────────────────────────────────────────┘
*
* 两阶段事务在恢复中的生命周期:
*
* PREPARE WAL记录 ──> PrepareRedoAdd()
* │ │
* │ gxact加入prepXacts[]
* │ │
* v v
* COMMIT/ABORT WAL ──> xact_redo_commit/abort()
* │ │
* │ PrepareRedoRemove()
* │ │
* v v
* gxact从prepXacts[]移除,归还freeGXacts
*/
LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);
PrepareRedoAdd(XLogRecGetData(record),
record->ReadRecPtr,
record->EndRecPtr,
XLogRecGetOrigin(record));
LWLockRelease(TwoPhaseStateLock);
}
/* ================================================================
* 分支6: XLOG_XACT_ASSIGNMENT --- 子事务XID分配
* ================================================================
*
* 当主库在一个事务中累积了 PGPROC_MAX_CACHED_SUBXIDS (64) 个
* 未报告的子事务XID时,写入此WAL记录。
*
* 目的: 告知 Hot Standby 备机哪些子事务属于哪个顶层事务,
* 以限制备机共享内存中 KnownAssignedXids 的大小。
*
* 此记录仅在 standbyState >= STANDBY_INITIALIZED 时才有实际意义。
*/
else if (info == XLOG_XACT_ASSIGNMENT)
{
xl_xact_assignment *xlrec = (xl_xact_assignment *) XLogRecGetData(record);
/*
* xl_xact_assignment 结构:
* xtop -- 顶层事务XID
* nsubxacts -- 子事务数量
* xsub[] -- 子事务XID数组 (变长)
*/
/*
* ---- ProcArrayApplyXidAssignment() 详解 ----
* 源码: src/backend/storage/ipc/procarray.c:1313
*
* 功能: 在备机上处理子事务XID分配记录。
* 将子事务与顶层事务的映射关系记录到 pg_subtrans,
* 并维护 KnownAssignedXids 数据结构。
*
* 执行步骤:
*
* ┌─────────────────────────────────────────────────────────────┐
* │ 步骤1: RecordKnownAssignedTransactionIds(max_xid) │
* │ 确保所有涉及的XID都被记录为"已知已分配" │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤2: SubTransSetParent(subxids[i], topxid) │
* │ 为每个子事务设置父事务ID(写入 pg_subtrans) │
* │ 注意: 恢复中直接用顶层xid,跳过中间层级 │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤3: 如果 standbyState == STANDBY_INITIALIZED,到此结束 │
* │ (KnownAssignedXids尚未完全维护) │
* ├─────────────────────────────────────────────────────────────┤
* │ 步骤4: (standbyState > STANDBY_INITIALIZED) │
* │ 获取 ProcArrayLock (LW_EXCLUSIVE) │
* │ 将这些子事务XID从 KnownAssignedXids 中移除 │
* │ (因为已经通过pg_subtrans关联到顶层事务, │
* │ 不再需要在KnownAssignedXids中单独跟踪) │
* │ 释放 ProcArrayLock │
* └─────────────────────────────────────────────────────────────┘
*
* 为什么需要这个机制:
*
* 主库 备机
* ┌─────────────┐ ┌──────────────────────┐
* │ 顶层事务 T1 │ │ KnownAssignedXids │
* │ ├─ sub1 │ WAL │ [T1, sub1, sub2, ... │
* │ ├─ sub2 │ ────> │ sub64] │
* │ ├─ ... │ │ │
* │ └─ sub64 │ │ 空间有限! 需要清理 │
* └─────────────┘ └──────────────────────┘
* │
* XLOG_XACT_ASSIGNMENT 到达
* │
* v
* ┌──────────────────────┐
* │ pg_subtrans: │
* │ sub1->T1 │
* │ sub2->T1 │
* │ ...->T1 │
* │ KnownAssignedXids: │
* │ [T1] (子事务已移除) │
* └──────────────────────┘
*/
if (standbyState >= STANDBY_INITIALIZED)
ProcArrayApplyXidAssignment(xlrec->xtop,
xlrec->nsubxacts, xlrec->xsub);
}
/* ================================================================
* 分支7: XLOG_XACT_INVALIDATIONS --- 缓存失效消息
* ================================================================
*
* 此类WAL记录包含事务中间产生的缓存失效消息。
*
* 当前在恢复中被忽略。原因:
* 真正需要执行的失效消息已经嵌入在 COMMIT 记录中
* (通过 XACT_XINFO_HAS_INVALS 标志),
* 并在 xact_redo_commit -> ProcessCommittedInvalidationMessages()
* 中处理。
*
* 此WAL记录的存在主要是为了逻辑解码(logical decoding)使用,
* 让解码器能在事务未提交时就看到失效消息。
*/
else if (info == XLOG_XACT_INVALIDATIONS)
{
/*
* XXX 当前忽略此记录,真正重要的失效消息在提交记录中处理。
*/
}
/* ================================================================
* 默认分支: 未知操作码 --- PANIC
* ================================================================
* 如果遇到无法识别的操作码,说明WAL记录已损坏或版本不兼容,
* 必须PANIC停机,不允许继续恢复(否则可能导致数据不一致)。
*/
else
elog(PANIC, "xact_redo: unknown op code %u", info);
}