Giter Site home page Giter Site logo

vladkopanev / cats-saga Goto Github PK

View Code? Open in Web Editor NEW
122.0 7.0 13.0 380 KB

Purely Functional Transaction Management In Scala With Cats

License: MIT License

Scala 100.00%
fp functional-programming scala saga saga-pattern sagas cats cats-effect concurrency distributed-systems

cats-saga's Introduction

CATS-SAGA

Purely Functional Transaction Management In Scala With Cats

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 Ukraine badge-scala-ukraine

CI Coverage Release
Build Status Coverage Status Release Artifacts Scala Steward badge

Cats Friendly Badge

Disclaimer

This library was inspired by goedverhaal, but it's implementation and semantics differs, it tries to be semantically consistent with zio-saga and provide you with flexible and powerful functions for building Sagas of different complexities.

For whom this library?

This library is designed for those who want to apply Saga pattern on their codebase and to encode long-running transactions. Moreover if you use tagless final encoding this library is a perfect fit. Also consider looking at zio-saga, it's designed specifically for ZIO users. Although you could use this library with ZIO as well.

Getting started

Add zio-saga dependency to your build.sbt:

libraryDependencies += "com.vladkopanev" %% "cats-saga" % "0.3.0"

Example of usage:

Consider the following case, we have built our food delivery system in microservices fashion, so we have Order service, Payment service, LoyaltyProgram service, etc. And now we need to implement a closing order method, that collects payment, assigns loyalty points and closes the order. This method should run transactionally so if e.g. closing order fails we will rollback the state for user and refund payments, cancel loyalty points.

Applying Saga pattern we need a compensating action for each call to particular microservice, those actions 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:

def orderSaga(): IO[Unit] = {
  for {
    _ <- collectPayments(2d, 2) handleErrorWith (_ => refundPayments(2d, 2))
    _ <- assignLoyaltyPoints(1d, 1) handleErrorWith (_ => cancelLoyaltyPoints(1d, 1))
    _ <- closeOrder(1) handleErrorWith (_ => reopenOrder(1))
  } yield ()  
}

Looks pretty simple and straightforward, handleErrorWith 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 corresponding compensation reopenOrder 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 want full rollback logic to be triggered in Saga, whatever error occurred.

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

def orderSaga: IO[Unit] = {
  collectPayments(2d, 2).flatMap { _ =>
    assignLoyaltyPoints(1d, 1).flatMap { _ =>
      closeOrder(1) handleErrorWith(e => reopenOrder(1) *> IO.raiseError(e))
    } handleErrorWith (e => cancelLoyaltyPoints(1d, 1)  *> IO.raiseError(e))
  } handleErrorWith(e => refundPayments(2d, 2) *> IO.raiseError(e))  
}

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.

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

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

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

With cats-saga we could do it like so:

def orderSaga(): IO[Unit] = {
  import com.vladkopanev.cats.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 new Saga object which then you can compose with other Sagas. To materialize Saga object to IO when it's complete it is required to use transact method.

Because Saga is effect polymorphic you could use whatever effect type you want in tagless final style:

def orderSaga[F[_]: Concurrent](): F[Unit] = {
  import com.vladkopanev.cats.saga.Saga._
    
  (for {
    _ <- collectPayments(2d, 2) compensate refundPayments(2d, 2)
    _ <- assignLoyaltyPoints(1d, 1) compensate cancelLoyaltyPoints(1d, 1)
    _ <- closeOrder(1) compensate reopenOrder(1)
  } yield ()).transact
}

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

Advanced

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

Retrying

cats-saga provides you with functions for retrying your compensating actions, so you could write:

collectPayments(2d, 2) retryableCompensate (refundPayments(2d, 2), RetryPolicies.exponentialBackoff(1.second))

In this example your Saga will retry compensating action refundPayments after exponentially increasing timeouts (based on cats-retry).

Parallel execution

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

    val flight          = bookFlight compensate cancelFlight
    val hotel           = bookHotel compensate cancelHotel
    val bookingSaga     = 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 using Saga#zipWithParAll function, it allows arbitrary combinations of compensating actions.

Result dependent compensations

Depending on the result of compensable effect you may want to execute specific compensation, for such cases cats-saga contains specific functions:

  • compensate(compensation: Either[E, A] => F[Unit]) this function makes compensation dependent on the result of corresponding effect that either fails or succeeds.
  • compensateIfFail(compensation: E => F[Unit]) this function makes compensation dependent only on error type hence compensation will only be triggered if corresponding effect fails.
  • compensateIfSuccess(compensation: A => F[Unit]) this function makes compensation dependent only on successful 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 to choose what to do: stop compensation (by default), retry failed compensation step until it succeeds or proceed to next compensation steps ignoring the failure.

For ZIO users

zio-saga

cats-saga's People

Contributors

scala-steward avatar vladkopanev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

cats-saga's Issues

Cats Effect 3 support

Hi! Just wondering if you have plans to support cats effect 3? I can attempt to find some time to assist if necessary :)

Consider providing a non-concurrent Saga

Having support for parallel execution is great! However it comes at the cost of requiring Concurrent for transacting a Saga. Possibly there could be a version of Saga which doesn't need Concurrent and could support types like doobie's ConnectionIO.

Cross publish for Scala 3

Hi! I was wondering if there are any plans to cross publish cats-saga for Scala 3 or if there is any major road-blocker.
I could happily spare some time if you need help.

Thanks for this library and your efforts!

Support for non-Throwable domain-level errors

I've been trying Saga out and really appreciating it so far.

But I noticed one area where my design style doesn't fit with the assumptions in Saga.

Saga assumes that errors are Throwables. I tend towards:

  • Errors that can be reasonably anticipated (domain layer errors) are modeled in the payload F[Either[E, A]]
  • Unexpected errors are modeled via Throwable, such as network layer problems.

Right now, there seems no way to trigger compensating actions without raising a Throwable.

I would like to examine the payload data and trigger compensation based on that. In my specific example, I'm trying to fulfill an order, but if any of the order components is OutOfStock (a domain layer error) I want to cancel the whole order and put all items back in inventory (a compensation that should also happen on unexpected error).

I looked at the design and concluded it might need another case in the Saga algebra, eg:

private case class EitherStep[F[_], A, E](action: F[Either[E, A]], compensate: E => F[Unit]) extends Saga[F, Either[E, A]]

WDYT?

Implement `cats.Parallel`

This should be pretty straightforward, as parZip assumingely already forms a valid Applicative :)

Support custom Saga interpreters

Supporting custom interpreters would allow user to create their own specialized versions of sagas, e.g. for custom error handling (non-Throwable).
This should also hide more logic inside interpreters so the data layer will be unaware of it.
The change will possibly remove some design flaws that were introduced in the early stages of library implementation like Concurrent instances passing through the saga objects.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.