Skip to main content

Table Synchronization Architecture

Overview

Springtail synchronizes tables from a primary PostgreSQL database using a sophisticated architecture that coordinates table copying with an ongoing three-stage replication pipeline (Log Writer → Log Reader → Committer). The PgCopyTable component operates independently to perform bulk table copies while the pipeline ensures consistency by tracking PostgreSQL transaction IDs (XIDs) and using snapshot-based visibility rules to determine which mutations should be applied during and after the copy. This document describes the architecture for synchronizing tables into Springtail, with particular emphasis on:
  • How the replication pipeline is stalled during table copies
  • How PostgreSQL XIDs (pgxids) are tracked and used
  • How snapshot-based visibility ensures correct synchronization points
  • How log replay interacts with table synchronization

Key Components

PgLogMgr

Location: src/pg_log_mgr/pg_log_mgr.{cc,hh} The orchestration hub for Springtail’s replication pipeline. Manages a three-stage processing pipeline:
  1. Log Writer Thread - Connects to PostgreSQL replication stream and writes to log files
  2. Log Reader Thread - Reads log files and parses transactions (via PgLogReader)
  3. Committer - Processes committed transactions and coordinates garbage collection
Additional Coordination Thread:
  • Copy Thread - Coordinates table synchronization requests from Redis queue, using PgCopyTable to perform the actual copying while ensuring consistent snapshots through pipeline coordination
State Machine:
Key Responsibilities:
  • Manages state transitions during table synchronization
  • Coordinates pipeline stalling via STALL messages in the logger queue
  • Executes table copies via PgCopyTable
  • Assigns Springtail XIDs to transactions

PgCopyTable

Location: src/pg_repl/pg_copy_table.{cc,hh} A separate component (not part of the main replication pipeline) that handles the actual table copying using PostgreSQL’s binary COPY protocol. Invoked by PgLogMgr’s Copy Thread and interacts with the pipeline through SyncTracker to ensure consistent snapshots. Key Features:
  • Multi-threaded: Uses 4 worker threads to copy tables in parallel
  • Transaction isolation: Captures PostgreSQL snapshots (xmin/xmax/xips) at copy time
  • Schema preservation: Extracts full table metadata including columns, types, indexes
  • Sync coordination: Marks tables as “in-flight” via SyncTracker
  • Replication messaging: Emits TABLE_SYNC messages via pg_logical_emit_message()
Snapshot Capture:
Copy Process:
  1. Lock table in ACCESS SHARE MODE (prevents schema changes)
  2. Capture snapshot via pg_current_snapshot()
  3. Mark table as in-flight in SyncTracker
  4. Execute COPY table TO STDOUT WITH (FORMAT binary)
  5. Parse binary data and insert into snapshot table
  6. Emit pg_logical_emit_message() with TABLE_SYNC_MSG

SyncTracker

Location: src/pg_log_mgr/sync_tracker.{cc,hh} Tracks table synchronization state and determines when mutations should be skipped during log replay. Acts as the bridge between PgCopyTable and the replication pipeline. Five Primary Data Structures:
  1. _resync_map - Tables where resync was issued but not yet picked up by copy thread
  2. _resync_picked_map - Tables picked for resync at specific XID
  3. _inflight_map - Tables whose COPY is currently in-flight (snapshot metadata stored here)
  4. _table_map - Completed syncs indexed by table_id (persists for skip logic)
  5. _sync_map - Completed syncs indexed by snapshot PG_XID (used for commit detection)
State Transition Flow:
Snapshot-based Skip Logic: The core visibility algorithm in Snapshot::should_skip():
Key Principle: Skip mutations from transactions that committed before the table snapshot was taken, since those rows are already in the copied table data.

PgLogReader

Location: src/pg_log_mgr/pg_log_reader.{cc,hh} Reads replication logs and applies mutations to the write cache. Coordinates with SyncTracker to skip mutations during table syncs. Skip Logic Integration: Every mutation (INSERT/UPDATE/DELETE) checks:
Lines where skip logic is applied:
  • Line 229-235: INSERT/UPDATE/DELETE mutations
  • Line 313-317: TRUNCATE operations
  • Line 487-491: CREATE_TABLE/ALTER_RESYNC DDL
  • Line 501-505: CREATE_INDEX/ALTER_TABLE/DROP_TABLE DDL
Check-Sync-Commit Logic: When a COPY_SYNC message arrives (line 1193-1197) or during transaction commits (line 1351), _check_sync_commit() is called:

Synchronization Flow

Full Table Sync Process

