Full Code of VladKopanev/zio-saga for AI

master aa12aaca59ce cached
30 files
70.9 KB
21.2k tokens
1 requests
Download .txt
Repository: VladKopanev/zio-saga
Branch: master
Commit: aa12aaca59ce
Files: 30
Total size: 70.9 KB

Directory structure:
gitextract_1zrhpsqz/

├── .github/
│   └── workflows/
│       └── scala.yml
├── .gitignore
├── .scalafmt.conf
├── .travis.yml
├── LICENSE
├── README.md
├── build.sbt
├── core/
│   └── src/
│       ├── main/
│       │   └── scala/
│       │       └── com/
│       │           └── vladkopanev/
│       │               └── zio/
│       │                   └── saga/
│       │                       └── Saga.scala
│       └── test/
│           └── scala/
│               └── com/
│                   └── vladkopanev/
│                       └── zio/
│                           └── saga/
│                               └── SagaSpec.scala
├── examples/
│   ├── README.md
│   └── src/
│       └── main/
│           ├── resources/
│           │   ├── application.conf
│           │   ├── logback.xml
│           │   └── sql/
│           │       └── saga.ddl
│           └── scala/
│               └── com/
│                   └── vladkopanev/
│                       └── zio/
│                           └── saga/
│                               └── example/
│                                   ├── OrderSagaCoordinator.scala
│                                   ├── SagaApp.scala
│                                   ├── client/
│                                   │   ├── LoyaltyPointsServiceClient.scala
│                                   │   ├── OrderServiceClient.scala
│                                   │   ├── PaymentServiceClient.scala
│                                   │   └── client.scala
│                                   ├── dao/
│                                   │   └── SagaLogDao.scala
│                                   ├── endpoint/
│                                   │   └── SagaEndpoint.scala
│                                   ├── example.scala
│                                   └── model/
│                                       ├── OrderInfo.scala
│                                       ├── OrderSagaError.scala
│                                       ├── SagaInfo.scala
│                                       └── SagaStep.scala
├── project/
│   ├── Versions.scala
│   ├── build.properties
│   └── plugins.sbt
└── travis/
    └── secrets.tar.enc

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/scala.yml
================================================
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

name: Scala CI

on:
  workflow_dispatch:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: '11'
        distribution: 'temurin'
        cache: 'sbt'
    - name: Run tests
      run: sbt test
      # Optional: This step uploads information to the GitHub dependency graph and unblocking Dependabot alerts for the repository
    - name: Upload dependency graph
      uses: scalacenter/sbt-dependency-submission@ab086b50c947c9774b70f39fc7f6e20ca2706c91


================================================
FILE: .gitignore
================================================
project/zecret
project/travis-deploy-key
project/secrets.tar.xz
target
test-output/
.sbtopts
project/.sbt
test-output/
local.*
.idea

# if you are here to add your IDE's files please read this instead:
# https://stackoverflow.com/questions/7335420/global-git-ignore#22885996
website/node_modules
website/build
website/i18n/en.json


================================================
FILE: .scalafmt.conf
================================================
version = "3.0.7"

maxColumn = 120
align {
  preset = more
  tokens = [{code = "->"}, {code = "<-"}, {code = "=>", owners = [{
      regex = "Case"
  }]}, {code = "%"}, {code = "%%"}]
  openParenCallSite = false
  openParenDefnSite = false
}

indent {
  significant = 2
  defnSite = 2
  extendSite = 2
}

optIn.annotationNewlines = true

lineEndings = preserve

spaces {
  inImportCurlyBraces = true
}

danglingParentheses.preset = true

docstrings.style = Asterisk

includeCurlyBraceInSelectChains = false

assumeStandardLibraryStripMargin = true

rewrite.rules = [SortImports, RedundantBraces]


================================================
FILE: .travis.yml
================================================
sudo: false

language: scala

matrix:
  include:
    # Scala 2.12, JVM
    - scala: 2.12.17
      jdk: openjdk8
    - scala: 2.12.17
      jdk: openjdk11
    # Scala 2.13, JVM
    - scala: 2.13.9
      jdk: openjdk8
    - scala: 2.13.9
      jdk: openjdk11
    # Scala 3.0, JVM
    - scala: 3.1.3
      jdk: openjdk8
    - scala: 3.1.3
      jdk: openjdk11

branches:
  only:
    - master
    -  /^v.*/

script:
- sbt clean coverage test coverageReport coverageOff

before_install:
- if [ $TRAVIS_PULL_REQUEST = 'false' ]; then
  openssl aes-256-cbc -K $encrypted_8febf8bb1214_key -iv $encrypted_8febf8bb1214_iv -in travis/secrets.tar.enc -out travis/secrets.tar -d;
  ls travis;
  tar xv -C travis -f travis/secrets.tar;
  fi

cache:
  directories:
    - $HOME/.sbt/1.0/dependency
    - $HOME/.sbt/boot/scala*
    - $HOME/.sbt/launchers
    - $HOME/.rvm
    - website/node_modules

before_cache:
  - find $HOME/.sbt -name "*.lock" -type f -delete
  - find $HOME/.ivy2/cache -name "ivydata-*.properties" -type f -delete
  - rm -rf $HOME/.ivy2/local

env:
  global:
  - secure: e1WmzDGzT3JUwCWWFyLS7IBEHYJUcCcaoip/0gMcdmlhB8g5dAjWGo8Sj3ofylRcyb+0quGZZNkh//ByOSsvig0tyZBOTSUA+R62LcXfkurpxlr21fopXMhJ6CZxU6YxVgYn7QEKbvcct5QCTz8E6trp1CuuwiqMEt5joh6h5D3AESbFz4ZB8EsdMhVDJAiGK+jBaLSaoTnzbhIvXDttl4k+CuRdQ5YGUbrFyYEJsDHj2ms30NQggdvaArqSflCPtZxREDgweuWcBK9HzyZD51+sLEFG/CH0MhcYFAtmFVdD3JeaOaE33au7PXHOpyVKErhqbr7ZhhB9vBfsWitnasZXWqAS6TBWekMp/2GLDFz+QWMOgfgg6t//SvdNFe07biZO5NiJ52l+kbxm+BswA80XXZ6Nrbs2VIjdvasxUUJ9ud1hvwMtMuIrwJcG3Jg0sxOaXdwmLahpn6BIw8nYyOhDDvtr0utbUkhMtNWv23MzWnwU1+BVCcFglH0KFyG30i3cSe6S73CRJv6ILE0xRjXVCGstZAYq0we9lA5NcQtPYQlAtsunTfyd6XR1RFn4cQaCtMVrGwG2XUz5Stq3y0Hd9P+wBaZdstl17dEgjHMdP758N8dsR/8saOt3w4i5ktsS8f9LACvGfW5a09VnQWkowDgKokSF8lmn0CPxDa0=
  - secure: kWcc2SnYmlbZAGUYifr1nb1m1i+iITUcxWELzokSkKFIIc870WFXyvu3VK9pxShiTDehg4shRnp+Ygfynbn44WI4guuzMQLuSlhxt0uDZ6UmLnSOLOpCOfpT6WuVQkiaxoETyNZ7u8bnZqmFSQWfSoH8PWdWBBdON8hD7DsAmvXj6hgIn8jmstq8dgLqqboDH7RGEIpgWIXB2a203Hoe/gYOXtXOm9LyRK1/RCOnGNxkAowNhKVpEvZKjp5P2is7i7bgQEKzagBtj977WnFe6TNJjhB49yrSDGJ2W7Nf0L1BWKtUiTGqeyDuaBSBMxUyzQaMn4zcVHHNMcWogbYPnsACBzmbd3ZfJu8WM6JLtLFIwMtdj9sl7UDc/hin/KlUOfi/FvJZSNYqGiIxj5dwVlmZg9g+oaz/9L4bMOZ9p6PmIYSHOApQg0v6MJMQfeKrlKcpMLA0FcZ0597nTJEy6UOBlMYlVxTgWiCUNDFALsWqOblNgoB1Z9aAQ8RT3GwOaMj4UXFXJbUK9Q2CqdYEi/DFi8Hhjyh0Th3a6cgLIq+18G5OrHASX8odecAFq6J8+eZO+SW+WUJFB6D3loLbM/S436SKB8VH7JeVBlVjPA+HOA6eAfC0mHVupKm0sPTNW4s00rNSQfQvBlmKe8v5sQtAKROu3rJ7rHn1uNAOQOE=
  - secure: ctGTO3DWvc1tG3mGWg+ffb+/Cw8zl6QNjtCzDaSv/KY5jo0a/+hBOMQjjhn+oSvFNhYfV1Ykc+Ym+mdrJRuVvi2QuABvI6gKhT3c9HOt7Al6/4Q1gEnSf0YdEhF/ZRVp4+4CmG9luk/OtHwRBtXOWSS7+b7RKCs5+Homy645oNuB3OJs9N0G3rAQ58riuLTtNTPQcQvNbzULc8CkP056mdGxjMBykp85X2mRqVDaEp0Gg7WaVxJnKIZt5eFJy1dll2xcPyJVojYVRnsObUHXcUgeEa9aNA+Vcwrpy9pL37jR18rExLWkf/zRYCi2fK3C48/rFmIqTg+q8O6h5MlupS5uZeQmLoJzr2l9okZg9FF22ONDNWJc614U2M3VzbDGOJ4Hw5rEpaFECHdZZ9omMRsuGambGMStcrt5GFwLZ5ANJOY+kB2od8C60Bk4kVSUJpFlsGUb3a/kSlvYE+tjlL0rMS2abQcLoEOypILm7vxNPd4NBDXPOX2tRKnKE+bv1gxmoQYmEVNofF8taKxHFd6K1o2H7FmEvNOt27iqXtTrI8TTOudP39raZwjWFeTNvxeIPewjAhbeRX/tB+QwVT1cuEgc/zPwq58K3aYU1dewZn3acepp00qrA8xWL1de+t73yiCDBj0bwKKMRA0BuawO3Al7Pugv1Po/4AmBbNQ=

