Skip to content

fix: skip InstantRetry when running inside an already active database transaction#655

Merged
dgafka merged 1 commit intomainfrom
fix-instant-retry-within-transaction
Apr 5, 2026
Merged

fix: skip InstantRetry when running inside an already active database transaction#655
dgafka merged 1 commit intomainfrom
fix-instant-retry-within-transaction

Conversation

@dgafka
Copy link
Copy Markdown
Member

@dgafka dgafka commented Apr 5, 2026

Why is this change proposed?

When withCommandBusRetry is enabled and a command is dispatched via CommandBus from inside an async handler with withTransactionOnAsynchronousEndpoints(true), the InstantRetry interceptor retries inside the already-active transaction. On PostgreSQL, after a SQL error the transaction enters "aborted" state — all subsequent SQL statements fail with "current transaction is aborted, commands ignored until end of transaction block". This makes all retry attempts useless, as they execute inside the same broken transaction.

Resolves #525

Description of Changes

  • Introduced TransactionStatusTracker in core (Ecotone\Modelling\Config\DatabaseTransaction), following the same pattern as RetryStatusTracker
  • DbalTransactionInterceptor marks the tracker when it starts a transaction, unmarks after commit/rollback
  • InstantRetryInterceptor checks the tracker and skips retry if already inside a transaction
  • Added two integration tests proving the behavior

How it works

When withCommandBusRetry runs inside an async endpoint transaction, InstantRetry now detects the active transaction and skips — letting the exception propagate up to the async endpoint level. Users should use withAsynchronousEndpointsRetry for async endpoints, which wraps the transaction and retries on fresh state.

sequenceDiagram
    participant AsyncRetry as InstantRetry (async, -2002)
    participant Txn as DbalTransaction (-2000)
    participant Handler as Async Handler
    participant CB as CommandBus
    participant CmdRetry as InstantRetry (commandBus)
    participant CmdHandler as Command Handler

    AsyncRetry->>Txn: proceed()
    Txn->>Txn: BEGIN TRANSACTION
    Txn->>Handler: proceed()
    Handler->>CB: sendWithRouting()
    CB->>CmdRetry: proceed()
    Note over CmdRetry: isInsideTransaction() = true → skip
    CmdRetry->>CmdHandler: proceed()
    CmdHandler--xCmdRetry: SQL Error
    CmdRetry--xCB: rethrow (no retry)
    CB--xHandler: rethrow
    Handler--xTxn: rethrow
    Txn->>Txn: ROLLBACK
    Txn--xAsyncRetry: rethrow
    AsyncRetry->>Txn: retry with fresh state
    Txn->>Txn: BEGIN TRANSACTION (fresh)
    Txn->>Handler: proceed()
    Handler->>CB: sendWithRouting()
    CB->>CmdRetry: proceed()
    CmdRetry->>CmdHandler: proceed()
    CmdHandler-->>AsyncRetry: success
    Txn->>Txn: COMMIT
Loading

Configuration

// Correct: retry at async endpoint level (wraps transaction)
ServiceConfiguration::createWithDefaults()
    ->withExtensionObjects([
        DbalConfiguration::createWithDefaults()
            ->withTransactionOnAsynchronousEndpoints(true),
        InstantRetryConfiguration::createWithDefaults()
            ->withAsynchronousEndpointsRetry(isEnabled: true, retryTimes: 3),
    ]);

Pull Request Contribution Terms

  • I have read and agree to the contribution terms outlined in CONTRIBUTING.

@dgafka
Copy link
Copy Markdown
Member Author

dgafka commented Apr 5, 2026

Fixes #525

@dgafka dgafka merged commit 5545483 into main Apr 5, 2026
9 checks passed
@dgafka dgafka deleted the fix-instant-retry-within-transaction branch April 5, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

InstantRetry should be skipped if a transaction is already started by an asynchronous endpoint

1 participant