1. Sync Request Initiation

Trigger Points:
  • Startup sync (STATE_STARTUP_SYNC)
  • ALTER TABLE requiring resync
  • Explicit resync via Redis queue
  • Table validation changes (ALTER_RESYNC)
Entry Point: PgLogMgr::_copy_thread() blocks on Redis queue for sync requests

2. Pipeline Stall Initiation

Critical Section: PgLogMgr::_do_table_copies()
Logger Queue STALL Message: The logger queue (_logger_queue) bridges the writer and reader threads. When a STALL message is pushed:
  1. Writer thread continues writing replication data but queues it
  2. Reader thread processes the STALL message in its main loop
  3. Reader sets state to STATE_SYNCING and blocks
  4. Writer receives acknowledgment and begins table copy
Stall Handling in Log Reader: In the reader thread’s processing loop, when a STALL entry is detected:

3. Table Copy Execution

Assign Target XID:
This XID becomes the target_xid for all tables in this sync batch. Execute Copy:
Worker Thread Process (PgCopyTable): Each of 4 worker threads:
  1. Pop table from queue
  2. Connect to PostgreSQL
  3. Lock table: LOCK TABLE schema.table IN ACCESS SHARE MODE
  4. Capture snapshot:
    Returns: (pg_xid, "xmin:xmax:xid,xid,...")
  5. Mark in-flight in SyncTracker:
  6. Create snapshot table:
  7. Execute COPY:
  8. Parse binary data:
    • Verify header: "PGCOPY\n\377\r\n\0"
    • Read tuples in PostgreSQL binary format
    • Insert into snapshot table
  9. Emit sync message:
This message enters the replication stream and will be processed by PgLogReader.

4. Resume Pipeline

After all table copies complete:
The log reader thread unblocks and resumes processing queued replication messages.

How the System is Stalled

Stall Mechanism Architecture

The stall mechanism uses inter-thread coordination via state synchronizer and queue messages: Components:
  1. StateSynchronizer - Thread-safe state machine with atomic test-and-set
  2. Logger Queue - Passes STALL message from writer to reader
  3. Committer Queue - Receives TABLE_SYNC_START to block commits
Detailed Stall Sequence: Why This Works:
  • Writer continues writing - Replication stream isn’t disconnected, just queued
  • Reader blocks safely - No partial transaction application
  • Snapshots are consistent - Captured while mutations are queued
  • No race conditions - State transitions are atomic

Commit Blocking Details

When SyncTracker::block_commits() is called:
The committer receives this message and:
  1. Stops advancing the committed XID
  2. Prevents garbage collection from removing data being copied
  3. Resumes when table swap completes

How PostgreSQL XIDs are Tracked and Used

XID Architecture Overview

Springtail maintains two parallel XID spaces:
  1. PostgreSQL XIDs (pg_xid) - 32-bit transaction IDs from PostgreSQL
    • Subject to wraparound at 2^32
    • Used for snapshot visibility
    • Tracked with epoch for 64-bit uniqueness
  2. Springtail XIDs (xid) - 64-bit global transaction IDs
    • Monotonically increasing
    • Never wrap around
    • Used for internal MVCC and garbage collection
Mapping: PgLogReader maintains _xid_ts_tracker (WalProgressTracker) that maps pg_xid → Springtail xid.

XID Assignment Flow

During Normal Operation:
Each committed PostgreSQL transaction receives a Springtail XID sequentially. During Table Copy:
All tables in a sync batch share the same Springtail XID (target_xid), ensuring atomic visibility. Result Structure:

XID Tracking During Sync

Three Critical XIDs:
  1. Target XID - Springtail XID assigned before copy starts
    • Used for snapshot table creation
    • Used for system table updates
    • Used for swap operation
  2. Copy PG XID - PostgreSQL XID when snapshot was taken
    • Captured via pg_current_xact_id()
    • Stored in PgCopyResult::pg_xid
    • Used in TABLE_SYNC message
  3. Snapshot XIDs - xmin/xmax/xips defining visibility
    • Captured via pg_current_snapshot()
    • Used by Snapshot::should_skip() for mutations
XID Flow Through System:

Snapshot Visibility Rules

PostgreSQL Snapshot Format: "xmin:xmax:xid1,xid2,..." Example: "1000:1010:1002,1005,1008"
  • xmin = 1000 - Oldest transaction still running
  • xmax = 1010 - One past highest completed XID
  • xips = [1002, 1005, 1008] - Transactions in progress between xmin and xmax
