Skip to content
Source
Sign inSign up

10_schedules.ts

111 lines3.6 KB

1/**
2 * @title Working with the Schedule module
3 *
4 * Build schedules, compose them, and use them with `Effect.retry` and `Effect.repeat`.
5 */
6import { Duration, Effect, Random, Schedule, Schema } from "effect"
7​
8// Production pattern: capped exponential backoff with jitter and at most 5 retries (6 attempts).
9// Base delays start at 250ms and grow exponentially, capped at 10s before jitter.
10export const productionRetrySchedule = Schedule.min([
11 Schedule.exponential("250 millis"),
12 // Cap the delay at 10 seconds to avoid excessively long waits.
13 Schedule.spaced("10 seconds")
14]).pipe(
15 Schedule.jittered,
16 Schedule.upTo({ times: 5 }),
17 Schedule.setInputType<HttpError>(),
18 Schedule.while(({ input }) => input.retryable)
20​
21export const fetchUserProfile = Effect.fn("fetchUserProfile")(
22 function*(userId: string) {
23 const random = yield* Random.next
24 const status = random > 0.7
25 ? 200
26 : random > 0.3
27 ? 503
28 : 401
29​
30 if (status !== 200) {
31 return yield* new HttpError({
32 message: `Request for ${userId} failed`,
33 status,
34 retryable: status >= 500
35 })
36 }
37​
38 return {
39 id: userId,
40 name: "Ada Lovelace"
41 } as const
42 }
44​
45// Use the schedule with `Effect.retry` to retry failures.
46export const loadUserWithRetry = fetchUserProfile("user-123").pipe(
47 Effect.retry(productionRetrySchedule),
48 // Turn the final error into a defect, either after exhausting the retries
49 // or immediately for a non-retryable error.
50 Effect.orDie
52​
53export const loadUserWithInferredInput = fetchUserProfile("user-123").pipe(
54 // You can also pass a schedule builder function that assists with inferring
55 // the input type. This is especially useful when the schedule needs to
56 // inspect the error to determine retryability.
57 Effect.retry(($) =>
58 $(Schedule.spaced("1 seconds")).pipe(
59 Schedule.while(({ input }) => input.retryable)
60 )
61 ),
62 Effect.orDie
64​
65export class HttpError extends Schema.TaggedError<HttpError>()("HttpError", {
66 message: Schema.String,
67 status: Schema.Int,
68 retryable: Schema.Boolean
69}) {}
70​
71// Start with a few schedule constructors.
72export const maxRetries = Schedule.recurs(5)
73export const spacedPolling = Schedule.spaced("30 seconds")
74export const exponentialBackoff = Schedule.exponential("200 millis")
75​
76// `Schedule.max` continues only while all schedules continue and outputs
77// the slowest delay. It is useful for combining a delay pattern with a hard
78// attempt cap.
79export const retryBackoffWithLimit = Schedule.max([
80 Schedule.exponential("250 millis"),
81 Schedule.recurs(6)
82])
83​
84// `Schedule.min` continues while any schedule continues and outputs
85// the fastest delay. It is useful for fallback behavior.
86//
87// This schedule will start at 500ms, exponentially increase, and will be capped
88// at 2 seconds.
89export const keepTryingUpToTwoSeconds = Schedule.min([
90 Schedule.exponential("500 millis"),
91 Schedule.spaced("2 seconds")
92])
93​
94// Use `Schedule.while` to continue only for retryable failures.
95// This lets non-retryable errors fail fast, even if attempts remain.
96export const retryableOnly = Schedule.exponential("200 millis").pipe(
97 // You can use `setInputType` to specify the type of input the schedule will
98 // receive.
99 Schedule.setInputType<HttpError>(),
100 Schedule.while(({ input }) => input.retryable)
102​
103// `tap` is useful for performing side effects like logging or metrics.
104export const instrumentedRetrySchedule = retryableOnly.pipe(
105 Schedule.setInputType<HttpError>(),
106 Schedule.tap((meta) =>
107 Effect.logDebug(
108 `Retrying after ${meta.input.status}: ${meta.input.message}. Next retry in ${Duration.toMillis(meta.duration)}ms`
109 )
110 )