after_success:
- bash <(curl -s https://codecov.io/bash)
- if [ $TRAVIS_PULL_REQUEST = 'false' ]; then sbt ++$TRAVIS_SCALA_VERSION releaseEarly;
  fi

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019 Kopaniev Vladyslav

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# ZIO-SAGA

> [!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](https://github.com/vitaliihonta/zio-temporal). Also feel free to fork this repository and modify it for your own needs.

[![Support Ukraine](https://img.shields.io/static/v1?label=United24&message=Support%20Ukraine&color=lightgrey&link=https%3A%2F%2Fu24.gov.ua&logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAASwAAADICAYAAABS39xVAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAANKSURBVHhe7dZBThRhFEbRnx1IgvtFiIoxbgemOHLAhAoJ1QyaBahroKxqE%2BMS6iZncPKSbwE3b4yr6W58en4Z148zwC5tjbqabrdgvZ59PS5nn2eAfVobtbbquAXrcBquJ4B9Wht1%2BrQEC9g9wQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8j4F6zL%2BTA%2BHpfxYR0A9mhr1OXzPC5u7g%2Fvv%2F1YLr58B9ilU6Nu7ufx6%2BH88Hs6X9YLsEtbo34%2BvJvH29M4LC9jWZ4Admpt1NqqNVjTGqz5bFkmgJ1aG%2FX2KFhAgWABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVk%2FBes1%2BX4dwDYpbVRa6uOW7Du3p7Hy1YvgF3aGjWN2z9qCgwkg1n6XwAAAABJRU5ErkJggg%3D%3D)](https://u24.gov.ua)
[![badge-scala-ukraine](https://img.shields.io/badge/Scala-Ukraine-EBD038?labelColor=4172CC)](https://t.me/scala_ukraine)
| CI | Coverage | Release |  |
| --- | --- | --- | --- |
| [![Build Status][Badge-Travis]][Link-Travis] | [![Coverage Status][Badge-Codecov]][Link-Codecov] | [![Release Artifacts][Badge-SonatypeReleases]][Link-SonatypeReleases] | [![Scala Steward badge][Badge-ScalaSteward]][Link-ScalaSteward] |

Build your transactions in purely functional way.

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


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

# Getting started

Add zio-saga dependency to your `build.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, 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](./images/diagrams/Order%20Saga%20Flow.jpeg)

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

The naive implementation could look like this:

```scala
def orderSaga(): 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 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.
  
```scala
def orderSaga(): IO[SagaError, Unit] = {
    collectPayments(2d, 2).flatMap { _ = >
        assignLoyaltyPoints(1d, 1).flatMap { _ => 
            closeOrder(1) orElse(reopenOrder(1)  *> IO.fail(new SagaError))
        } orElse (cancelLoyaltyPoints(1d, 1)  *> IO.fail(new SagaError))
    } orElse(refundPayments(2d, 2) *> IO.fail(new SagaError))
  }
```

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 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.

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

With `zio-saga` we could do it like so:

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

As you can see with `zio-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 control 
over transaction execution.

# Advanced

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

### Retrying 
`zio-saga` provides you with functions for retrying your compensating actions, so you could 
write:

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

In this example your Saga will retry compensating action `refundPayments` after exponentially 
increasing timeouts (based on `ZIO#retry` and `ZSchedule`).


### Parallel execution
Saga pattern does not limit transactional requests to run only in sequence.
Because of that `zio-saga` contains methods for parallel execution of requests. 

```scala
    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 `zio-saga`
contains specific functions:
- `compensate(compensation: Either[E, A] => Compensator[R, E])` this function makes compensation dependent on the result 
of corresponding effect that either fails or succeeds.
- `compensateIfFail(compensation: E => Compensator[R, E])` this function makes compensation dependent only on error type 
hence compensation will only be triggered if corresponding effect fails.
- `compensateIfSuccess(compensation: A => Compensator[R, E])` 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.

### Cats Compatible Sagas

[cats-saga](https://github.com/VladKopanev/cats-saga)

[Link-Codecov]: https://codecov.io/gh/VladKopanev/zio-saga?branch=master "Codecov"
[Link-Travis]: https://travis-ci.com/VladKopanev/zio-saga "circleci"
[Link-SonatypeReleases]: https://oss.sonatype.org/content/repositories/releases/com/vladkopanev/zio-saga-core_2.12/ "Sonatype Releases"
[Link-ScalaSteward]: https://scala-steward.org

[Badge-Codecov]: https://codecov.io/gh/VladKopanev/zio-saga/branch/master/graph/badge.svg "Codecov" 
[Badge-Travis]: https://travis-ci.com/VladKopanev/zio-saga.svg?branch=master "Codecov" 
[Badge-SonatypeReleases]: https://img.shields.io/nexus/r/https/oss.sonatype.org/com.vladkopanev/zio-saga-core_2.11.svg "Sonatype Releases"
[Badge-ScalaSteward]: https://img.shields.io/badge/Scala_Steward-helping-brightgreen.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=


================================================
FILE: build.sbt
================================================
import com.typesafe.sbt.SbtPgp.autoImportImpl.pgpSecretRing
import sbt.file

name := "zio-saga"

val mainScala = "2.13.9"
val allScala = Seq("2.12.17", mainScala, "3.1.3")

inThisBuild(
  List(
    organization := "com.vladkopanev",
    homepage := Some(url("https://github.com/VladKopanev/zio-saga")),
    licenses := List("MIT License" -> url("https://opensource.org/licenses/MIT")),
    developers := List(
      Developer(
        "VladKopanev",
        "Vladislav Kopanev",
        "ivengo53@gmail.com",
        url("http://vladkopanev.com")
      )
    ),
    scmInfo := Some(
      ScmInfo(url("https://github.com/VladKopanev/zio-saga"), "scm:git:git@github.com/VladKopanev/zio-saga.git")
    ),
    pgpPublicRing := file("./travis/local.pubring.asc"),
    pgpSecretRing := file("./travis/local.secring.asc"),
    releaseEarlyWith := SonatypePublisher
  )
)

lazy val commonSettings = Seq(
  scalaVersion := mainScala,
  scalacOptions ++= Seq(
    "-deprecation",
    "-encoding",
    "UTF-8",
    "-explaintypes",
    "-Yrangepos",
    "-feature",
    "-Xfuture",
    "-language:higherKinds",
    "-language:existentials",
    "-language:implicitConversions",
    "-unchecked",
    "-Xlint:_,-type-parameter-shadow",
    "-Ywarn-numeric-widen",
    "-Ywarn-unused",
    "-Ywarn-value-discard"
  ) ++ (CrossVersion.partialVersion(scalaVersion.value) match {
    case Some((3, _)) =>
      Seq("-Ykind-projector", "-unchecked")
    case Some((2, 12)) =>
      Seq(
        "-Xsource:2.13",
        "-Yno-adapted-args",
        "-Ypartial-unification",
        "-Ywarn-extra-implicit",
        "-Ywarn-inaccessible",
        "-Ywarn-infer-any",
        "-Ywarn-nullary-override",
        "-Ywarn-nullary-unit",
        "-opt-inline-from:<source>",
        "-opt-warnings",
        "-opt:l:inline"
      )
    case _ => Nil
  }),
  resolvers ++= Resolver.sonatypeOssRepos("snapshots") ++ Resolver.sonatypeOssRepos("releases")
)

lazy val root = project
  .in(file("."))
  .aggregate(core)

lazy val core = project
  .in(file("core"))
  .settings(
    commonSettings,
    name := "zio-saga-core",
    crossScalaVersions := allScala,
    libraryDependencies ++= Seq(
      "dev.zio" %% "zio"          % Versions.Zio,
      "dev.zio" %% "zio-test"     % Versions.Zio % "test",
      "dev.zio" %% "zio-test-sbt" % Versions.Zio % "test"
    ),
    testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))
  )

lazy val examples = project
  .in(file("examples"))
  .settings(
    commonSettings,
    scalaVersion := mainScala,
    coverageEnabled := false,
    libraryDependencies ++= Seq(
      "ch.qos.logback" % "logback-classic"     % "1.2.10",
      "dev.zio"       %% "zio-interop-cats"    % "3.2.9.0",
      "org.typelevel" %% "log4cats-core"       % Versions.Log4Cats,
      "org.typelevel" %% "log4cats-slf4j"      % Versions.Log4Cats,
      "io.circe"      %% "circe-generic"       % Versions.Circe,
      "io.circe"      %% "circe-parser"        % Versions.Circe,
      "org.http4s"    %% "http4s-circe"        % Versions.Http4s,
      "org.http4s"    %% "http4s-dsl"          % Versions.Http4s,
      "org.http4s"    %% "http4s-blaze-server" % Versions.Http4s,
      "org.tpolecat"  %% "doobie-core"         % Versions.Doobie,
      "org.tpolecat"  %% "doobie-hikari"       % Versions.Doobie,
      "org.tpolecat"  %% "doobie-postgres"     % Versions.Doobie,
      // compilerPlugin("org.scalamacros"  %% "paradise"           % "2.1.0"),
      compilerPlugin("org.typelevel" %% "kind-projector"     % "0.13.2" cross CrossVersion.full),
      compilerPlugin("com.olegpy"    %% "better-monadic-for" % "0.3.1")
    )
  )
  .dependsOn(core % "compile->compile")


================================================
FILE: core/src/main/scala/com/vladkopanev/zio/saga/Saga.scala
================================================
package com.vladkopanev.zio.saga

import com.vladkopanev.zio.saga.Saga.Compensator
import zio.Clock
import zio.{Cause, Exit, Fiber, IO, RIO, Schedule, Task, UIO, ZIO}

/**
 * A Saga is an immutable structure that models a distributed transaction.
 *
 * @see [[https://blog.couchbase.com/saga-pattern-implement-business-transactions-using-microservices-part/ Saga pattern]]
 *
 * Saga class has three type parameters - R for environment, E for errors and A for successful result.
 * Saga wraps a ZIO that carries the compensating action in both error and result channels and enables a composition
 * with another Sagas in for-comprehensions.
 * If error occurs Saga will execute compensating actions starting from action that corresponds to failed request
 * till the first already completed request.
 * */
final class Saga[-R, +E, +A] private (
  private val request: ZIO[R, (E, Compensator[R, E]), (A, Compensator[R, E])]
) extends AnyVal {
  self =>

  /**
   * Maps the resulting value `A` of this Saga to value `B` with function `f`.
   * */
  def map[B](f: A => B): Saga[R, E, B] =
    new Saga(request.map { case (a, comp) => (f(a), comp) })

  /**
   * Sequences the result of this Saga to the next Saga.
   * */
  def flatMap[R1 <: R, E1 >: E, B](f: A => Saga[R1, E1, B]): Saga[R1, E1, B] =
    new Saga(request.flatMap {
      case (a, compA) =>
        f(a).request.mapBoth(
          { case (e, compB) => (e, compB *> compA) },
          { case (r, compB) => (r, compB *> compA) }
        )
    })

  /**
   * Flattens the structure of this Saga by executing outer Saga first and then executes inner Saga.
   * */
  def flatten[R1 <: R, E1 >: E, B](implicit ev: A <:< Saga[R1, E1, B]): Saga[R1, E1, B] =
    self.flatMap(r => ev(r))

  /**
   * Returns Saga that will execute this Saga in parallel with other, combining the result in a tuple.
   * Both compensating actions would be executed in case of failure.
   * */
  def zipPar[R1 <: R, E1 >: E, B](that: Saga[R1, E1, B]): Saga[R1, E1, (A, B)] =
    zipWithPar(that)((a, b) => (a, b))

  /**
   * Returns Saga that will execute this Saga in sequence with other, combining the result in a tuple.
   * Only failed effect would be compensated.
   * */
  def zip[R1 <: R, E1 >: E, B](that: Saga[R1, E1, B]): Saga[R1, E1, (A, B)] =
    zipWith(that)((a, b) => (a, b))

  /**
   * Returns Saga that will execute this Saga in parallel with other, combining the result with specified function `f`.
   * Both compensating actions would be executed in case of failure.
   * */
  def zipWithPar[R1 <: R, E1 >: E, B, C](that: Saga[R1, E1, B])(f: (A, B) => C): Saga[R1, E1, C] =
    zipWithParAll(that)(f)((a, b) => a *> b)

  /**
   * Returns Saga that will execute this Saga in sequence with other, combining the result with specified function `f`.
   * Only failed effect would be compensated.
   * */
  def zipWith[R1 <: R, E1 >: E, B, C](that: Saga[R1, E1, B])(f: (A, B) => C): Saga[R1, E1, C] = (for {
    thisResult <- this
    thatResult <- that
  } yield f(thisResult, thatResult))

  /**
   * Returns Saga that will execute this Saga in parallel with other, combining the result with specified function `f`
   * and combining the compensating actions with function `g` (this allows user to choose a strategy of running both
   * compensating actions e.g. in sequence or in parallel).
   * */
  def zipWithParAll[R1 <: R, E1 >: E, B, C](
    that: Saga[R1, E1, B]
  )(f: (A, B) => C)(g: (Compensator[R1, E1], Compensator[R1, E1]) => Compensator[R1, E1]): Saga[R1, E1, C] = {
    def coordinate[A1, B1, C1](f: (A1, B1) => C1)(
      fasterSaga: Exit[(E1, Compensator[R1, E1]), (A1, Compensator[R1, E1])],
      slowerSaga: Fiber[(E1, Compensator[R1, E1]), (B1, Compensator[R1, E1])]
    ): ZIO[R1, (E1, Compensator[R1, E1]), (C1, Compensator[R1, E1])] =
      fasterSaga.foldZIO(
        { cause =>
          def extractCompensatorFrom(c: Cause[(E1, Compensator[R1, E1])]): Compensator[R1, E1] =
            c.failures.headOption.map[Compensator[R1, E1]](_._2).getOrElse(ZIO.dieMessage("Compensator was lost"))
          /* we can't use interrupt here because we won't get a compensation action in case
           IO was still running and interrupted */
          slowerSaga.await.flatMap(
            _.foldZIO(
              { loserCause =>
                val compA = extractCompensatorFrom(cause)
                val compB = extractCompensatorFrom(loserCause)
                ZIO.failCause((cause && loserCause).map { case (e, _) => (e, g(compB, compA)) })
              }, { case (_, compB) => ZIO.failCause(cause.map { case (e, compA) => (e, g(compB, compA)) }) }
            )
          )
        }, {
          case (a, compA) =>
            slowerSaga.join.mapBoth(
              { case (e, compB) => (e, g(compB, compA)) },
              { case (b, compB) => (f(a, b), g(compB, compA)) }
            )
        }
      )

    val h = (b: B, a: A) => f(a, b)
    new Saga(request.raceWith(that.request)(coordinate(f), coordinate(h)))
  }

  /**
   * Materializes this Saga to ZIO effect.
   * */
  def transact: ZIO[R, E, A] =
    request.tapError({ case (e, compA) => compA.orElseFail((e, IO.unit)) }).mapBoth(_._1, _._1)
}

object Saga {

  type Compensator[-R, +E] = ZIO[R, E, Unit]

  /**
   * Constructs new Saga from action and compensating action.
   * */
  def compensate[R, E, A](request: ZIO[R, E, A], compensator: Compensator[R, E]): Saga[R, E, A] =
    compensate(request, (_: Either[E, A]) => compensator)

  /**
   * Constructs new Saga from action and compensation function that will be applied the result of this request.
   * */
  def compensate[R, E, A](action: ZIO[R, E, A], compensation: Either[E, A] => Compensator[R, E]): Saga[R, E, A] =
    new Saga(action.mapBoth(e => (e, compensation(Left(e))), a => (a, compensation(Right(a)))))

  /**
   * Constructs new Saga from action and compensation function that will be applied only to failed result of this request.
   * If given action succeeds associated compensating action would not be executed during the compensation phase.
   * */
  def compensateIfFail[R, E, A](action: ZIO[R, E, A], compensation: E => Compensator[R, E]): Saga[R, E, A] =
    compensate(action, (result: Either[E, A]) => result.fold(compensation, _ => ZIO.unit))

  /**
   * Constructs new Saga from action and compensation function that will be applied only to successful result of this request.
   * If given action fails associated compensating action would not be executed during the compensation phase.
   * */
  def compensateIfSuccess[R, E, A](action: ZIO[R, E, A], compensation: A => Compensator[R, E]): Saga[R, E, A] =
    compensate(action, (result: Either[E, A]) => result.fold(_ => ZIO.unit, compensation))

  /**
   * Runs all Sagas in iterable in parallel and collects
   * the results.
   */
  def collectAllPar[R, E, A](sagas: Iterable[Saga[R, E, A]]): Saga[R, E, List[A]] =
    foreachPar[R, E, Saga[R, E, A], A](sagas)(identity)

  /**
   * Runs all Sagas in iterable in sequence and collects the results.
   */
  def collectAll[R, E, A](sagas: Iterable[Saga[R, E, A]]): Saga[R, E, List[A]] =
    foreach(sagas)(identity)

  /**
   * Runs all Sagas in iterable in parallel, and collect
   * the results.
   */
  def collectAllPar[R, E, A](sagas: Saga[R, E, A]*): Saga[R, E, List[A]] =
    collectAllPar(sagas)

  /**
   * Constructs Saga without compensation that fails with an error.
   * */
  def fail[R, E](error: E): Saga[R, E, Nothing] = noCompensate(ZIO.fail(error))

  /**
   * Constructs a Saga that applies the function `f` to each element of the `Iterable[A]` in parallel,
   * and returns the results in a new `List[B]`.
   *
   */
  def foreachPar[R, E, A, B](as: Iterable[A])(fn: A => Saga[R, E, B]): Saga[R, E, List[B]] =
    as.foldRight[Saga[R, E, List[B]]](Saga.noCompensate(IO.succeed(Nil))) { (a, io) =>
      fn(a).zipWithPar(io)((b, bs) => b :: bs)
    }

  /**
   * Constructs a Saga that applies the function `f` to each element of the `Iterable[A]` in sequence, and returns the
   * results in a new `List[B]`.
   */
  def foreach[R, E, A, B](as: Iterable[A])(fn: A => Saga[R, E, B]): Saga[R, E, List[B]] =
    as.foldRight[Saga[R, E, List[B]]](Saga.noCompensate(IO.succeed(Nil))) { (a, io) =>
      fn(a).zipWith(io)((b, bs) => b :: bs)
    }

  /**
   * Constructs new `no-op` Saga that will do nothing on error.
   * */
  def noCompensate[R, E, A](request: ZIO[R, E, A]): Saga[R, E, A] = compensate(request, ZIO.unit)

  /**
   * Constructs new Saga from action, compensating action and a scheduling policy for retrying compensation.
   * */
  def retryableCompensate[R, SR, E, A](
    request: ZIO[R, E, A],
    compensator: Compensator[R, E],
    schedule: Schedule[SR, E, Any]
  ): Saga[R with SR with Clock, E, A] = {
    val retry: Compensator[R with SR with Clock, E] = compensator.retry(schedule.unit)
    compensate(request, retry)
  }

  /**
   * Constructs Saga without compensation that succeeds with a strict value.
   * */
  def succeed[R, A](value: A): Saga[R, Nothing, A] = noCompensate(ZIO.succeed(value))

  // $COVERAGE-OFF$
  implicit def IOtoCompensable[E, A](io: IO[E, A]): Compensable[Any, E, A] = new Compensable(io)

  implicit def ZIOtoCompensable[R, E, A](zio: ZIO[R, E, A]): Compensable[R, E, A] = new Compensable(zio)

  implicit def UIOtoCompensable[A](uio: UIO[A]): Compensable[Any, Nothing, A] = new Compensable(uio)

  implicit def RIOtoCompensable[R, A](rio: RIO[R, A]): Compensable[R, Throwable, A] = new Compensable(rio)

  implicit def TaskToCompensable[A](task: Task[A]): Compensable[Any, Throwable, A] = new Compensable(task)
  // $COVERAGE-ON$

  /**
   * Extension methods for IO requests.
   * */
  class Compensable[-R, +E, +A](val request: ZIO[R, E, A]) extends AnyVal {

    def compensate[R1 <: R, E1 >: E](c: Compensator[R1, E1]): Saga[R1, E1, A] =
      Saga.compensate(request, c)

    def compensate[R1 <: R, E1 >: E](compensation: Either[E1, A] => Compensator[R1, E1]): Saga[R1, E1, A] =
      Saga.compensate(request, compensation)

    def compensateIfFail[R1 <: R, E1 >: E](compensation: E1 => Compensator[R1, E1]): Saga[R1, E1, A] =
      Saga.compensateIfFail(request, compensation)

    def compensateIfSuccess[R1 <: R, E1 >: E](compensation: A => Compensator[R1, E1]): Saga[R1, E1, A] =
      Saga.compensateIfSuccess(request, compensation)

    def retryableCompensate[R1 <: R, SR, E1 >: E](
      c: Compensator[R1, E1],
      schedule: Schedule[SR, E1, Any]
    ): Saga[R1 with SR with Clock, E1, A] =
      Saga.retryableCompensate(request, c, schedule)

    def noCompensate: Saga[R, E, A] = Saga.noCompensate(request)
  }

}


================================================
FILE: core/src/test/scala/com/vladkopanev/zio/saga/SagaSpec.scala
================================================
package com.vladkopanev.zio.saga

import java.util.concurrent.TimeUnit
import com.vladkopanev.zio.saga.Saga.Compensator
import zio._
import zio.Clock
import zio.test._
import zio.test.Assertion._
import zio.test.TestEnvironment
import Saga._
import SagaSpecUtil._
import zio.Clock.currentTime

object SagaSpec
    extends DefaultRunnableSpec {
      override val spec: ZSpec[TestEnvironment, Any] = suite("SagaSpec")(
        suite("Saga#map")(test("should change the result value with provided function") {
          assertM(Saga.compensate(ZIO.succeed(1), ZIO.unit).map(_.toString).transact)(equalTo("1"))
        }),
        suite("Saga#zipPar")(test("should successfully run two Sagas in parallel") {
          assertM((bookFlight compensate cancelFlight zipPar (bookHotel compensate cancelHotel)).transact)(equalTo((FlightPayment, HotelPayment)))
        }),
        suite("Saga#zip")(test("should successfully run two Sagas in sequence") {
          assertM((bookFlight compensate cancelFlight zip (bookHotel compensate cancelHotel)).transact)(equalTo((FlightPayment, HotelPayment)))
        }),
        suite("Saga#zipWithPar")(
          test("should successfully run two Sagas in parallel") {
            for {
              startTime <- currentTime(TimeUnit.MILLISECONDS).provideLayer(Clock.live)
              _ <- (sleep(1000.millis) *> bookFlight compensate cancelFlight)
                    .zipWithPar(sleep(1000.millis) *> bookHotel compensate cancelHotel)((_, _) => ())
                    .transact
              endTime <- currentTime(TimeUnit.MILLISECONDS).provideLayer(Clock.live)
            } yield assert(endTime - startTime)(isLessThanEqualTo(3000L))
          },
          test("should run both compensating actions in case right request fails") {
            val bookFlightS = sleep(1000.millis) *> bookFlight
            val failHotel   = sleep(100.millis) *> IO.fail(HotelBookingError())

            for {
              actionLog <- Ref.make(Vector.empty[String])
              _ <- (bookFlightS compensate cancelFlight(actionLog.update(_ :+ "flight canceled")))
                    .zipWithPar(failHotel compensate cancelHotel(actionLog.update(_ :+ "hotel canceled")))((_, _) => ())
                    .transact
                    .orElse(IO.unit)
              log <- actionLog.get
            } yield assert(log)(equalTo(Vector("flight canceled", "hotel canceled")))
          },
          test("should run both compensating actions in case left request fails") {
            val bookFlightS = sleep(1000.millis) *> bookFlight
            val failHotel   = sleep(100.millis) *> IO.fail(HotelBookingError())

            for {
              actionLog <- Ref.make(Vector.empty[String])
              _ <- (failHotel compensate cancelHotel(actionLog.update(_ :+ "hotel canceled")))
                    .zipWithPar(bookFlightS compensate cancelFlight(actionLog.update(_ :+ "flight canceled")))((_, _) =>
                      ()
                    )
                    .transact
                    .orElse(IO.unit)
              log <- actionLog.get
            } yield assert(log)(equalTo(Vector("flight canceled", "hotel canceled")))
          },
          test("should run both compensating actions in case both requests fails") {
            val failFlight = sleep(1000.millis) *> IO.fail(FlightBookingError())
            val failHotel  = sleep(1000.millis) *> IO.fail(HotelBookingError())

            for {
              actionLog <- Ref.make(Vector.empty[String])
              _ <- (failFlight compensate cancelFlight(actionLog.update(_ :+ "flight canceled")))
                    .zipWithPar(failHotel compensate cancelHotel(actionLog.update(_ :+ "hotel canceled")))((_, _) => ())
                    .transact
                    .orElse(IO.unit)
              log <- actionLog.get
            } yield assert(log)(hasSameElements(Vector("flight canceled", "hotel canceled")))
          },
          test("should run compensating actions in order that is opposite to which requests finished") {
            val failFlight = sleep(1000.millis) *> IO.fail(FlightBookingError())
            val failHotel  = sleep(100.millis) *> IO.fail(HotelBookingError())

            for {
              actionLog <- Ref.make(Vector.empty[String])
              _ <- (failFlight compensate cancelFlight(actionLog.update(_ :+ "flight canceled")))
                    .zipWithPar(failHotel compensate cancelHotel(actionLog.update(_ :+ "hotel canceled")))((_, _) => ())
                    .transact
                    .orElse(IO.unit)
              log <- actionLog.get
            } yield assert(log)(equalTo(Vector("flight canceled", "hotel canceled")))
          }
        ),
        suite("Saga#zipWith")(
          test("should successfully run two Sagas in sequence") {
            (for {
              actionLog <- Ref.make(Vector.empty[String]).noCompensate
              hotelBooked = bookHotel <* actionLog.update(_ :+ "hotel booked") compensate cancelHotel
              flightBooked = bookFlight <* actionLog.update(_ :+ "flight booked") compensate cancelFlight
              _       <- hotelBooked.zipWith(flightBooked)((_, _) => ())
              actions <- actionLog.get.noCompensate
            } yield assertTrue(actions == Vector("hotel booked", "flight booked"))).transact
          },
          test("when right failed then should compensate right before left") {
            for {
              actionLog <- Ref.make(Vector.empty[String])
              _ <- (bookFlight compensate cancelFlight(actionLog.update(_ :+ "flight canceled")))
                .zipWith(IO.fail(HotelBookingError()) compensate cancelHotel(actionLog.update(_ :+ "hotel canceled")))((_, _) => ())
                .transact
                .orElse(IO.unit)
              log <- actionLog.get
            } yield assertTrue(log == Vector("hotel canceled", "flight canceled"))
          },
          test("when left failed should compensate left without calling right") {
            for {
              actionLog <- Ref.make(Vector.empty[String])
              _ <- (IO.fail(HotelBookingError()) compensate cancelHotel(actionLog.update(_ :+ "hotel canceled")))
                .zipWith(actionLog.update(_ :+ "flight booked") *> bookFlight compensate cancelFlight(actionLog.update(_ :+ "flight canceled")))((_, _) =>
                  ()
                )
                .transact
                .orElse(IO.unit)
              log <- actionLog.get
            } yield assertTrue(log == Vector("hotel canceled"))
          }
        ),
        suite("Saga#zipWithParAll")(test("should allow combining compensations in parallel") {
          val failFlight = IO.fail(FlightBookingError())
          val failHotel  = IO.fail(HotelBookingError())

          def cancelFlightC(actionLog: Ref[Vector[String]]) =
            sleep(100.millis) *>
              cancelFlight(actionLog.update(_ :+ "flight canceled"))
          def cancelHotelC(actionLog: Ref[Vector[String]]) =
            sleep(100.millis) *>
              cancelHotel(actionLog.update(_ :+ "hotel canceled"))

          for {
            actionLog <- Ref.make(Vector.empty[String])
            _ <- (failFlight compensate cancelFlightC(actionLog))
                  .zipWithParAll(failHotel compensate cancelHotelC(actionLog))((_, _) => ())((a, b) => a.zipPar(b).unit)
                  .transact
                  .orElse(IO.unit)
            log <- actionLog.get
          } yield assert(log)(hasSameElements(Vector("flight canceled", "hotel canceled")))
        }),
        suite("Saga#retryableCompensate")(
          test("should construct Saga that repeats compensating action once") {
            val failFlight: ZIO[Clock, FlightBookingError, PaymentInfo] = sleep(1000.millis) *> IO.fail(
              FlightBookingError()
            )

            def failCompensator(log: Ref[Vector[String]]): Compensator[Any, FlightBookingError] =
              cancelFlight(log.update(_ :+ "Compensation failed")) *> IO.fail(FlightBookingError())

            for {
              actionLog <- Ref.make(Vector.empty[String])
              _ <- (failFlight retryableCompensate (failCompensator(actionLog), Schedule.once)).transact
                    .orElse(ZIO.unit)
              log <- actionLog.get
            } yield assert(log)(equalTo(Vector.fill(2)("Compensation failed")))
          },
          test("should work with other combinators") {
            val saga = for {
              _ <- bookFlight.noCompensate
              _ <- bookHotel retryableCompensate (cancelHotel, Schedule.once)
              _ <- bookCar compensate cancelCar
            } yield ()

            assertM(saga.transact)(anything)
          }
        ),
        suite("Saga#collectAllPar")(
          test("should construct a Saga that runs several requests in parallel") {
            def bookFlightS(log: Ref[Vector[String]]): ZIO[Clock, FlightBookingError, PaymentInfo] =
              sleep(1000.millis) *> bookFlight <* log.update(_ :+ "flight is booked")
            def bookHotelS(log: Ref[Vector[String]]): ZIO[Clock, HotelBookingError, PaymentInfo] =
              sleep(600.millis) *> bookHotel <* log.update(_ :+ "hotel is booked")
            def bookCarS(log: Ref[Vector[String]]): ZIO[Clock, CarBookingError, PaymentInfo] =
              sleep(300.millis) *> bookCar <* log.update(_ :+ "car is booked")
            def bookCarS2(log: Ref[Vector[String]]): ZIO[Clock, CarBookingError, PaymentInfo] =
              sleep(100.millis) *> bookCar <* log.update(_ :+ "car2 is booked")

            for {
              actionLog <- Ref.make(Vector.empty[String])
              flight    = bookFlightS(actionLog) compensate cancelFlight
              hotel     = bookHotelS(actionLog) compensate cancelHotel
              car       = bookCarS(actionLog) compensate cancelCar
              car2      = bookCarS2(actionLog) compensate cancelCar
              _         <- Saga.collectAllPar(flight, hotel, car, car2).transact
              log       <- actionLog.get
            } yield assert(log)(hasSameElements(Vector("car2 is booked", "car is booked", "hotel is booked", "flight is booked")))
          },
          test("should run all compensating actions in case of error") {
            val failFlightBooking = sleep(1000.millis) *> IO.fail(FlightBookingError())
            val bookHotelS        = sleep(600.millis) *> bookHotel
            val bookCarS          = sleep(300.millis) *> bookCar
            val bookCarS2         = sleep(100.millis) *> bookCar

            for {
              actionLog <- Ref.make(Vector.empty[String])
              flight    = failFlightBooking compensate cancelFlight(actionLog.update(_ :+ "flight canceled"))
              hotel     = bookHotelS compensate cancelHotel(actionLog.update(_ :+ "hotel canceled"))
              car       = bookCarS compensate cancelCar(actionLog.update(_ :+ "car canceled"))
              car2      = bookCarS2 compensate cancelCar(actionLog.update(_ :+ "car2 canceled"))
              _         <- Saga.collectAllPar(List(flight, hotel, car, car2)).transact.orElse(IO.unit)
              log       <- actionLog.get
            } yield assert(log)(hasSameElements(Vector("flight canceled", "hotel canceled", "car canceled", "car2 canceled")))
          }
        ),
        suite("Saga#succeed")(test("should construct saga that will succeed") {
          val failFlightBooking = IO.fail(FlightBookingError())
          val stub              = 1

          for {
            actionLog <- Ref.make(Vector.empty[String])
            _ <- (for {
                  i <- Saga.succeed(stub)
                  _ <- failFlightBooking compensate cancelFlight(actionLog.update(_ :+ s"flight canceled $i"))
                } yield ()).transact.orElse(ZIO.unit)
            log <- actionLog.get
          } yield assert(log)(equalTo(Vector(s"flight canceled $stub")))
        }),
        suite("Saga#fail")(test("should construct saga that will fail") {
          val failFlightBooking = IO.fail(FlightBookingError())

          for {
            actionLog <- Ref.make(Vector.empty[String])
            _ <- (for {
                  i <- Saga.fail(FlightBookingError())
                  _ <- failFlightBooking compensate cancelFlight(actionLog.update(_ :+ s"flight canceled $i"))
                } yield ()).transact.orElse(ZIO.unit)
            log <- actionLog.get
          } yield assert(log)(equalTo(Vector.empty))
        }),
        suite("Saga#compensateIfFail")(
          test("should construct saga step that executes it's compensation if it's requests fails") {
            val failCar = IO.fail(CarBookingError())
            for {
              actionLog <- Ref.make(Vector.empty[String])
              _ <- (for {
                    _ <- bookFlight compensate cancelFlight(actionLog.update(_ :+ "flight canceled"))
                    _ <- bookHotel compensate cancelHotel(actionLog.update(_ :+ "hotel canceled"))
                    _ <- failCar compensateIfFail ((_: SagaError) => cancelCar(actionLog.update(_ :+ "car canceled")))
                  } yield ()).transact.orElse(ZIO.unit)
              log <- actionLog.get
            } yield assert(log)(equalTo(Vector("car canceled", "hotel canceled", "flight canceled")))
          },
          test("should construct saga step that do not executes it's compensation if it's request succeeds") {
            val failFlightBooking = IO.fail(FlightBookingError())
            for {
              actionLog <- Ref.make(Vector.empty[String])
              _ <- (for {
                    _ <- bookCar compensateIfFail ((_: SagaError) => cancelCar(actionLog.update(_ :+ "car canceled")))
                    _ <- bookHotel compensate cancelHotel(actionLog.update(_ :+ "hotel canceled"))
                    _ <- failFlightBooking compensate cancelFlight(actionLog.update(_ :+ "flight canceled"))
                  } yield ()).transact.orElse(ZIO.unit)
              log <- actionLog.get
            } yield assert(log)(equalTo(Vector("flight canceled", "hotel canceled")))
          }
        ),
        suite("Saga#compensateIfSuccess")(
          test(
            "should construct saga step that executes it's compensation if it's requests succeeds"
          ) {
            val failFlightBooking = IO.fail(FlightBookingError())
            for {
              actionLog <- Ref.make(Vector.empty[String])
              _ <- (for {
                    _ <- bookCar compensateIfSuccess (
                          (_: PaymentInfo) => cancelCar(actionLog.update(_ :+ "car canceled"))
                        )
                    _ <- bookHotel compensate cancelHotel(actionLog.update(_ :+ "hotel canceled"))
                    _ <- failFlightBooking compensate cancelFlight(actionLog.update(_ :+ "flight canceled"))
                  } yield ()).transact.orElse(ZIO.unit)
              log <- actionLog.get
            } yield assert(log)(equalTo(Vector("flight canceled", "hotel canceled", "car canceled")))
          },
          test("should construct saga step that do not executes it's compensation if it's request fails") {
            val failCar = IO.fail(CarBookingError())
            for {
              actionLog <- Ref.make(Vector.empty[String])
              _ <- (for {
                    _ <- bookHotel compensate cancelHotel(actionLog.update(_ :+ "hotel canceled"))
                    _ <- bookFlight compensate cancelFlight(actionLog.update(_ :+ "flight canceled"))
                    _ <- failCar compensateIfSuccess (
                          (_: PaymentInfo) => cancelCar(actionLog.update(_ :+ "car canceled"))
                        )
                  } yield ()).transact.orElse(ZIO.unit)
              log <- actionLog.get
            } yield assert(log)(equalTo(Vector("flight canceled", "hotel canceled")))
          }
        ),
        suite("Saga#compensate")(
          test("should allow compensation to be dependent on the result of corresponding effect") {
            val failCar = IO.fail(CarBookingError())
            for {
              actionLog <- Ref.make(Vector.empty[String])
              _ <- (for {
                    _ <- bookHotel compensate cancelHotel(actionLog.update(_ :+ "hotel canceled"))
                    _ <- bookFlight compensate cancelFlight(actionLog.update(_ :+ "flight canceled"))
                    _ <- failCar compensate (
                          (_: Either[SagaError, PaymentInfo]) => cancelCar(actionLog.update(_ :+ "car canceled"))
                        )
                  } yield ()).transact.orElse(ZIO.unit)
              log <- actionLog.get
            } yield assert(log)(equalTo(Vector("car canceled", "flight canceled", "hotel canceled")))
          }
        ),
        suite("Saga#flatten")(
          test("should execute outer effect first and then the inner one producing the result of it") {
            for {
              actionLog <- Ref.make(Vector.empty[String])
              outer     = bookFlight compensate cancelFlight(actionLog.update(_ :+ "flight canceled"))
              inner     = bookHotel compensate cancelHotel(actionLog.update(_ :+ "hotel canceled"))
              failCar   = IO.fail(CarBookingError()) compensate cancelCar(actionLog.update(_ :+ "car canceled"))

              _ <- outer
                    .map(_ => inner)
                    .flatten[Any, SagaError, PaymentInfo]
                    .flatMap(_ => failCar)
                    .transact
                    .orElse(ZIO.unit)
              log <- actionLog.get
            } yield assert(log)(equalTo(Vector("car canceled", "hotel canceled", "flight canceled")))
          }
        ),
        suite("Saga#transact")(
          test("should return original error in case compensator also fails") {
            val expectedError                                   = FlightBookingError()
            val failFlight: ZIO[Clock, FlightBookingError, Any] = sleep(1000.millis) *> IO.fail(expectedError)

            val failCompensator = cancelFlight *> IO.fail(CarBookingError())

            val saga = (failFlight compensate failCompensator).transact.catchAll(e => IO.succeed(e))

            assertM(saga)(equalTo(expectedError))
          },
          test("should return original error in case compensator also fails 2") {
            val expectedError                                   = FlightBookingError()
            val failFlight: ZIO[Clock, FlightBookingError, Any] = sleep(1000.millis) *> IO.fail(expectedError)

            val failCompensator = cancelFlight *> IO.fail(new RuntimeException())

            val saga = (for {
              _ <- bookHotel compensate cancelHotel
              _ <- failFlight compensate failCompensator
              _ <- bookCar compensate cancelCar
            } yield ()).transact.catchAll[Clock, Any, Any](e => IO.succeed(e))

            assertM(saga)(equalTo(expectedError))
          }
        ),
        suite("Saga#collectAll")(
          test("should collect all effects in one collection in sequential way") {
            for {
              actionLog <- Ref.make(Vector.empty[String])

              payments <- Saga
                .collectAll(
                  (bookFlight <* actionLog.update(_ :+ "flight booked")).noCompensate ::
                    (bookHotel <* actionLog.update(_ :+ "hotel booked")).noCompensate ::
                    (bookCar <* actionLog.update(_ :+ "car booked")).noCompensate ::
                    Nil
                )
                .transact

              actionsOrder <- actionLog.get
            } yield assert(payments)(hasSameElements(FlightPayment :: HotelPayment :: CarPayment :: Nil)) &&
              assertTrue(actionsOrder == Vector("flight booked", "hotel booked", "car booked"))
          },
          test("should compensate made effects when one of them failed") {
            for {
              log <- Ref.make(Vector.empty[String])

              successfullyBookFlight = bookFlight.compensate(cancelFlight(log.update(_ :+ "flight canceled")))

              failOnHotelBook = IO.fail(HotelBookingError)
                .compensate(cancelHotel(log.update(_ :+ "hotel canceled")))

              successfullyBookCar = (bookCar <* log.update(_ :+ "car booked"))
                .compensate(cancelCar(log.update(_ :+ "car canceled")))

              _ <- Saga
                .collectAll(successfullyBookFlight :: failOnHotelBook :: successfullyBookCar :: Nil)
                .transact
                .fold(
                  _ => (),
                  _ => ()
                )

              actionsOrder <- log.get
            } yield assertTrue(actionsOrder == Vector("hotel canceled", "flight canceled"))
          }
        )
      )
}

object SagaSpecUtil {

  def sleep(d: Duration): URIO[Clock, Unit] = ZIO.sleep(d).provide(Clock.live)

  sealed trait SagaError extends Product with Serializable {
    def message: String
  }
  case class FlightBookingError(message: String = "Can't book a flight")        extends SagaError
  case class HotelBookingError(message: String = "Can't book a hotel room")     extends SagaError
  case class CarBookingError(message: String = "Can't book a car")              extends SagaError
  case class PaymentFailedError(message: String = "Can't collect the payments") extends SagaError

  case class PaymentInfo(amount: Double)

  val FlightPayment = PaymentInfo(420d)
  val HotelPayment  = PaymentInfo(1448d)
  val CarPayment    = PaymentInfo(42d)

  def bookFlight: IO[FlightBookingError, PaymentInfo] = IO.succeed(FlightPayment)

  def bookHotel: IO[HotelBookingError, PaymentInfo] = IO.succeed(HotelPayment)

  def bookCar: IO[CarBookingError, PaymentInfo] = IO.succeed(CarPayment)

  def collectPayments(paymentInfo: PaymentInfo*): IO[PaymentFailedError, Unit] = IO.succeed(paymentInfo).unit

  def cancelFlight: Compensator[Any, FlightBookingError] = IO.unit

  def cancelFlight(postAction: UIO[Any]): Compensator[Any, FlightBookingError] =
    postAction *> IO.unit

  def cancelHotel: Compensator[Any, HotelBookingError] = IO.unit

  def cancelHotel(postAction: UIO[Any]): Compensator[Any, HotelBookingError] =
    postAction *> IO.unit

  def cancelCar: Compensator[Any, CarBookingError] = IO.unit

  def cancelCar(postAction: UIO[Any]): Compensator[Any, CarBookingError] = postAction *> IO.unit

  def refundPayments(paymentInfo: PaymentInfo*): Compensator[Any, PaymentFailedError] = IO.succeed(paymentInfo).unit

}


================================================
FILE: examples/README.md
================================================
This is the example that extends one provided in the parent REAMDE file.

This projects shows one of the implementation variants of Saga Executor Coordinator
that also writes transaction log to database and knows how to recover from failures. 

TODO:
- pack project to docker image
- minimal GUI, SSE for saga execution tracking
- in case of coordinator scaling we need to differentiate saga that is ongoing from failed saga that we need to restore

================================================
FILE: examples/src/main/resources/application.conf
================================================


================================================
FILE: examples/src/main/resources/logback.xml
================================================
<configuration>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n [%X{userId} %X{orderId} %X{sagaId}]</pattern>
        </encoder>
    </appender>

    <root level="debug">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

================================================
FILE: examples/src/main/resources/sql/saga.ddl
================================================
CREATE TABLE public.saga
(
    id sequence,
    initiator uuid NOT NULL,
    "createdAt" timestamp without time zone NOT NULL,
    "finishedAt" timestamp without time zone,
    data jsonb NOT NULL,
    type text,
    CONSTRAINT "Saga_pkey" PRIMARY KEY (id)
)

CREATE TABLE public.saga_step
(
    "sagaId" integer NOT NULL,
    name text NOT NULL,
    result jsonb,
    "finishedAt" timestamp without time zone,
    failure text,
    CONSTRAINT saga_step_pkey PRIMARY KEY ("sagaId", name),
    CONSTRAINT saga_id_fk FOREIGN KEY ("sagaId")
        REFERENCES public.saga (id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE CASCADE
)

================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/OrderSagaCoordinator.scala
================================================
package com.vladkopanev.zio.saga.example

import java.util.UUID

import com.vladkopanev.zio.saga.example.client.{
    LoyaltyPointsServiceClient,
    OrderServiceClient,
    PaymentServiceClient
  }
import com.vladkopanev.zio.saga.example.dao.SagaLogDao
import com.vladkopanev.zio.saga.example.model.{ OrderSagaData, OrderSagaError, SagaStep }
import io.chrisdavenport.log4cats.StructuredLogger
import io.chrisdavenport.log4cats.slf4j.Slf4jLogger
import zio.{ Schedule, Task, ZIO }

import scala.concurrent.TimeoutException

trait OrderSagaCoordinator {
  def runSaga(userId: UUID, orderId: BigInt, money: BigDecimal, bonuses: Double, sagaIdOpt: Option[Long]): TaskC[Unit]

  def recoverSagas: TaskC[Unit]
}

class OrderSagaCoordinatorImpl(
  paymentServiceClient: PaymentServiceClient,
  loyaltyPointsServiceClient: LoyaltyPointsServiceClient,
  orderServiceClient: OrderServiceClient,
  sagaLogDao: SagaLogDao,
  maxRequestTimeout: Int,
  logger: StructuredLogger[Task]
) extends OrderSagaCoordinator {

  import com.vladkopanev.zio.saga.Saga._

  def runSaga(
    userId: UUID,
    orderId: BigInt,
    money: BigDecimal,
    bonuses: Double,
    sagaIdOpt: Option[Long]
  ): TaskC[Unit] = {

    import zio.duration._

    def mkSagaRequest(
      request: TaskC[Unit],
      sagaId: Long,
      stepName: String,
      executedSteps: List[SagaStep],
      compensating: Boolean = false
    ) =
      ZIO.fromOption(
          executedSteps.find(step => step.name == stepName && !compensating).flatMap(_.failure).map(new OrderSagaError(_))
        ).flip *> request
          .timeoutFail(new TimeoutException(s"Execution Timeout occurred for $stepName step"))(maxRequestTimeout.seconds)
          .tapBoth(
            e => sagaLogDao.createSagaStep(stepName, sagaId, result = None, failure = Some(e.getMessage)),
            _ => sagaLogDao.createSagaStep(stepName, sagaId, result = None)
          )
          .when(!executedSteps.exists(_.name == stepName))

    def collectPayments(executed: List[SagaStep], sagaId: Long) = mkSagaRequest(
      paymentServiceClient.collectPayments(userId, money, sagaId.toString),
      sagaId,
      "collectPayments",
      executed
    )

    def assignLoyaltyPoints(executed: List[SagaStep], sagaId: Long) = mkSagaRequest(
      loyaltyPointsServiceClient.assignLoyaltyPoints(userId, bonuses, sagaId.toString),
      sagaId,
      "assignLoyaltyPoints",
      executed
    )

    def closeOrder(executed: List[SagaStep], sagaId: Long) =
      mkSagaRequest(orderServiceClient.closeOrder(userId, orderId, sagaId.toString), sagaId, "closeOrder", executed)

    def refundPayments(executed: List[SagaStep], sagaId: Long) = mkSagaRequest(
      paymentServiceClient.refundPayments(userId, money, sagaId.toString),
      sagaId,
      "refundPayments",
      executed,
      compensating = true
    )

    def cancelLoyaltyPoints(executed: List[SagaStep], sagaId: Long) = mkSagaRequest(
      loyaltyPointsServiceClient.cancelLoyaltyPoints(userId, bonuses, sagaId.toString),
      sagaId,
      "cancelLoyaltyPoints",
      executed,
      compensating = true
    )

    def reopenOrder(executed: List[SagaStep], sagaId: Long) =
      mkSagaRequest(
        orderServiceClient.reopenOrder(userId, orderId, sagaId.toString),
        sagaId,
        "reopenOrder",
        executed,
        compensating = true
      )

    val expSchedule = Schedule.exponential(1.second)
    def buildSaga(sagaId: Long, executedSteps: List[SagaStep]) =
      for {
        _ <- collectPayments(executedSteps, sagaId) retryableCompensate (refundPayments(executedSteps, sagaId), expSchedule)
        _ <- assignLoyaltyPoints(executedSteps, sagaId) retryableCompensate (cancelLoyaltyPoints(executedSteps, sagaId), expSchedule)
        _ <- closeOrder(executedSteps, sagaId) retryableCompensate (reopenOrder(executedSteps, sagaId), expSchedule)
      } yield ()

    import io.circe.syntax._

    val mdcLog = wrapMDC(logger, userId, orderId, sagaIdOpt)
    val data   = OrderSagaData(userId, orderId, money, bonuses).asJson

    for {
      _        <- mdcLog.info("Saga execution started")
      sagaId   <- sagaIdOpt.fold(sagaLogDao.startSaga(userId, data))(i => Task.succeed(i))
      executed <- sagaLogDao.listExecutedSteps(sagaId)
      _ <- buildSaga(sagaId, executed).transact.tapBoth({
            case _: OrderSagaError => sagaLogDao.finishSaga(sagaId)
            case _                 => ZIO.unit
          }, _ => sagaLogDao.finishSaga(sagaId))
      _ <- mdcLog.info("Saga execution finished")
    } yield ()

  }

  override def recoverSagas: TaskC[Unit] =
    for {
      _     <- logger.info("Sagas recovery stared")
      sagas <- sagaLogDao.listUnfinishedSagas
      _     <- logger.info(s"Found unfinished sagas: $sagas")
      _ <- ZIO.foreachParN_(100)(sagas) { sagaInfo =>
            ZIO.fromEither(sagaInfo.data.as[OrderSagaData]).flatMap {
              case OrderSagaData(userId, orderId, money, bonuses) =>
                runSaga(userId, orderId, money, bonuses, Some(sagaInfo.id)).catchSome {
                  case _: OrderSagaError => ZIO.unit
                }
            }
          }
      _ <- logger.info("Sagas recovery finished")
    } yield ()

  private def wrapMDC(logger: StructuredLogger[Task], userId: UUID, orderId: BigInt, sagaIdOpt: Option[Long]) =
    StructuredLogger.withContext(logger)(
      Map("userId" -> userId.toString, "orderId" -> orderId.toString, "sagaId" -> sagaIdOpt.toString)
    )
}

object OrderSagaCoordinatorImpl {
  import zio.interop.catz._

  def apply(
    paymentServiceClient: PaymentServiceClient,
    loyaltyPointsServiceClient: LoyaltyPointsServiceClient,
    orderServiceClient: OrderServiceClient,
    sagaLogDao: SagaLogDao,
    maxRequestTimeout: Int
  ): Task[OrderSagaCoordinatorImpl] =
    Slf4jLogger
      .create[Task]
      .map(
        new OrderSagaCoordinatorImpl(
          paymentServiceClient,
          loyaltyPointsServiceClient,
          orderServiceClient,
          sagaLogDao,
          maxRequestTimeout,
          _
        )
      )
}


================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/SagaApp.scala
================================================
package com.vladkopanev.zio.saga.example
import com.vladkopanev.zio.saga.example.client.{LoyaltyPointsServiceClientStub, OrderServiceClientStub, PaymentServiceClientStub}
import com.vladkopanev.zio.saga.example.dao.SagaLogDaoImpl
import com.vladkopanev.zio.saga.example.endpoint.SagaEndpoint
import zio.interop.catz._
import zio.console.putStrLn
import zio.{App, ExitCode, ZEnv, ZIO}

import scala.concurrent.ExecutionContext

object SagaApp extends App {

  import org.http4s.server.blaze._

  implicit val runtime = this

  override def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] = {
    val flakyClient         = sys.env.getOrElse("FLAKY_CLIENT", "false").toBoolean
    val clientMaxReqTimeout = sys.env.getOrElse("CLIENT_MAX_REQUEST_TIMEOUT_SEC", "10").toInt
    val sagaMaxReqTimeout   = sys.env.getOrElse("SAGA_MAX_REQUEST_TIMEOUT_SEC", "12").toInt

    (for {
      paymentService <- PaymentServiceClientStub(clientMaxReqTimeout, flakyClient)
      loyaltyPoints  <- LoyaltyPointsServiceClientStub(clientMaxReqTimeout, flakyClient)
      orderService   <- OrderServiceClientStub(clientMaxReqTimeout, flakyClient)
      logDao         = new SagaLogDaoImpl
      orderSEC       <- OrderSagaCoordinatorImpl(paymentService, loyaltyPoints, orderService, logDao, sagaMaxReqTimeout)
      app            = new SagaEndpoint(orderSEC).service
      _              <- orderSEC.recoverSagas.fork
      _              <- BlazeServerBuilder[TaskC](ExecutionContext.global).bindHttp(8042).withHttpApp(app).serve.compile.drain
    } yield ()).foldM(
      e => putStrLn(s"Saga Coordinator fails with error $e, stopping server...").as(ExitCode.failure),
      _ => putStrLn(s"Saga Coordinator finished successfully, stopping server...").as(ExitCode.success)
    )
  }

}


================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/client/LoyaltyPointsServiceClient.scala
================================================
package com.vladkopanev.zio.saga.example.client

import java.util.UUID

import com.vladkopanev.zio.saga.example.TaskC
import io.chrisdavenport.log4cats.Logger
import io.chrisdavenport.log4cats.slf4j.Slf4jLogger
import zio.Task

trait LoyaltyPointsServiceClient {

  def assignLoyaltyPoints(userId: UUID, amount: Double, traceId: String): TaskC[Unit]

  def cancelLoyaltyPoints(userId: UUID, amount: Double, traceId: String): TaskC[Unit]
}

class LoyaltyPointsServiceClientStub(logger: Logger[Task], maxRequestTimeout: Int, flaky: Boolean)
    extends LoyaltyPointsServiceClient {

  override def assignLoyaltyPoints(userId: UUID, amount: Double, traceId: String): TaskC[Unit] =
    for {
      _ <- randomSleep(maxRequestTimeout)
      _ <- randomFail("assignLoyaltyPoints").when(flaky)
      _ <- logger.info(s"Loyalty points assigned to user $userId")
    } yield ()

  override def cancelLoyaltyPoints(userId: UUID, amount: Double, traceId: String): TaskC[Unit] =
    for {
      _ <- randomSleep(maxRequestTimeout)
      _ <- randomFail("cancelLoyaltyPoints").when(flaky)
      _ <- logger.info(s"Loyalty points canceled for user $userId")
    } yield ()

}

object LoyaltyPointsServiceClientStub {

  import zio.interop.catz._

  def apply(maxRequestTimeout: Int, flaky: Boolean): Task[LoyaltyPointsServiceClientStub] =
    Slf4jLogger.create[Task].map(new LoyaltyPointsServiceClientStub(_, maxRequestTimeout, flaky))
}


================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/client/OrderServiceClient.scala
================================================
package com.vladkopanev.zio.saga.example.client

import java.util.UUID

import com.vladkopanev.zio.saga.example.TaskC
import io.chrisdavenport.log4cats.Logger
import io.chrisdavenport.log4cats.slf4j.Slf4jLogger
import zio.Task

trait OrderServiceClient {

  def closeOrder(userId: UUID, orderId: BigInt, traceId: String): TaskC[Unit]

  def reopenOrder(userId: UUID, orderId: BigInt, traceId: String): TaskC[Unit]
}

class OrderServiceClientStub(logger: Logger[Task], maxRequestTimeout: Int, flaky: Boolean) extends OrderServiceClient {

  override def closeOrder(userId: UUID, orderId: BigInt, traceId: String): TaskC[Unit] =
    for {
      _ <- randomSleep(maxRequestTimeout)
      _ <- randomFail("closeOrder").when(flaky)
      _ <- logger.info(s"Order #$orderId closed")
    } yield ()

  override def reopenOrder(userId: UUID, orderId: BigInt, traceId: String): TaskC[Unit] =
    for {
      _ <- randomSleep(maxRequestTimeout)
      _ <- randomFail("reopenOrder").when(flaky)
      _ <- logger.info(s"Order #$orderId reopened")
    } yield ()
}

object OrderServiceClientStub {

  import zio.interop.catz._

  def apply(maxRequestTimeout: Int, flaky: Boolean): Task[OrderServiceClientStub] =
    Slf4jLogger.create[Task].map(new OrderServiceClientStub(_, maxRequestTimeout, flaky))
}


================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/client/PaymentServiceClient.scala
================================================
package com.vladkopanev.zio.saga.example.client

import java.util.UUID

import com.vladkopanev.zio.saga.example.TaskC
import io.chrisdavenport.log4cats.Logger
import io.chrisdavenport.log4cats.slf4j.Slf4jLogger
import zio.Task

trait PaymentServiceClient {

  def collectPayments(userId: UUID, amount: BigDecimal, traceId: String): TaskC[Unit]

  def refundPayments(userId: UUID, amount: BigDecimal, traceId: String): TaskC[Unit]
}

class PaymentServiceClientStub(logger: Logger[Task], maxRequestTimeout: Int, flaky: Boolean)
    extends PaymentServiceClient {

  override def collectPayments(userId: UUID, amount: BigDecimal, traceId: String): TaskC[Unit] =
    for {
      _ <- randomSleep(maxRequestTimeout)
      _ <- randomFail("collectPayments").when(flaky)
      _ <- logger.info(s"Payments collected from user #$userId")
    } yield ()

  override def refundPayments(userId: UUID, amount: BigDecimal, traceId: String): TaskC[Unit] =
    for {
      _ <- randomSleep(maxRequestTimeout)
      _ <- randomFail("refundPayments").when(flaky)
      _ <- logger.info(s"Payments refunded to user #$userId")
    } yield ()
}

object PaymentServiceClientStub {

  import zio.interop.catz._

  def apply(maxRequestTimeout: Int, flaky: Boolean): Task[PaymentServiceClient] =
    Slf4jLogger.create[Task].map(new PaymentServiceClientStub(_, maxRequestTimeout, flaky))
}


================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/client/client.scala
================================================
package com.vladkopanev.zio.saga.example

import zio.{ Task, ZIO }

import scala.util.Random

package object client {

  import zio.duration._

  def randomSleep(maxTimeout: Int): TaskC[Unit] =
    for {
      randomSeconds <- ZIO.effectTotal(Random.nextInt(maxTimeout))
      _             <- ZIO.sleep(randomSeconds.seconds)
    } yield ()

  def randomFail(operationName: String): Task[Unit] =
    for {
      randomInt <- ZIO.effectTotal(Random.nextInt(100))
      _         <- if (randomInt % 10 == 0) ZIO.fail(new RuntimeException(s"Failed to execute $operationName")) else ZIO.unit
    } yield ()
}


================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/dao/SagaLogDao.scala
================================================
package com.vladkopanev.zio.saga.example.dao

import java.util.UUID

import com.vladkopanev.zio.saga.example.model.{ SagaInfo, SagaStep }
import doobie.implicits.javatime.JavaTimeInstantMeta
import io.circe.Json
import org.postgresql.util.PGobject
import zio.{ Task, ZIO }

trait SagaLogDao {
  def finishSaga(sagaId: Long): ZIO[Any, Throwable, Unit]

  def startSaga(initiator: UUID, data: Json): ZIO[Any, Throwable, Long]

  def createSagaStep(
    name: String,
    sagaId: Long,
    result: Option[Json],
    failure: Option[String] = None
  ): ZIO[Any, Throwable, Unit]

  def listExecutedSteps(sagaId: Long): ZIO[Any, Throwable, List[SagaStep]]

  def listUnfinishedSagas: ZIO[Any, Throwable, List[SagaInfo]]
}

class SagaLogDaoImpl extends SagaLogDao {
  import doobie._
  import doobie.implicits._
  import doobie.postgres.implicits._
  import zio.interop.catz._

  val xa = Transactor.fromDriverManager[Task](
    "org.postgresql.Driver",
    "jdbc:postgresql:Saga",
    "postgres",
    "root"
  )
  implicit val han = LogHandler.jdkLogHandler

  override def finishSaga(sagaId: Long): ZIO[Any, Throwable, Unit] =
    sql"""UPDATE saga SET "finishedAt" = now() WHERE id = $sagaId""".update.run.transact(xa).unit

  override def startSaga(initiator: UUID, data: Json): ZIO[Any, Throwable, Long] =
    sql"""INSERT INTO saga("initiator", "createdAt", "finishedAt", "data", "type") 
          VALUES ($initiator, now(), null, $data, 'order')""".update
      .withUniqueGeneratedKeys[Long]("id")
      .transact(xa)

  override def createSagaStep(
    name: String,
    sagaId: Long,
    result: Option[Json],
    failure: Option[String]
  ): ZIO[Any, Throwable, Unit] =
    sql"""INSERT INTO saga_step("sagaId", "name", "result", "finishedAt", "failure")
          VALUES ($sagaId, $name, $result, now(), $failure)""".update.run
      .transact(xa)
      .unit

  override def listExecutedSteps(sagaId: Long): ZIO[Any, Throwable, List[SagaStep]] =
    sql"""SELECT "sagaId", "name", "finishedAt", "result", "failure"
          from saga_step WHERE "sagaId" = $sagaId""".query[SagaStep].to[List].transact(xa)

  override def listUnfinishedSagas: ZIO[Any, Throwable, List[SagaInfo]] =
    sql"""SELECT "id", "initiator", "createdAt", "finishedAt", "data", "type"
          from saga s WHERE "finishedAt" IS NULL""".query[SagaInfo].to[List].transact(xa)

  implicit lazy val JsonMeta: Meta[Json] = {
    import io.circe.parser._
    Meta.Advanced
      .other[PGobject]("jsonb")
      .timap[Json](pgObj => parse(pgObj.getValue).fold(e => sys.error(e.message), identity)) { json =>
        val pgObj = new PGobject
        pgObj.setType("jsonb")
        pgObj.setValue(json.noSpaces)
        pgObj
      }
  }

}


================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/endpoint/SagaEndpoint.scala
================================================
package com.vladkopanev.zio.saga.example.endpoint

import com.vladkopanev.zio.saga.example.{ OrderSagaCoordinator, TaskC }
import com.vladkopanev.zio.saga.example.model.OrderInfo
import org.http4s.circe._
import org.http4s.dsl.Http4sDsl
import org.http4s.implicits._
import org.http4s.{ HttpApp, HttpRoutes }
import zio.interop.catz._

final class SagaEndpoint(orderSagaCoordinator: OrderSagaCoordinator) extends Http4sDsl[TaskC] {

  private implicit val decoder = jsonOf[TaskC, OrderInfo]

  val service: HttpApp[TaskC] = HttpRoutes
    .of[TaskC] {
      case req @ POST -> Root / "saga" / "finishOrder" =>
        for {
          OrderInfo(userId, orderId, money, bonuses) <- req.as[OrderInfo]
          resp <- orderSagaCoordinator
                   .runSaga(userId, orderId, money, bonuses, None)
                   .foldM(fail => InternalServerError(fail.getMessage), _ => Ok("Saga submitted"))
        } yield resp
    }
    .orNotFound
}


================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/example.scala
================================================
package com.vladkopanev.zio.saga
import zio.RIO
import zio.clock.Clock

package object example {
  type TaskC[+A] = RIO[Clock, A]
}


================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/model/OrderInfo.scala
================================================
package com.vladkopanev.zio.saga.example.model

import java.util.UUID


case class OrderInfo(userId: UUID, orderId: BigInt, money: BigDecimal, bonuses: Double)

object OrderInfo {
  import io.circe._
  import io.circe.generic.semiauto._
  implicit val OrderInfoDecoder: Decoder[OrderInfo] = deriveDecoder[OrderInfo]
}


================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/model/OrderSagaError.scala
================================================
package com.vladkopanev.zio.saga.example.model

class OrderSagaError(message: String) extends RuntimeException(s"Saga failed with message: $message")


================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/model/SagaInfo.scala
================================================
package com.vladkopanev.zio.saga.example.model

import java.time.Instant
import java.util.UUID

import io.circe.Json

case class SagaInfo(id: Long,
                    initiator: UUID,
                    createdAt: Instant,
                    finishedAt: Option[Instant],
                    data: Json,
                    `type`: String)

case class OrderSagaData(userId: UUID, orderId: BigInt, money: BigDecimal, bonuses: Double)

object OrderSagaData {
  import io.circe._, io.circe.generic.semiauto._
  implicit val decoder: Decoder[OrderSagaData] = deriveDecoder[OrderSagaData]
  implicit val encoder: Encoder[OrderSagaData] = deriveEncoder[OrderSagaData]
}


================================================
FILE: examples/src/main/scala/com/vladkopanev/zio/saga/example/model/SagaStep.scala
================================================
package com.vladkopanev.zio.saga.example.model

import java.time.Instant

import io.circe.Json

case class SagaStep(sagaId: Long, name: String, finishedAt: Instant, result: Option[Json], failure: Option[String])


================================================
FILE: project/Versions.scala
================================================
object Versions {

  val Circe = "0.14.1"
  val Doobie = "1.0.0-RC1"
  val Http4s = "0.23.6"
  val Log4Cats = "2.1.1"
  val Zio = "2.0.0-RC1"
}


================================================
FILE: project/build.properties
================================================
sbt.version = 1.8.3

================================================
FILE: project/plugins.sbt
================================================
addSbtPlugin("org.scalameta" % "sbt-scalafmt"      % "2.3.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage"     % "2.0.7")
addSbtPlugin("ch.epfl.scala" % "sbt-release-early" % "2.1.1")

// from https://github.com/sbt/sbt/issues/6745#issuecomment-1442315151
libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always
Download .txt
gitextract_1zrhpsqz/

├── .github/
│   └── workflows/
│       └── scala.yml
├── .gitignore
├── .scalafmt.conf
├── .travis.yml
├── LICENSE
├── README.md
├── build.sbt
├── core/
│   └── src/
│       ├── main/
│       │   └── scala/
│       │       └── com/
│       │           └── vladkopanev/
│       │               └── zio/
│       │                   └── saga/
│       │                       └── Saga.scala
│       └── test/
│           └── scala/
│               └── com/
│                   └── vladkopanev/
│                       └── zio/
│                           └── saga/
│                               └── SagaSpec.scala
├── examples/
│   ├── README.md
│   └── src/
│       └── main/
│           ├── resources/
│           │   ├── application.conf
│           │   ├── logback.xml
│           │   └── sql/
│           │       └── saga.ddl
│           └── scala/
│               └── com/
│                   └── vladkopanev/
│                       └── zio/
│                           └── saga/
│                               └── example/
│                                   ├── OrderSagaCoordinator.scala
│                                   ├── SagaApp.scala
│                                   ├── client/
│                                   │   ├── LoyaltyPointsServiceClient.scala
│                                   │   ├── OrderServiceClient.scala
│                                   │   ├── PaymentServiceClient.scala
│                                   │   └── client.scala
│                                   ├── dao/
│                                   │   └── SagaLogDao.scala
│                                   ├── endpoint/
│                                   │   └── SagaEndpoint.scala
│                                   ├── example.scala
│                                   └── model/
│                                       ├── OrderInfo.scala
│                                       ├── OrderSagaError.scala
│                                       ├── SagaInfo.scala
│                                       └── SagaStep.scala
├── project/
│   ├── Versions.scala
│   ├── build.properties
│   └── plugins.sbt
└── travis/
    └── secrets.tar.enc
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (77K chars).
[
  {
    "path": ".github/workflows/scala.yml",
    "chars": 885,
    "preview": "# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n"
  },
  {
    "path": ".gitignore",
    "chars": 331,
    "preview": "project/zecret\nproject/travis-deploy-key\nproject/secrets.tar.xz\ntarget\ntest-output/\n.sbtopts\nproject/.sbt\ntest-output/\nl"
  },
  {
    "path": ".scalafmt.conf",
    "chars": 596,
    "preview": "version = \"3.0.7\"\n\nmaxColumn = 120\nalign {\n  preset = more\n  tokens = [{code = \"->\"}, {code = \"<-\"}, {code = \"=>\", owner"
  },
  {
    "path": ".travis.yml",
    "chars": 3306,
    "preview": "sudo: false\n\nlanguage: scala\n\nmatrix:\n  include:\n    # Scala 2.12, JVM\n    - scala: 2.12.17\n      jdk: openjdk8\n    - sc"
  },
  {
    "path": "LICENSE",
    "chars": 1075,
    "preview": "MIT License\n\nCopyright (c) 2019 Kopaniev Vladyslav\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "README.md",
    "chars": 10028,
    "preview": "# ZIO-SAGA\n\n> [!WARNING]\n> This project is no longer supported. For implementing real world sagas consider workflow orch"
  },
  {
    "path": "build.sbt",
    "chars": 3683,
    "preview": "import com.typesafe.sbt.SbtPgp.autoImportImpl.pgpSecretRing\nimport sbt.file\n\nname := \"zio-saga\"\n\nval mainScala = \"2.13.9"
  },
  {
    "path": "core/src/main/scala/com/vladkopanev/zio/saga/Saga.scala",
    "chars": 10647,
    "preview": "package com.vladkopanev.zio.saga\n\nimport com.vladkopanev.zio.saga.Saga.Compensator\nimport zio.Clock\nimport zio.{Cause, E"
  },
  {
    "path": "core/src/test/scala/com/vladkopanev/zio/saga/SagaSpec.scala",
    "chars": 22461,
    "preview": "package com.vladkopanev.zio.saga\n\nimport java.util.concurrent.TimeUnit\nimport com.vladkopanev.zio.saga.Saga.Compensator\n"
  },
  {
    "path": "examples/README.md",
    "chars": 448,
    "preview": "This is the example that extends one provided in the parent REAMDE file.\n\nThis projects shows one of the implementation "
  },
  {
    "path": "examples/src/main/resources/application.conf",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "examples/src/main/resources/logback.xml",
    "chars": 359,
    "preview": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            "
  },
  {
    "path": "examples/src/main/resources/sql/saga.ddl",
    "chars": 642,
    "preview": "CREATE TABLE public.saga\n(\n    id sequence,\n    initiator uuid NOT NULL,\n    \"createdAt\" timestamp without time zone NOT"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/OrderSagaCoordinator.scala",
    "chars": 6064,
    "preview": "package com.vladkopanev.zio.saga.example\n\nimport java.util.UUID\n\nimport com.vladkopanev.zio.saga.example.client.{\n    Lo"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/SagaApp.scala",
    "chars": 1773,
    "preview": "package com.vladkopanev.zio.saga.example\nimport com.vladkopanev.zio.saga.example.client.{LoyaltyPointsServiceClientStub,"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/client/LoyaltyPointsServiceClient.scala",
    "chars": 1425,
    "preview": "package com.vladkopanev.zio.saga.example.client\n\nimport java.util.UUID\n\nimport com.vladkopanev.zio.saga.example.TaskC\nim"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/client/OrderServiceClient.scala",
    "chars": 1292,
    "preview": "package com.vladkopanev.zio.saga.example.client\n\nimport java.util.UUID\n\nimport com.vladkopanev.zio.saga.example.TaskC\nim"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/client/PaymentServiceClient.scala",
    "chars": 1365,
    "preview": "package com.vladkopanev.zio.saga.example.client\n\nimport java.util.UUID\n\nimport com.vladkopanev.zio.saga.example.TaskC\nim"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/client/client.scala",
    "chars": 606,
    "preview": "package com.vladkopanev.zio.saga.example\n\nimport zio.{ Task, ZIO }\n\nimport scala.util.Random\n\npackage object client {\n\n "
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/dao/SagaLogDao.scala",
    "chars": 2716,
    "preview": "package com.vladkopanev.zio.saga.example.dao\n\nimport java.util.UUID\n\nimport com.vladkopanev.zio.saga.example.model.{ Sag"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/endpoint/SagaEndpoint.scala",
    "chars": 948,
    "preview": "package com.vladkopanev.zio.saga.example.endpoint\n\nimport com.vladkopanev.zio.saga.example.{ OrderSagaCoordinator, TaskC"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/example.scala",
    "chars": 132,
    "preview": "package com.vladkopanev.zio.saga\nimport zio.RIO\nimport zio.clock.Clock\n\npackage object example {\n  type TaskC[+A] = RIO["
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/model/OrderInfo.scala",
    "chars": 318,
    "preview": "package com.vladkopanev.zio.saga.example.model\n\nimport java.util.UUID\n\n\ncase class OrderInfo(userId: UUID, orderId: BigI"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/model/OrderSagaError.scala",
    "chars": 150,
    "preview": "package com.vladkopanev.zio.saga.example.model\n\nclass OrderSagaError(message: String) extends RuntimeException(s\"Saga fa"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/model/SagaInfo.scala",
    "chars": 666,
    "preview": "package com.vladkopanev.zio.saga.example.model\n\nimport java.time.Instant\nimport java.util.UUID\n\nimport io.circe.Json\n\nca"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/model/SagaStep.scala",
    "chars": 212,
    "preview": "package com.vladkopanev.zio.saga.example.model\n\nimport java.time.Instant\n\nimport io.circe.Json\n\ncase class SagaStep(saga"
  },
  {
    "path": "project/Versions.scala",
    "chars": 144,
    "preview": "object Versions {\n\n  val Circe = \"0.14.1\"\n  val Doobie = \"1.0.0-RC1\"\n  val Http4s = \"0.23.6\"\n  val Log4Cats = \"2.1.1\"\n  "
  },
  {
    "path": "project/build.properties",
    "chars": 19,
    "preview": "sbt.version = 1.8.3"
  },
  {
    "path": "project/plugins.sbt",
    "chars": 348,
    "preview": "addSbtPlugin(\"org.scalameta\" % \"sbt-scalafmt\"      % \"2.3.0\")\naddSbtPlugin(\"org.scoverage\" % \"sbt-scoverage\"     % \"2.0."
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the VladKopanev/zio-saga GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 30 files (70.9 KB), approximately 21.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!