1. Introduction

ZIO is a popular library in Scala to build concurrent and asynchronous applications. ZIO provides a distinctive approach to building resilient applications using functional programming concepts.

The core concept of ZIO is its data structure ZIO, which provides a lot of in-built methods to handle different operations with ease. In this tutorial, we’ll look into one of ZIO’s most useful features: retry and repeat operations of the ZIO effects.

2. Setup

Before we get started with the code, we’ll set up a project with the required ZIO dependency. We can add the dependency to the build.sbt file:

libraryDependencies += "dev.zio" %% "zio" % "2.0.16"

3. ZIO Repeat

ZIO has a built-in scheduler to schedule operations. It provides a method called repeat to perform the same operations multiple times.

There are various variations of repeat operations. We’ll look into some of the popular ones in this section.

3.1. repeat

We can invoke the method repeat() on a ZIO action to perform it multiple times. Let’s create a simple ZIO effect and see it in action:

val simpleZio: ZIO[Any, Nothing, Unit] = ZIO.succeed(println("Hello ZIO!"))

We can invoke the repeat method on simpleZio by passing in the scheduler:

val repeat_recurs = simpleZio.repeat(Schedule.recurs(3))

The recurs method on Schedule takes the number of times the action needs to be repeated. We can combine all of the code into a runnable application:

object RepeatSamples extends ZIOAppDefault {
  override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] = {
    val simpleZio: ZIO[Any, Nothing, Unit] = ZIO.succeed(println("Hello ZIO!"))
    simpleZio.repeat(Schedule.recurs(3))
  }
}

When we execute this, the statement Hello ZIO! gets printed four times in the console. Notably, the action gets repeated in addition to the original effect. That’s why it prints the statement four times.

Furthermore, repetition doesn’t occur if the action fails at any point:

override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] = {
  val aFailingZIO = ZIO.attempt {
    println("a failing action here")
    throw new Exception("Failure block")
  }
  aFailingZIO.repeat(Schedule.recurs(3))
}

Since the aFailingZIO call fails, the println statement is executed only once.

3.2. repeatN

Since repetition with a number is frequent, ZIO offers an alternative method that simplifies usage by accepting the desired number of repetitions. We can use the method repeatN() for this purpose:

val repeatN = simpleZio.repeatN(2)

This repeats the simpleZio action two more times.

3.3. repeatUntil and repeatWhile

We can repeat an action until a condition matches using the method repeatUntil:

val simpleZio: ZIO[Any, Nothing, Unit] = ZIO.succeed(println("Hello ZIO!"))
def isEven = scala.util.Random.nextInt(100) % 2 == 0
simpleZio.repeatUntil(_ => isEven)

The above code prints Hello ZIO! until the random generator generates an even number.

Similarly, we can repeat an action as long as the given predicate is true using the method repeatWhile:

simpleZio.repeatWhile(_ => isEven)

Different versions of repeatUntil and repeatWhile exist, which expect a ZIO effect instead of a basic value. These versions are named repeatUntilZIO and repeatWhileZIO:

def isOdd: ZIO[Any, Nothing, Boolean] = ZIO.succeed(Random.nextInt(100) % 2 == 1)
for {
  repeatUntilZIO <- simpleZio.repeatUntilZIO(_ => isOdd) 
  repeatWhileZIO <- simpleZio.repeatWhileZIO(_ => isOdd)
} yield()

isOdd returns a ZIO effect used in the repeatUntilZIO and repeatWhileZIO methods.

4. ZIO Retry

Retry is the process of repeatedly attempting an operation that has initially failed, continuing the attempts until either the operation succeeds or a specific condition is met.

ZIO provides several in-built methods to retry failed operations.

In this section, we’ll look at some useful methods for retrying failed effects.

4.1. retry

We can use the method retry on a ZIO effect by providing a scheduler. The scheduler value determines the number of re-attempts. We’ll create a simple effect that may fail:

val mayBeFailingZIO = for {
  num <- Random.nextIntBounded(100)
  _ <- zio.Console.printLine("Calculating with number: " + num)
  _ <- ZIO.when(num % 3 != 0)(ZIO.fail(new Exception(s"$num is not an multiple of 3!")))
} yield num

The above code only works if the generated number is a multiple of 3. We can apply the retry method to this effect:

mayBeFailingZIO.retry(Schedule.recurs(3))

In the event of a failure, we retry the effect up to 3 times (in addition to the initial execution).

The ZIO scheduler provides a lot of options to create schedules. For example, we can create a schedule that follows exponential backoff and apply it to the ZIO effect:

mayBeFailingZIO.retry(Schedule.exponential(zio.Duration.fromSeconds(3)))

Similarly, methods such as Fibonacci back-off, fixed-interval schedules, and so on can enrich the retry functionality. Additionally, the same scheduler options can be applied to repeat operations.

4.2. retryN

Since count-based retries are the most popular, ZIO provides another method that takes the number of retries. This uses the recurs scheduler for attempting the retries:

mayBeFailingZIO.retryN(5)

This retries a maximum of 5 times if the initial action fails.

4.3. retryUntil and retryWhile

Instead of using a maximum number of attempts, we can retry an operation until a condition is met using the method retryUntil:

def canRetry = scala.util.Random.nextBoolean()
mayBeFailingZIO.retryUntil(_ => canRetry)

The above code retries the ZIO effect until the method canRetry returns false.

Similarly, we can use retryWhile to retry the effect while a condition matches.

There are also variations of the above methods that take ZIO values instead of simple values as conditions. These are retryUntilZIO and retryWhileZIO.

It’s worth noting that the ZIO effect keeps retrying indefinitely if the condition is never met.

4.4. retryOrElse

ZIO provides another method to retryOrElse to execute a default handler if all the retries fail. Let’s rewrite the above example to return the value as 0 if the three retries fail:

mayBeFailingZIO.retryOrElse(
  policy = Schedule.recurs(3),
  orElse = (err, value: Long) =>
    zio.Console.printLine(
      s"Terminating retries since we are not able to succeed even after three attempts. Returning 0 "
    ) *> ZIO.succeed(0L)
)

In the above case, if all three retries fail, it executes the orElse handler and exits the scheduler.

5. Conclusion

In this article, we learned the difference between repeat and retry operations in ZIO. We also discussed different variations of repeat and retry methods provided by ZIO. Moreover, we looked at the different scheduler options that we can use in repeat and retry operations.

As always, the code used in this tutorial is available over on GitHub.