Replication Architecture¶
9.1 Physical Streaming Replication¶
REFRESH MATERIALIZED VIEW is a heap rewrite. That write is WAL-logged and carried to all physical standbys automatically. No local refresh action on a standby is needed or permitted.
Triggers do not fire during WAL replay. pg_relay on a standby sits idle (its queue poll finds no eligible rows — logical replication is the only way data would reach the queue on a standby, which is read-only anyway). After promotion the node becomes writable, local DML begins firing triggers, and the already-connected pg_relay binary starts dispatching without any restart.
The pg_is_in_recovery() guard at step 0 of _process_pending() is the belt-and-suspenders safety: if a queue row somehow existed on a standby (manually inserted, or replicated despite the node-local design), the function returns immediately before attempting any write.
9.2 Multi-Write-Master (Logical Replication / pgEdge / Spock)¶
Why all operational tables must be node-local:
mv_statecontainslast_refresh_completed, which drivescooldown_untilandrefresh_due_at. If node A's refresh state replicates to node B, node B will see a false "already refreshed recently" condition and suppress its own refresh. Each node's matview heap is independent — node A's refresh does not update node B's matview.mv_eventsandmv_refresh_logusebigserialprimary keys which diverge across nodes, producing conflicts on merge.materialised_viewscontainsmv_oid, which is assigned independently by each PostgreSQL instance at DDL execution time. An OID from node A is meaningless (or refers to a different object) on node B.
Replication exclusion list: materialised_views, watch_tables, mv_state, mv_events, mv_refresh_log, mv_audit_history, sync_log, mv_chains — all must be excluded from every replication set.
mv_truncate_signals must be included.
fire_on_replica = true mode:
Row-level ENABLE ALWAYS triggers fire during logical apply (the apply worker runs with session_replication_role = 'replica'; only ENABLE ALWAYS triggers fire). This means every node refreshes its matview in response to both local writes and replicated writes.
The _on_truncate_signal() trigger guards against the origin-node double-fire with:
session_replication_role is 'origin' — the trigger is a no-op. On replica nodes it is 'replica' — the trigger fires.
pg_relay v1.1 multi-master modes and node-restricted channels:
pg_relay v1.1 runs its Processor in one of three modes: --mode=single (one writable primary; the default), --mode=multi-node (Spock active-active, each node dispatches its own rows, a dead node's rows are adopted), and --mode=leader (Spock active-active, the lowest live write-master dispatches all unrestricted rows). In multi-node/leader the pgrelay.queue and pgrelay.log tables are replicated and every node must set a distinct pg_relay.node_id GUC (enforced at Processor startup).
Location-independent dispatch is wrong for pg_auto_mv: the same (channel, payload) on two nodes means two different pieces of physical work (each node's own matview heap). pg_auto_mv 1.1 therefore registers both of its channels with node_restricted = true, and pg_relay guarantees:
- Every pg_auto_mv queue row is stamped with the creating node's
pg_relay.node_id(pgrelay.queue.run_in_node) by aBEFORE INSERTtrigger — coveringpgrelay.notify()calls, retry rows, and pg_auto_mv's direct scheduled-refresh inserts alike. - Only that node's Processor ever dispatches the row, in every mode. In
leadermode non-leader nodes keep draining their own pinned refresh rows while the leader handles everything else. - The row is never adopted or reassigned — a dead node's scheduled-refresh chain rows wait for it to return, keeping
mv_state.scheduled_queue_idvalid. p_deduplicatesuppresses only same-node pending rows, so node A's replicated pending row can never swallow node B's deferred or retry notify.
In --mode=single (pg_relay.node_id unset ⇒ node 0) every restricted row is stamped run_in_node = 0 and behaviour is identical to pg_auto_mv 1.0. On a physical standby nothing changes either — the pg_is_in_recovery() guard still applies.
Multi-master deployment procedure:
- Install pg_relay >= 1.1 on every node; set a distinct
pg_relay.node_idper node (ALTER SYSTEM SET pg_relay.node_id = '<n>'; SELECT pg_reload_conf();) CREATE EXTENSION pg_auto_mvon every node (afterpg_relay— Spock does not replicate DDL)SELECT pgauto_mv.register_mv(... p_fire_on_replica => true, p_auto_mv_id => '<shared-uuid>')on every node — the same UUID pinned across all nodes- Replicate
pgrelay.queueandpgrelay.log(a pg_relay v1.1 multi-master requirement); exclude all pgauto_mv operational tables from the replication set; includepgauto_mv.mv_truncate_signals - One
pg_relaybinary per node, connected to its local database only (Unix socket or localhost), started with--mode=multi-nodeor--mode=leader
Decommissioning a node: a node-restricted queue row pinned to a node that never returns will never run and never expire. After permanently retiring a node, discard its pending pg_auto_mv rows manually — see "Decommissioned nodes" in pg_relay's MULTI_MASTER_DEPLOYMENT.md.