Repository navigation
Add standby SQL sinks: --standby-lock and --stall-timeout (PostgreSQL) - #1013
Conversation
Several sinks can now run against one database with a single writer, so a sink can run on more than one machine and fail over without a human. --standby-lock: before streaming, the sink takes a session advisory lock keyed on the cursors table (one writer per cursors table, whatever the module), and reads its cursor and writes through that same session, so losing the session loses the lock and the ability to write together. Other sinks wait as standbys, without opening a Substreams stream, checking every --standby-lock-poll-interval (default 2s). The first to see the lock released takes over from the stored cursor. A sink whose locked session is gone stops instead of retrying its flush; a connection whose lock state is uncertain is discarded rather than returned to the pool. Standbys serve the metrics endpoint while they wait, with substreams_sink_postgres_writer_lock_held. --stall-timeout: stops the sink when the stream delivers no block or undo for that long once live (blocks held by an undo buffer count, see the new Sinker.LastBlockMessageAt), so a stuck sink cannot keep the lock. On PostgreSQL a flush now honours its context, so the stop cancels a database call in progress and its transaction rolls back; ClickHouse flushes still always run to the end, since its writes are not transactional. The cursor check from the previous commit keeps any other writer from corrupting data. docs/references/sql/standby-sinks.md describes the setup: server TCP keepalives, session connections, failover with a fenced old primary, stalls and network cuts.
271688f to
9e9bb9a
Compare
sduchesneau
left a comment
There was a problem hiding this comment.
You should probably ping the connection every standby-lock-poll-interval so that, when processing large segments (initial loading) or on long stretches that yield no result (slow chain, sparse content...), the active connection doesn't get killed by a proxy or idle_session_timeout. -- this would cause some failover pingpong between instances.
it's the opposite of the idea of stall-timeout, it's when you want to support sparse data and prevent "spurious failovers".
The active sink can go a long time without writing: large segments while catching up, or a slow chain or sparse module with nothing to flush. Its locked session then sits idle, and a proxy's idle timeout or the server's idle_session_timeout can end it. That hands the lock to a standby, the old sink fails its next flush and comes back as a standby, and the sinks take turns failing over on data that is merely sparse. The active sink now runs SELECT 1 on the locked session every --standby-lock-poll-interval. It skips the query while a transaction is open on that session (a running flush keeps it busy anyway, and a query inside an aborted transaction would fail), so writes and the keepalive never interleave. A timed-out query only logs a warning; any other failure means the session and the lock are gone, and the sink stops like it does when a flush finds the session gone. This also makes idle_session_timeout useful as a fence: a frozen sink stops querying, its session ends, and a standby takes over. The standby sinks doc explains both.
The keepalive skipped the writer connection only while a transaction was
open on it. The cursor read at startup runs outside a transaction on that
same connection, and lib/pq refuses a query while another query's rows are
still being read ("there is already a query being processed on this
connection"). A keepalive tick landing in that window failed, and the sink
stopped as if it had lost its session.
The mutex now also covers that read, so the keepalive skips its tick while
the rows are open. A test reads many cursor rows while the keepalive runs in
a tight loop; it fails without the fix.
|
@sduchesneau Good call, thanks. Done in c2aaffa: the active sink pings its locked session every |
|
@aggris2 all good, please resolve conflicts and push, I will approve/merge it then |
|
@sduchesneau conflicts resolved |
Builds on #1007, #1011 and #1012. Until those are merged, this PR also shows their commits.
Today only one sink can safely write to a database, so running the sink on more than one machine needs something outside it to make sure only one runs (start/stop scripts tied to the database failover, for example). This adds a standby mode to the SQL sink (PostgreSQL): several sinks run against one database, one writes, and the others take over by themselves.
--standby-lock: before streaming, the sink takes a session advisory lock keyed on the cursors table (one writer per cursors table, whatever the module). It reads its cursor and does all its writes through that same session, so losing the session loses the lock and the ability to write together. Other sinks wait as standbys, without opening a Substreams stream, checking every--standby-lock-poll-interval(default 2s). The first to see the lock released takes over from the stored cursor.--prometheus-addrendpoint while they wait;substreams_sink_postgres_writer_lock_heldis 1 on the active sink.--stall-timeout: stops the sink when the stream delivers no block or undo for that long once live, so a stuck sink can't keep the lock. Blocks held by an undo buffer count (newSinker.LastBlockMessageAt). On PostgreSQL a flush now honours its context, so the stop cancels a database call in progress and its transaction rolls back. ClickHouse flushes still run to the end, since its writes aren't transactional.The cursor check from #1012 keeps any other writer from corrupting data. The new
docs/references/sql/standby-sinks.mdcovers the setup: server TCP keepalives so a dead client's session (and lock) is released, session connections (not a transaction-mode pooler), failover relying on the old primary being fenced, stalls and network cuts.Tests, with real Postgres (
PG_DSN):TestWriterLock_Postgres: a standby waits, takes over when the active sink's session is terminated, the old writer can't write anymore, a sink of another module waits too, a standby stops when told to, closing releases the lock;TestDiscardEndsLockedSession_PostgresandTestIsCancellation;TestSinker_StallTimeout, and a cursor conflict while stopping is still reported.End to end, over 20k mainnet blocks with a flush per block:
--standby-lock, killing the active one, cutting its database session, then killing the standby;Final tables were identical to a clean run in every case, and the standby took over within about a second.