Commit At Most Once v5.6
Commit scope kind: CAMO
Overview
The objective of the Commit At Most Once (CAMO) feature is to prevent the application from committing more than once.
Without CAMO, when a client loses connection after a COMMIT
is submitted, the application might not receive a reply from the server and is therefore unsure whether the transaction committed.
The application can't easily decide between the two options of:
Retrying the transaction with the same data, since this can in some cases cause the data to be entered twice
Not retrying the transaction and risk that the data doesn't get processed at all
Either of those is a critical error with high-value data.
One way to avoid this situation is to make sure that the transaction includes at least one INSERT
into a table with a unique index. However, that depends on the application design and requires application-specific error-handling logic, so it isn't effective in all cases.
The CAMO feature in PGD offers a more general solution and doesn't require an INSERT
. When activated by bdr.commit_scope
, the application receives a message containing the transaction identifier, if already assigned. Otherwise, the first write statement in a transaction sends that information to the client.
If the application sends an explicit COMMIT
, the protocol ensures that the application receives the notification of the transaction identifier before the COMMIT
is sent. If the server doesn't reply to the COMMIT
, the application can handle this error by using the transaction identifier to request the final status of the transaction from another PGD node. If the prior transaction status is known, then the application can safely decide whether to retry the transaction.
CAMO works by creating a pair of partner nodes that are two PGD nodes from the same PGD group. In this operation mode, each node in the pair knows the outcome of any recent transaction executed on the other peer and especially (for our need) knows the outcome of any transaction disconnected during COMMIT
. The node that receives the transactions from the application might be referred to as "origin" and the node that confirms these transactions as "partner." However, there's no difference in the CAMO configuration for the nodes in the CAMO pair. The pair is symmetric.
Warning
CAMO requires changes to the user's application to take advantage of the advanced error handling. Enabling a parameter isn't enough to gain protection. Reference client implementations are provided to customers on request.
Note
The CAMO
commit scope kind is mostly an alias for GROUP COMMIT (transaction_tracking = true, commit_decision = partner)
with an additional DEGRADE ON
clause.
Requirements
To use CAMO, an application must issue an explicit COMMIT
message as a separate request, not as part of a multi-statement request. CAMO can't provide status for transactions issued from procedures or from single-statement transactions that use implicit commits.
Configuration
See theCAMO
commit scope reference for configuration parameters.
Confirmation
Confirmation Level | CAMO handling |
---|---|
received | Not applicable, only uses the default, VISIBLE . |
replicated | Not applicable, only uses the default, VISIBLE . |
durable | Not applicable, only uses the default, VISIBLE . |
visible (default) | Confirms the transaction after all of its changes are flushed to disk and it's visible to concurrent transactions. |
Limitations
See the CAMO section of Limitations.
Failure scenarios
Different failure scenarios occur in different configurations.
Data persistence at receiver side
By default, a PGL writer operates in bdr.synchronous_commit = off
mode when applying transactions from remote nodes. This holds true for CAMO as well, meaning that transactions are confirmed to the origin node possibly before reaching the disk of the CAMO partner. In case of a crash or hardware failure, a confirmed transaction might be unrecoverable on the CAMO partner by itself. This isn't an issue as long as the CAMO origin node remains operational, as it redistributes the transaction once the CAMO partner node recovers.
This in turn means CAMO can protect against a single-node failure, which is correct for local mode as well as or even in combination with remote write.
To cover an outage of both nodes of a CAMO pair, you can use bdr.synchronous_commit = local
to enforce a flush prior to the pre-commit confirmation. This doesn't work with either remote write or local mode and has a performance impact due to I/O requirements on the CAMO partner in the latency sensitive commit path.
Asynchronous mode
When the DEGRADE ON ... TO ASYNC
clause is used in the commit scope, a node detects whether its CAMO partner is ready. If not, it temporarily switches to asynchronous (local) mode. When in this mode, a node commits transactions locally until switching back to CAMO mode.
This doesn't allow COMMIT status to be retrieved, but it does let you choose availability over consistency. This mode can tolerate a single-node failure. In case both nodes of a CAMO pair fail, they might choose incongruent commit decisions to maintain availability, leading to data inconsistencies.
For a CAMO partner to switch to ready, it needs to be connected, and the estimated catchup interval needs to drop below the timeout
value of TO ASYNC
. You can check the current readiness status of a CAMO partner with bdr.is_camo_partner_ready()
, while bdr.node_replication_rates
provides the current estimate of the catchup time.
The switch from CAMO-protected to asynchronous mode is only ever triggered by an actual CAMO transaction. This is true either because the commit exceeds the timeout
value of TO ASYNC
or, in case the CAMO partner is already known, disconnected at the time of commit. This switch is independent of the estimated catchup interval. If the CAMO pair is configured to require the current node to be the write lead of a group as configured through the enable_proxy_routing
node group option. See Commit scopes for syntax. This can prevent a split brain situation due to an isolated node from switching to asynchronous mode. If enable_proxy_routing
isn't set for the CAMO group, the origin node switches to asynchronous mode immediately.
The switch from asynchronous mode to CAMO mode depends on the CAMO partner node, which initiates the connection. The CAMO partner tries to reconnect at least every 30 seconds. After connectivity is reestablished, it might therefore take up to 30 seconds until the CAMO partner connects back to its origin node. Any lag that accumulated on the CAMO partner further delays the switch back to CAMO protected mode.
Unlike during normal CAMO operation, in asynchronous mode there's no added commit overhead. This can be problematic, as it allows the node to continuously process more transactions than the CAMO pair can normally process. Even if the CAMO partner eventually reconnects and applies transactions, its lag only ever increases
in such a situation, preventing reestablishing the CAMO protection. To artificially throttle transactional throughput, PGD provides the bdr.camo_local_mode_delay
setting, which allows you to delay a COMMIT
in local mode by an arbitrary amount of time. We recommend measuring commit times in normal CAMO mode during expected workloads and configuring this delay accordingly. The default is 5 ms, which reflects a asynchronous network and a relatively quick CAMO partner response.
Consider the choice of whether to allow asynchronous mode in view of the architecture and the availability requirements. The following examples provide some detail.
Example
This example considers a setup with two PGD nodes that are the CAMO partner of each other:
For this CAMO commit scope to be legal, the number of nodes in the group must equal exactly 2. Using ALL or ANY 2 on a group consisting of several nodes is an error because the unquantified group expression doesn't resolve to a definite pair of nodes.
With asynchronous mode
If asynchronous mode is allowed, there's no single point of failure. When one node fails:
- The other node can determine the status of all transactions that were disconnected during
COMMIT
on the failed node. - New write transactions are allowed. If the second node also fails, then the outcome of those transactions that were being committed at that time is unknown.
Without asynchronous mode
If asynchronous mode isn't allowed, then each node requires the other node for committing transactions, that is, each node is a single point of failure. When one node fails:
- The other node can determine the status of all transactions that were disconnected during
COMMIT
on the failed node. - New write transactions are prevented until the node recovers.
Application use
Overview and requirements
CAMO relies on a retry loop and specific error handling on the client side. There are three aspects to it:
- The result of a transaction's
COMMIT
needs to be checked and, in case of a temporary error, the client must retry the transaction. - Prior to
COMMIT
, the client must retrieve a global identifier for the transaction, consisting of a node id and a transaction id (both 32-bit integers). - If the current server fails while attempting a
COMMIT
of a transaction, the application must connect to its CAMO partner, retrieve the status of that transaction, and retry depending on the response.
The application must store the global transaction identifier only for the purpose of verifying the transaction status in case of disconnection during COMMIT
. In particular, the application doesn't need another persistence layer. If the application fails, it needs only the information in the database to restart.
To illustrate this, this example shows a retry loop in a CAMO-aware client application, written in a C-like pseudo-code. It expects two DSNs, origin_dsn
and partner_dsn
, providing connection information. These usually are the same DSNs as used for the initial call to bdr.create_node
and can be looked up in bdr.node_summary
, column interface_connstr
.
The process starts connecting to the origin node. Now enter the loop:
Next, start the transaction and begin populating it with changes:
Once you're done, you need to make a record of the local node id and the transaction id. Both are available as parameters.