Visibility Decision for Mutation with pg_xid:
Wraparound Handling: PostgreSQL XIDs wrap at 2^32. SyncTracker detects wraparound using threshold detection:
Threshold of 2^26 (≈67 million) provides safe margin for detecting wraps.

Table Swap and Commit

When Swap Occurs

The swap happens when all in-progress transactions at snapshot time have committed. Check Logic in SyncTracker::check_commit():
Key Point: When PgLogReader processes a commit for transaction X, it calls check_commit(X). If X matches the pg_xid from a table sync snapshot, all transactions visible to that snapshot have now committed, so it’s safe to swap.

Swap Process

In PgLogReader::_check_sync_commit():
System Table Updates: The swap_sync_table() operation:
  1. Updates springtail.tables with new table root pointer
  2. Updates springtail.namespaces if needed
  3. Creates index entries in springtail.indexes
  4. Invalidates client caches
  5. Returns DDL statements for FDW propagation
Atomicity: The swap is atomic from the perspective of readers - they either see the old table or the new table, never partial state.

Recovery and Error Handling

Log Recovery on Startup

Entry Point: PgLogMgr::startup()

Recovery Phases

Phase 1: Repair Logs Scans replication logs to find last valid committed LSN:
Phase 2: Replay Logs Four-step replay process:
Step 1 - Revert System Tables:
  • Query xid_mgr for last committed XID
  • Revert springtail.tables, springtail.namespaces, etc. to that XID
  • Ensures system catalog consistency
Step 2 - Skip Committed:
  • Read log from last committed LSN
  • Skip transactions already committed before crash
  • Prevents duplicate application
Step 3 - Replay Active:
  • Replay transactions that were in-progress at crash time
  • Use snapshot visibility to skip invalid mutations
Step 4 - Replay Uncommitted:
  • Process messages after last committed transaction
  • Re-apply schema changes and mutations
  • Rebuild write cache state

Handling Copy Failures

Worker Thread Error Handling:
Retry Logic:
  • Transient errors (connection loss, deadlock) → Re-queue table
  • Table not found → Log error, mark as dropped, continue
  • Other errors → Fatal, stop process
Table Dropped During Sync: If a table is dropped after copy starts but before swap:

XID Consistency Across Restarts

XID Manager Persistence: The XidMgr tracks committed XIDs to persistent storage. On restart:
Duplicate Prevention: In commit processing:
This prevents double-application of transactions during recovery.

Message Flow Diagram

Full Sync Lifecycle

Performance Considerations

Parallel Table Copying

  • 4 worker threads copy tables concurrently
  • Each worker maintains independent PostgreSQL connection
  • Results aggregated when all workers complete
  • Thread-safe queue coordinates work distribution

Binary COPY Protocol

  • Efficient binary data transfer (vs. text COPY)
  • No serialization overhead
  • Direct field parsing into internal format
  • Typical throughput: 100K+ rows/second per table

Memory Management

  • Write cache batching: 4MB extent size before flush
  • Snapshot tables: Stored in mutable B-trees, not memory
  • Queue flow control: Memory/file hybrid mode based on watermarks
  • Log archiving: Old logs cleaned based on min active timestamp

Stall Duration Minimization

The pipeline stall only occurs during:
  1. Snapshot capture (~milliseconds)
  2. Table metadata extraction (~seconds)
Bulk data copying happens after pipeline resumes, so replication lag is minimal.

Configuration

Key Constants

From pg_log_mgr.hh:
From pg_copy_table.hh:
From pg_log_reader.hh:

Tuning Recommendations

  1. Increase worker threads for databases with many small tables
  2. Decrease batch size if memory pressure is high
  3. Increase queue size if replication lag spikes during sync
  4. Enable log archiving for regulatory compliance

Summary

Springtail’s table synchronization architecture achieves zero downtime and consistency through:
  1. Snapshot isolation - Captures PostgreSQL snapshots to establish visibility boundaries
  2. Pipeline coordination - Stalls log processing during snapshot capture only
  3. Skip-based replay - Uses PostgreSQL XID visibility rules to skip redundant mutations
  4. Atomic swap - Swaps tables when all in-flight transactions commit
  5. Recovery support - Replays logs correctly after crashes
  6. Parallel execution - Copies multiple tables concurrently for performance
The system ensures that:
  • No mutations are lost during table sync
  • No mutations are double-applied
  • Table data is consistent with a specific point in the replication stream
  • The system can recover from failures at any stage
This architecture enables Springtail to maintain real-time replication while performing bulk table synchronization operations.