Real time · docs/streams/prepared-transactions.md

Prepared and two-phase transactions

Prepared-transaction delivery is an opt-in Streams preview feature. The default PreparedTransactionMode.Fail behavior rejects every two-phase pgoutput message before changing assembler state. Enable PreparedTransactio…

3 min read536 wordsSource synchronized
View source on GitHub
ON THIS PAGE

Prepared and two-phase transactions

Prepared-transaction delivery is an opt-in Streams preview feature. The default PreparedTransactionMode.Fail behavior rejects every two-phase pgoutput message before changing assembler state. Enable PreparedTransactionMode.Stage only when the destination can durably stage source changes without making them visible.

var assembly = new TransactionAssemblyOptions
{
    PreparedTransactionMode = PreparedTransactionMode.Stage,
};

PostgreSqlConsistentSnapshotSource automatically selects pgoutput protocol 3 and enables the PostgreSQL two_phase option when staging is enabled. Code that manually composes a replication stream must configure both the replication request and decoder consistently:

var source = replication.StartReplicationAsync(
    new BlueTuskPgOutputReplicationOptions
    {
        SlotName = slotName,
        PublicationNames = [publicationName],
        ProtocolVersion = 3,
        StreamingMode = BlueTuskLogicalStreamingMode.On,
        TwoPhase = true,
    }).DecodePgOutputAsync(
    new BlueTuskPgOutputDecoderOptions
    {
        ProtocolVersion = 3,
        StreamingMode = BlueTuskPgOutputStreamingMode.On,
        TwoPhase = true,
    });

PostgreSQL can emit a two-phase transaction as one ordinary committed transaction, including its changes, when logical decoding did not process that transaction at PREPARE TRANSACTION time. This can occur while a consumer is starting or catching up. It is PostgreSQL’s documented fallback and does not lose changes: consumers must always handle ordinary committed deliveries in addition to the staged lifecycle below. A workflow that must observe a staged delivery can first emit and consume a non-transactional logical message as a stream-readiness barrier before it begins the prepared transaction.

Lifecycle deliveries

Streams does not keep an acknowledged prepared transaction only in process memory. It exposes three ordered delivery states instead:

ChangeTransaction.Outcome Changes Required destination action
Prepared Complete source transaction Durably stage all changes under the source identity, transaction ID, and GlobalTransactionId; do not expose them.
Committed with IsTwoPhase == true Empty Atomically make the corresponding staged changes visible and record the final lifecycle delivery.
RolledBack Empty Atomically discard the corresponding staged changes and record the final lifecycle delivery.

Ordinary commits and synthetic logical-message transactions have Outcome == Committed, a null GlobalTransactionId, and IsTwoPhase == false. Prepared, commit-prepared, and rollback-prepared deliveries preserve the PostgreSQL transaction ID and global transaction ID. Streamed prepared transactions use the same bounded memory and disk-spool limits as ordinary streamed transactions.

Acknowledgement means the lifecycle action is durable. For Prepared, the consumer must finish durable staging before acknowledging. For the two final states, it must atomically finalize or discard the staged state before acknowledging. The normal destination → checkpoint → replication-feedback ordering then applies independently to every lifecycle delivery.

Crashes can redeliver any state, so all three actions must be idempotent. A destination should retain a compact final-state tombstone for at least its configured replay/resume window. BlueTusk continues to advertise at-least-once delivery and does not infer cross-system exactly-once behavior from PostgreSQL two-phase commit.

Relay compatibility

Transaction relay envelopes use format 2 for lifecycle outcome and global transaction ID metadata. The decoder continues to accept integrity-checked format 1 envelopes, treating them as ordinary committed transactions. New writers always emit format 2. Unknown future versions and invalid lifecycle combinations fail closed.

The feature is covered by fake ordinary and streamed pgoutput sequences, disk spilling, format 1-to-2 upgrade fixtures, and live PostgreSQL prepared-commit acceptance. It remains preview until the complete Streams 1.0 format-upgrade and 72-hour fault-injected endurance gates pass.