Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
This repository was archived by the owner on Sep 14, 2023. It is now read-only.
/zio-sagaPublic archive

Purely Functional Transaction Management In Scala With ZIO

License

NotificationsYou must be signed in to change notification settings

VladKopanev/zio-saga

Repository files navigation

Warning

This project is no longer supported. For implementing real world sagas consider workflow orchestration tools like Temporal that has available libraries for Scala e.g.zio-temporal. Also feel free to fork this repository and modify it for your own needs.

Support Ukrainebadge-scala-ukraine

CICoverageRelease
Build StatusCoverage StatusRelease ArtifactsScala Steward badge

Build your transactions in purely functional way.

zio-saga allows you to compose your requests and compensating actions from Saga pattern in one transactionwithout any boilerplate.

Backed by ZIO it adds a simple abstraction called Saga that takes the responsibility ofproper composition of effects and associated compensating actions.

Getting started

Add zio-saga dependency to yourbuild.sbt:

libraryDependencies += "com.vladkopanev" %% "zio-saga-core" % "0.4.0"

Example of usage:

Consider the following case, we have built our food delivery system in microservices fashion, sowe haveOrder service,Payment service,LoyaltyProgram service, etc.And now we need to implement a closing order method, that collectspayment, assignsloyalty pointsand closes theorder. This method should run transactionally so if e.g.closing order fails we willrollback the state for user andrefund payments,cancel loyalty points.

Applying Saga pattern we need a compensating action for each call to particular microservice, thoseactions needs to be run for each completed request in case some of the requests fails.

Order Saga Flow

Let's think for a moment about how we could implement this pattern without any specific libraries.

The naive implementation could look like this:

deforderSaga():IO[SagaError,Unit]= {for {      _<- collectPayments(2d,2) orElse refundPayments(2d,2)      _<- assignLoyaltyPoints(1d,1) orElse cancelLoyaltyPoints(1d,1)      _<- closeOrder(1) orElse reopenOrder(1)    }yield ()  }

Looks pretty simple and straightforward,orElse function tries to recover the original request if it fails.We have covered every request with a compensating action. But what if last request fails? We know for sure that correspondingcompensationreopenOrder will be executed, but when other compensations would be run? Right, they would not be triggered,because the error would not be propagated higher, thus not triggering compensating actions. That is not what we want, we wantfull rollback logic to be triggered in Saga, whatever error occurred.

Second try, this time let's somehow trigger all compensating actions.

deforderSaga():IO[SagaError,Unit]= {    collectPayments(2d,2).flatMap { _=>        assignLoyaltyPoints(1d,1).flatMap { _=>             closeOrder(1) orElse(reopenOrder(1)*>IO.fail(newSagaError))        } orElse (cancelLoyaltyPoints(1d,1)*>IO.fail(newSagaError))    } orElse(refundPayments(2d,2)*>IO.fail(newSagaError))  }

This works, we trigger all rollback actions by failing after each.But the implementation itself looks awful, we lost expressiveness in the call-back hell, imagine 15 saga steps implemented in such manner,and we also lost the original error that we wanted to show to the user.

You can solve this problems in different ways, but you will encounter a number of difficulties, and your code still wouldlook pretty much the same as we did in our last try.

Achieve a generic solution is not that simple, so you will end uprepeating the same boilerplate code from service to service.

zio-saga tries to address this concerns and provide you with simple syntax to compose your Sagas.

Withzio-saga we could do it like so:

deforderSaga():IO[SagaError,Unit]= {importcom.vladkopanev.zio.saga.Saga._    (for {      _<- collectPayments(2d,2) compensate refundPayments(2d,2)      _<- assignLoyaltyPoints(1d,1) compensate cancelLoyaltyPoints(1d,1)      _<- closeOrder(1) compensate reopenOrder(1)    }yield ()).transact  }

compensate pairs request IO with compensating action IO and returns a newSaga object which then you can compose with otherSagas.To materializeSaga object toZIO when it's complete it is required to usetransact method.

As you can see withzio-saga the process of building your Sagas is greatly simplified comparably to ad-hoc solutions.ZIO-Sagas are composable, boilerplate-free and intuitively understandable for people that aware of Saga pattern.This library let you compose transaction steps both in sequence and in parallel, this feature gives you more powerful controlover transaction execution.

Advanced

Advanced example of working application that stores saga state in DB (journaling) could be foundhereexamples.

Retrying

zio-saga provides you with functions for retrying your compensating actions, so you couldwrite:

collectPayments(2d,2) retryableCompensate (refundPayments(2d,2),Schedule.exponential(1.second))

In this example your Saga will retry compensating actionrefundPayments after exponentiallyincreasing timeouts (based onZIO#retry andZSchedule).

Parallel execution

Saga pattern does not limit transactional requests to run only in sequence.Because of thatzio-saga contains methods for parallel execution of requests.

valflight= bookFlight compensate cancelFlightvalhotel= bookHotel compensate cancelHotelvalbookingSaga= flight zipPar hotel

Note that in this case two compensations would run in sequence, one after another by default.If you need to execute compensations in parallel consider usingSaga#zipWithParAll function, it allows arbitrarycombinations of compensating actions.

Result dependent compensations

Depending on the result of compensable effect you may want to execute specific compensation, for such caseszio-sagacontains specific functions:

  • compensate(compensation: Either[E, A] => Compensator[R, E]) this function makes compensation dependent on the resultof corresponding effect that either fails or succeeds.
  • compensateIfFail(compensation: E => Compensator[R, E]) this function makes compensation dependent only on error typehence compensation will only be triggered if corresponding effect fails.
  • compensateIfSuccess(compensation: A => Compensator[R, E]) this function makes compensation dependent only onsuccessful result type hence compensation can only occur if corresponding effect succeeds.

Notes on compensation action failures

By default, if some compensation action fails no other compensation would run and therefore user has the ability tochoose what to do: stop compensation (by default), retry failed compensation step until it succeeds or proceed to nextcompensation steps ignoring the failure.

Cats Compatible Sagas

cats-saga


[8]ページ先頭

©2009-2025 Movatter.jp