Problem
Currently, the ApplyLoop auto-advances flush_lsn after write_events() returns successfully. This works well for destinations that durably persist data synchronously in write_events() — by the time the method returns, the data is safe, so it's correct to tell Postgres it can discard that WAL.
But some destinations process data asynchronously. For example, Feldera ingests events via HTTP and processes them through a streaming pipeline before durably persisting results. When write_events() returns, the data has been accepted but not yet durably processed. If the destination crashes before processing completes and etl has already advanced flush_lsn, Postgres has discarded the WAL and those events are lost.
This makes it impossible to build at-least-once delivery guarantees for asynchronous destinations using etl today.
Related: feldera/feldera#5208 — native Postgres CDC connector for Feldera, which would use etl as its foundation.
Proposal
Add an optional method to the Destination trait that lets the destination report its own confirmed flush LSN:
/// Returns the LSN up to which the destination has durably processed data.
///
/// If `None` (the default), etl auto-advances flush_lsn after each
/// successful write_events() call — preserving current behavior.
/// If `Some(lsn)`, etl uses this value instead, letting the destination
/// control when Postgres is told it can discard WAL.
fn confirmed_flush_lsn(&self) -> Option<PgLsn> {
None
}
In the ApplyLoop, when sending standby_status_update, check if the destination provides a confirmed LSN:
let flush_lsn = match destination.confirmed_flush_lsn() {
Some(lsn) => lsn,
None => self.progress.last_flush_lsn, // current behavior
};
Why this approach
- Backwards-compatible: default returns
None, all existing destinations behave identically
- Small change: one new trait method with a default impl, a few lines in the apply loop
- No new dependencies or concepts: just lets the destination participate in LSN tracking
- Correct: the destination knows best when it has durably processed data
Use case
We're building a native Postgres CDC connector for Feldera and want to use etl as the CDC source (snapshot + follow) feeding into Feldera's HTTP ingress. Without this change, we can ship a working connector but cannot offer fault tolerance guarantees — a crash after etl advances flush_lsn but before Feldera processes the data would silently lose events.
With this change, our Destination implementation would query Feldera for its latest checkpointed LSN and report it back, giving us at-least-once delivery end-to-end.
Alternatives considered
- Custom WAL loop: skip etl for the follow phase and use
LogicalReplicationStream directly, calling standby_status_update ourselves. Works but duplicates significant logic that etl already handles well (batching, keepalives, table sync coordination).
- Re-snapshot on crash: our current plan for v1 — use etl as-is with no FT. Functional but not ideal for large tables where re-snapshot is expensive.
Problem
Currently, the
ApplyLoopauto-advancesflush_lsnafterwrite_events()returns successfully. This works well for destinations that durably persist data synchronously inwrite_events()— by the time the method returns, the data is safe, so it's correct to tell Postgres it can discard that WAL.But some destinations process data asynchronously. For example, Feldera ingests events via HTTP and processes them through a streaming pipeline before durably persisting results. When
write_events()returns, the data has been accepted but not yet durably processed. If the destination crashes before processing completes and etl has already advancedflush_lsn, Postgres has discarded the WAL and those events are lost.This makes it impossible to build at-least-once delivery guarantees for asynchronous destinations using etl today.
Related: feldera/feldera#5208 — native Postgres CDC connector for Feldera, which would use etl as its foundation.
Proposal
Add an optional method to the
Destinationtrait that lets the destination report its own confirmed flush LSN:In the
ApplyLoop, when sendingstandby_status_update, check if the destination provides a confirmed LSN:Why this approach
None, all existing destinations behave identicallyUse case
We're building a native Postgres CDC connector for Feldera and want to use etl as the CDC source (snapshot + follow) feeding into Feldera's HTTP ingress. Without this change, we can ship a working connector but cannot offer fault tolerance guarantees — a crash after etl advances
flush_lsnbut before Feldera processes the data would silently lose events.With this change, our Destination implementation would query Feldera for its latest checkpointed LSN and report it back, giving us at-least-once delivery end-to-end.
Alternatives considered
LogicalReplicationStreamdirectly, callingstandby_status_updateourselves. Works but duplicates significant logic that etl already handles well (batching, keepalives, table sync coordination).