-
Notifications
You must be signed in to change notification settings - Fork 18
Retry and Resilience Patterns
This page describes the retry strategies and resilience patterns used by phoss-ap for outbound AS4 sending and inbound forwarding, including the per-C3 circuit breaker for outbound communication.
When an AS4 sending attempt fails, the outbound transaction is marked as failed and scheduled for retry by the periodic retry scheduler.
The retry strategy is configurable via Java interfaces and the following properties:
-
retry.sending.max-attempts— Maximum number of attempts before permanent failure -
retry.sending.initial-backoff.ms— Initial backoff interval -
retry.sending.backoff-multiplier— Exponential backoff multiplier -
retry.sending.max-backoff.ms— Maximum backoff cap -
retry.scheduler.interval.ms— How often the scheduler checks for eligible retries
Each failed attempt creates a new outbound_sending_attempt row. The next_retry_dt on the parent outbound_transaction is computed using the backoff strategy and stored in the database, so the scheduler can efficiently query WHERE status = 'failed' AND next_retry_dt <= NOW().
The retry scheduler uses SELECT ... FOR UPDATE SKIP LOCKED when picking up eligible transactions. This ensures that when multiple AP instances run concurrently, each transaction is retried by exactly one instance. No leader election is needed — all instances run the scheduler, and the database coordinates work distribution.
phase4 offers its own internal HTTP retry mechanism (HttpRetrySettings). In phoss-ap this is set to 0 (disabled), because all retry logic is handled at the application level by the phoss-ap retry scheduler. This ensures a clean separation: each phoss-ap retry attempt creates exactly one outbound_sending_attempt row with its own AS4 Message ID.
The circuit breaker (see below) also operates at the application level, gating whether a new sending attempt should be initiated at all.
When forwarding a received document to the Receiver Backend fails, the inbound transaction is marked as forward_failed and scheduled for retry.
Analogous to outbound, configured via:
-
retry.forwarding.max-attempts— Maximum number of attempts before permanent failure -
retry.forwarding.initial-backoff.ms— Initial backoff interval -
retry.forwarding.backoff-multiplier— Exponential backoff multiplier -
retry.forwarding.max-backoff.ms— Maximum backoff cap
Each failed attempt creates a new inbound_forwarding_attempt row. The next_retry_dt on the parent inbound_transaction is updated accordingly.
Same SKIP LOCKED approach as outbound retry — safe for concurrent instances without coordination.
When a remote AP (C3) is down or unreachable, repeatedly attempting to send AS4 messages wastes resources and delays processing of messages to other destinations. A per-C3 circuit breaker prevents this by temporarily blocking send attempts to an unhealthy endpoint, allowing it time to recover.
The circuit breaker is implemented in-memory using a library such as Failsafe and is keyed by the C3 AP endpoint URL (resolved from SMP lookup), not by receiver participant ID, since multiple participants may share the same AP endpoint.
State transitions follow the standard circuit breaker pattern:
CLOSED ──(failure threshold reached)──> OPEN
OPEN ──(timeout expires)──> HALF_OPEN
HALF_OPEN ──(probe succeeds)──> CLOSED
HALF_OPEN ──(probe fails)──> OPEN
- Closed: Normal operation. Failures are counted.
-
Open: All send attempts to this C3 are blocked immediately. The transaction stays in
failedstatus withnext_retry_dtset — it will be retried later when the breaker transitions to half-open. - Half-open: A limited number of probe attempts are allowed through. If they succeed, the breaker closes. If they fail, it reopens.
| Property | Description | Default |
|---|---|---|
circuit-breaker.failure-threshold |
Number of consecutive failures before opening | 5 |
circuit-breaker.open-duration.ms |
How long the breaker stays open before transitioning to half-open |
60000 (1 minute) |
circuit-breaker.half-open-max-attempts |
Number of probe attempts allowed in half-open state | 1 |
The circuit breaker integrates with phase4's existing extension points — no phase4 source code modifications are required. The sendMessageAndCheckForReceipt() method provides two key feedback channels:
- A
Consumer<Phase4Exception>parameter for exception handling - An
IAS4SignalMessageConsumercallback for inspecting the AS4 Signal Message (receipt or error)
Combined with IAS4SenderInterrupt for pre-send gating, the full integration uses three phase4 interfaces:
Called after all message preparation is done but before the actual AS4 HTTP transmission. If the circuit breaker for the target C3 is open, it returns EContinue.BREAK to prevent sending.
// In phoss-ap-core outbound orchestration:
Phase4PeppolSender.builder ()
// ... standard configuration ...
.senderInterrupt (() -> {
if (circuitBreakerManager.isOpen (c3EndpointURL))
return EContinue.BREAK;
return EContinue.CONTINUE;
})When the interrupt blocks sending, the transaction remains in failed status. The retry scheduler will attempt it again after next_retry_dt.
Set via .signalMsgConsumer() on the builder. Called when the synchronous AS4 response (Receipt or Error) is received. The Ebms3SignalMessage can be inspected to determine whether the response is a valid receipt or an AS4 error signal. On success, the circuit breaker records a successful call.
.signalMsgConsumer ((aSignalMsg, aMessageMetadata, aState) -> {
// A receipt was received — record success
if (aSignalMsg.getReceipt () != null)
circuitBreakerManager.recordSuccess (c3EndpointURL);
else {
// Sending failed — record failure in circuit breaker
circuitBreakerManager.recordFailure (c3EndpointURL);
// Also log or store the exception for error_details
LOGGER.error ("AS4 error received from " + c3EndpointURL, ex);
}
})The sendMessageAndCheckForReceipt(Consumer) method accepts a Consumer<Phase4Exception> that is invoked when an exception occurs during sending (e.g., connection refused, timeout, TLS errors). This is the failure signal for the circuit breaker.
final EAS4UserMessageSendResult eResult =
aBuilder.sendMessageAndCheckForReceipt (ex -> {
// Sending failed — record failure in circuit breaker
circuitBreakerManager.recordFailure (c3EndpointURL);
// Also log or store the exception for error_details
LOGGER.error ("AS4 sending failed to " + c3EndpointURL, ex);
});The returned EAS4UserMessageSendResult is then used by phoss-ap to determine the overall outcome and update the outbound_sending_attempt and outbound_transaction accordingly.
Phase4PeppolSender.builder()
│
├─ .senderInterrupt() ← Circuit breaker gate (pre-send)
├─ .signalMsgConsumer() ← Success tracking (receipt received)
│
└─ sendMessageAndCheckForReceipt(exceptionConsumer)
│
├─ HTTP transmission
│ │
│ ├─ On success or failure → IAS4SignalMessageConsumer called
│ │ → recordSuccess()
│ │
│ └─ On failure → Consumer<Phase4Exception> called
│ → recordFailure()
│
└─ Returns EAS4UserMessageSendResult
The circuit breaker state is held purely in-memory (ConcurrentHashMap<String, CircuitBreaker> keyed by C3 endpoint URL). This means:
- State is lost on AP restart — this is intentional, as a restart is itself a recovery event.
- No database persistence overhead for circuit breaker state.
- Thread-safe for concurrent sending to different C3 endpoints.
- In a multi-instance deployment, each instance maintains its own independent circuit breaker state. This is acceptable — each instance protects itself, and different instances may have different views of a C3 endpoint's health.
When the circuit breaker blocks a send attempt via IAS4SenderInterrupt:
- The sending attempt is not made (no
outbound_sending_attemptrow is created). - The
outbound_transactionremains infailedstatus. - The
next_retry_dtis recalculated using the backoff strategy and updated. - The retry scheduler will try again after the backoff period — by which time the breaker may have transitioned to half-open.
This ensures that the circuit breaker and the retry scheduler work together without conflicting.
A similar circuit breaker pattern could be applied to inbound forwarding (per Receiver Backend endpoint). Since the forwarding mode is configured per AP instance (single endpoint), this would be a single circuit breaker rather than a per-endpoint map. This is noted as a future consideration and not part of the initial implementation.
| Concern | Module |
|---|---|
| Circuit breaker manager (in-memory state, Failsafe integration) | phoss-ap-core |
| Retry strategy interface | phoss-ap-api |
| Retry scheduler | phoss-ap-core |
| Circuit breaker configuration properties | phoss-ap-webapp (application.properties) |
The Failsafe library dependency is added to phoss-ap-core only.
It is appreciated if you star the GitHub project if you like it.
Donation link: https://paypal.me/PhilipHelger
- Home
- News and noteworthy
- Running phoss AP
- Architecture Overview
- API Specification
- Configuration Properties
- Code Lists
- Database Design Notes
- Maven Module Structure
- Runtime Extensions
- OpenTelemetry Integration
- Security Considerations
- Peppol Specifics
- Testing Without Peppol Network
- Known Users
- Migrating from phase4-peppol-standalone
- Contributing