Skip to content

Allow Destination to control confirmed_flush_lsn #621

Description

@no-flaks-given

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions