Bound memory usage during manual slot sync retries

First seen: 2026-05-15 05:32:43+00:00 · Messages: 8 · Participants: 4

Latest Update

2026-05-18 · claude-opus-4-6

Bound Memory Usage During Manual Slot Sync Retries

Core Problem

PostgreSQL's pg_sync_replication_slots() SQL function, introduced to allow manual synchronization of replication slots on standbys, has an unbounded memory growth problem during its internal retry loop.

Architectural Root Cause

The key architectural difference between the manual sync path and the background slotsync worker is transaction boundary management:

What Leaks

The existing cleanup (list_free_deep(remote_slots)) only frees the List cells and the top-level RemoteSlot structs. It does not free:

  1. Strings owned by RemoteSlot structs — slot name, plugin name, database name (copied via pstrdup or similar)
  2. Per-cycle transient allocationsquote_literal_cstr() strings for query building, TextDatumGetCString() results for invalidation_reason, standalone TupleTableSlot objects from fetch_remote_slots(), and list containers from get_local_synced_slots()

Practical Severity

The author's measurements with 100 failover logical slots show:

The retry backoff (doubling sleep up to 30s via wait_for_slot_activity()) naturally limits the accumulation rate in the quiet case. However, with a "churning slot" that defeats the backoff, the growth becomes much more pronounced (findings pending at thread end).

Proposed Solution

The patch introduces a short-lived memory context (cycle_ctx) that wraps each retry iteration of the manual sync loop. Before each retry, the context is reset, reclaiming all per-cycle allocations deterministically.

Key Design Decisions

  1. Per-retry memory context vs. manual pfree: The author chose a memory context over individual pfree() calls because:

    • It makes the retry-cycle lifetime explicit and self-documenting
    • It avoids maintaining a destructor for every current and future allocation in the path
    • It's future-proof: new allocations added to the sync path automatically get bounded
  2. Slot names copied outside cycle_ctx: The list of slot names needed across retries (for drop_local_obsolete_slots() and similar cross-cycle logic) is explicitly copied into the parent context before the cycle context is reset.

  3. Explicit TupleTableSlot cleanup: MakeSingleTupleTableSlot() results are released with ExecDropSingleTupleTableSlot() before clearing the walreceiver result. While the memory context would eventually reclaim this, the slot holds a reference to the result's tuple descriptor, making explicit release at the ownership boundary semantically clearer.

  4. list_free() in drop_local_obsolete_slots(): Initially proposed for self-containment of the helper, but this was pushed back on by Hou Zhijie based on prior community consensus to rely on memory context management rather than explicit freeing in static helper functions.

Technical Discussion Points

Error Path Cleanup (slotsync_failure_callback)

Shveta Malik raised whether the new memory context needs deletion in slotsync_failure_callback(). Hou Zhijie countered that the context is a child of the transaction's memory context and will be destroyed automatically on transaction abort. The author agreed, avoiding callback state complexity.

Explicit Freeing Philosophy

A recurring PostgreSQL community debate surfaced: whether to explicitly free memory in helper functions or rely on memory context lifetime management. The consensus (per Hou Zhijie, citing prior discussions) is to prefer memory context management over scattered pfree()/list_free() calls, as explicit freeing adds maintenance burden without benefit when a proper context boundary exists.

Amit Kapila's Probe on Practical Impact

Amit Kapila asked for concrete measurements of what "unrelated" memory accumulates per cycle beyond the obvious RemoteSlot strings. This prompted the author to perform detailed benchmarking with pg_log_backend_memory_contexts(), which strengthened the case by showing the growth is real but modest under backoff, and entirely eliminated by the memory context approach.

Implications

This is a correctness-of-resource-management fix rather than a critical bug. The function is designed to potentially wait indefinitely on a lagging or misconfigured standby, making unbounded growth a theoretical concern even if the practical rate is slow. The fix follows established PostgreSQL patterns for bounding memory in long-running operations (similar to how VACUUM, logical decoding, and other iterative operations use resettable memory contexts).