diff --git a/2pc.html b/2pc.html index 8767cde..3143f2f 100644 --- a/2pc.html +++ b/2pc.html @@ -8,7 +8,7 @@ - + @@ -170,7 +170,7 @@
It’s up to the implementer to decide the level of consistency in the execution of the commit. Transaction failure is also a valid state and can be signaled by raising an exception in the target effect. This will lead to some degree of inconsistency in the overall system state, but that can be an acceptable compromise in some use cases.
-For instance, in our imaginary example the “in-memory cache” could be locked only for a short time to preserve throughput. Because it’s not essential to the journey process, updating it could be skipped in case of delays. On the other hand, if the “internal database” update would still fail despite the lock, this could be signaled by raising an exception. This would conclude the transaction in a failed state, which could be surfaced in the UI to allow for manual remediation.
| Do nothing: reservations were already made | External API | - | | Do nothing: the guarantee charge was already made | Internal service | - | | Update customer details in the internal database | Database | Perform the row update and unlock | | Update the “recent bookings” cache and release the semaphore | In-memory resource | Edit and unlock an in-memory resource | | Do nothing: the reminder notifications were already scheduled | Actor cluster | - | | Do nothing: the bookings log was already updated | File | - |
It’s up to the implementer to decide the level of consistency in the execution of the commit. Transaction failure is also valid and can be signaled by raising an exception in the target effect. Failure will lead to inconsistency in the overall system state, which can be an acceptable compromise in some use cases.
+For instance, in our imaginary example, the “in-memory cache” could be locked only briefly to preserve throughput. Because it’s optional to the journey process, updating it could be skipped in case of delays. On the other hand, if the “internal database” update still fails despite the lock, the commit expression could signal this by raising an exception. The exception would conclude the transaction in a failed state, allowing surfacing in the UI for manual remediation.
This is triggered by the coordinator after at least one branch has voted for abort. The branch is expected to effect the local context to roll back the changes and return a confirmation value. The same flexibility as for prepare and commit applies here.
Some examples of abort operations in the same imaginary process:
@@ -334,10 +303,10 @@The same flexibility applies here as for the commit operation: it’s up to the implementer to decide the level of consistency in the execution of the abort. In this dummy example, let’s suppose traveler reminders have already been sent, a compensation action could be to schedule a new notification inviting the customer to ignore previous messages. On the other hand, failing to cancel the hotel and flight reservations would be a more serious issue and should be surfaced as a failed transaction, to elicit manual remediation.
+The same flexibility applies here as for the commit operation: it’s up to the implementer to decide the level of consistency in the execution of the abort. In this dummy example, let’s suppose traveler reminders have already been sent: a compensation action could be to schedule a new notification inviting the customer to ignore previous messages. On the other hand, failing to cancel the hotel and flight reservations would be a more serious issue and should be considered a failed transaction to elicit manual remediation.
Protocol state throughout the aforementioned phases is tracked by an event-sourced entity, with events representing transitions of the state. The transaction state machine diagram is depicted below. As usual, side-effects are invoked after successful event persistence, and repeated in case of recovery (at least once delivery characteristics).
+Protocol state throughout the phases mentioned above is tracked by an event-sourced entity, with events representing state transitions. The transaction state machine diagram is depicted below. As usual, side-effects are invoked after successful event persistence and repeated in case of recovery (at least once delivery characteristics).
Certain use cases call for different techniques: for instance, the classical challenge of publishing events to a broker atomically with a change in a database is best solved by the simpler transaction outbox pattern or event sourcing.
A coordinator is created with TransferID
as the transaction identifier type, AccountID
as the branch identifier, Transfer
as the query payload (the amount, origin, and destination), and TransferFailure
as the error coproduct.
sourcetransactor.coordinator[TransferID, AccountID, Transfer, TransferFailure](
+sourcetransactor.coordinator[TransferID, AccountID, Transfer, TransferFailure](
"transfer",
{ accountID =>
val account = sharding.entityFor(accountID)
@@ -249,7 +249,7 @@ Transaction
The snippet below shows the logic: the code creates a transfer with two branches, one for the origin account, and the other for the destination account.
-sourcedef transfer(from: AccountID, to: AccountID, amount: PosAmount): F[TransferFailure \/ Unit] =
+sourcedef transfer(from: AccountID, to: AccountID, amount: PosAmount): F[TransferFailure \/ Unit] =
coordinator
.create(TransferID.random, Transfer(from, to, amount), from, to)
.use(_.pollForFinalStatus())
@@ -274,10 +274,12 @@ Branch
An account’s involvement in a transfer is described by TransferBranch
, as exemplified below with the implementation of the prepare
method:
-sourceclass TransferBranch[F[_]: Logger](accountID: AccountID, account: Account[F])(implicit
+sourceclass TransferBranch[F[_]: Logger](accountID: AccountID, account: Account[F])(implicit
+ retryParameters: TransferParameters.BranchRetryParameters,
temporal: Temporal[F]
) extends Branch[F, TransferID, Transfer, TransferFailure] {
import temporal.*
+ private implicit val onErrorRetryParameters: RetryParameters = retryParameters.onError
def prepare(transferID: TransferID, transfer: Transfer): F[Branch.Vote[TransferFailure]] = {
if (accountID === transfer.origin)
@@ -286,44 +288,47 @@
Add the following dependency to your build.sbt
file:
libraryDependencies += "io.github.endless4s" %% "endless-transaction" % "0.0.3"
+libraryDependencies += "io.github.endless4s" %% "endless-transaction" % "0.1.0"
This will pull in the module containing the abstractions. You should add this dependency to the project that contains your business domain logic (typically “domain”).
The Pekko runtime is available in endless-transaction-pekko
and provides PekkoTransactor
, an implementation of Transactor
which is the entry-point to create a transaction coordinator (for Akka, use endless-transaction-akka
).
Compatibility
-Since Pekko/Akka do not allow mixed versions in a project, dependencies of endless-transaction-pekko
(and endless-transaction-akka
respectively) are marked a Provided
. This means that your application libraryDependencies
needs to directly include Pekko or Akka as a direct dependency. The minimal supported Pekko version is 1.0.0, and Akka version is 2.6.20.
+Since Pekko/Akka do not allow mixed versions in a project, dependencies of endless-transaction-pekko
(and endless-transaction-akka
respectively) are marked a Provided
. This means that your application libraryDependencies
needs to directly include Pekko or Akka as a direct dependency. The minimal supported Pekko version is 1.0.2, and Akka version is 2.6.20.
endless-transaction
is designed to be asynchronous and non-blocking and features fine-grained failure and retry semantics. It can be used as a ready-made tool to achieve various degrees of consistency in distributed systems, relying behind the scenes on an actor cluster for scalability and resilience.
The three tenants of the 2PC protocol are prepare, commit, and abort: in endless-transaction, participating branches have full flexibility in defining effectful expressions for the three operations. For certain domains, strong consistency and locking might be required. For others, some degree of inconsistency can be tolerated. It can even be a mix of both in a single transaction. The lifetime of the transaction isn’t limited either, transaction timeout tracking is optional.
Diverse forms of two-phase consensus appear frequently in service-oriented systems, especially when involving complex business workflows and heterogeneous data stores. Its usefulness goes beyond the specific area of strongly consistent, atomic transactions such as XA. Long-running transactions, also known as sagas, typically make use of rollback or undo operations in the abort phase, also called compensating transactions.
-Abstractions in the library are implemented by one of the two available runtimes, Pekko or Akka. Internally, transactions are materialized with a persistent sharded entity implementing the two-phase protocol asynchronously with at least once delivery guarantee.
+Abstractions in the library are implemented by one of the two available runtimes, Pekko or Akka. Internally, transactions are materialized with a persistent sharded entity implementing the two-phase protocol asynchronously with at least once delivery guarantee.
Check out the blog article Two-phase consensus with functional Scala