Skip to main content
Version: ZIO 2.x

Scheduling

For scenarios where an action needs to be performed multiple times, Schedule can be used to customize the:

  • number of repetitions
  • rate of repetition
  • effect performed on each repetition

Retry strategy for HTTP requests#

One potential issue when dealing with a 3rd party API is the unreliability of a given endpoint. Since you have no control over the software, you cannot directly improve the reliability. Here's a mock request that has approximately a 70% chance of succeeding:

import zio.Taskimport java.util.Random
object API {  def makeRequest = Task.attempt {    if (new Random().nextInt(10) < 7) "some value" else throw new Exception("hi")  }}

One solution to improve reliability is to retry the request until success. There are many considerations:

  • What should the maximum number of attempts be?
  • How often should you make the request?
  • Do you want to log attempts?

Schedule can be used to address all of these concerns.

If you don't want to retry the request forever, create a schedule that specifies max number of attempts.

import zio.Schedule
Schedule.recurs(4)

The above schedule retries immediately after failing. Typically, you will want to space out your requests a bit to give the endpoint a chance to stabilize. There are many rates which you can use such as spaced, exponential, fibonacci, forever. For simplicity, we will retry the request every second.

import zio.durationIntimport zio.Schedule
Schedule.spaced(1.second)

You can compose the schedules using operators to create a more complex schedule:

import zio.Schedule
def schedule = Schedule.recurs(4) && Schedule.spaced(1.second)

For monitoring purposes, you may also want to log attempts. While this logic can be placed in the request itself, it's more scalable to add that logic to the schedule so it can be reused.

import zio.Console.printLineimport zio.Scheduleimport zio.Schedule.Decision
object ScheduleUtil {  def schedule[A] = Schedule.spaced(1.second) && Schedule.recurs(4).onDecision({    case (_, _, Decision.Done)              => printLine(s"done trying").orDie    case (_, attempt, Decision.Continue(_)) => printLine(s"attempt #$attempt").orDie  })}

You've now created a retry strategy that will attempt an effect every second for a maximum of 5 attempts while logging each attempt. The usage of the schedule would look like this:

import zio._import zio.Console._import ScheduleUtil._import API._
object ScheduleApp extends scala.App {
  implicit val rt: Runtime[Has[Clock] with Has[Console]] = Runtime.default
  rt.unsafeRun(makeRequest.retry(schedule).foldZIO(    ex => printLine("Exception Failed"),    v => printLine(s"Succeeded with $v"))  )}

The output of the above program where the request succeeds in time could be:

attempt #1attempt #2attempt #3Succeeded with some value

If the server is completely down with no chance of the request succeeding, the output would look like:

attempt #1attempt #2attempt #3attempt #4Exception Failed