已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

示例

探索在 Effect 中处理调度、重试、超时以及周期性任务执行的实用示例。

以下示例展示了使用 Effect 处理超时、重试和周期性执行的不同方式。每个场景都确保应用保持响应能力、能从失败中恢复,同时动态适应各种情况。

处理 API 调用的超时与重试

在调用第三方 API 时,通常需要设置超时并实现重试机制,以应对临时性失败。在本例中,API 调用在失败时最多重试两次;如果耗时超过 4 秒,就会被中断。

示例(带超时的 API 调用重试)

import { Console, Effect } from "effect"

// Function to make the API call
const getJson = (url: string) =>
  Effect.tryPromise(() =>
    fetch(url).then((res) => {
      if (!res.ok) {
        console.log("error")
        throw new Error(res.statusText)
      }
      console.log("ok")
      return res.json() as unknown
    }),
  )

// Program that retries the API call twice, times out after 4 seconds,
// and logs errors
const program = (url: string) =>
  getJson(url).pipe(
    Effect.retry({ times: 2 }),
    Effect.timeout("4 seconds"),
    Effect.catchAll(Console.error),
  )

// Test case: successful API response
Effect.runFork(program("https://dummyjson.com/products/1?delay=1000"))
/*
Output:
ok
*/

// Test case: API call exceeding timeout limit
Effect.runFork(program("https://dummyjson.com/products/1?delay=5000"))
/*
Output:
TimeoutException: Operation timed out before the specified duration of '4s' elapsed
*/

// Test case: API returning an error response
Effect.runFork(program("https://dummyjson.com/auth/products/1?delay=500"))
/*
Output:
error
error
error
UnknownException: An unknown error occurred
*/

根据特定错误重试 API 调用

有时,只应针对某些错误情况进行重试。例如,如果 API 调用以 401 Unauthorized 响应失败,重试可能是合理的;而 404 Not Found 错误则不应触发重试。

示例(仅针对特定错误码重试)

import { Console, Effect, Data } from "effect"

// Custom error class for handling status codes
class Err extends Data.TaggedError("Err")<{
  readonly message: string
  readonly status: number
}> {}

// Function to make the API call
const getJson = (url: string) =>
  Effect.tryPromise({
    try: () =>
      fetch(url).then((res) => {
        if (!res.ok) {
          console.log(res.status)
          throw new Err({ message: res.statusText, status: res.status })
        }
        return res.json() as unknown
      }),
    catch: (e) => e as Err,
  })

// Program that retries only when the error status is 401 (Unauthorized)
const program = (url: string) =>
  getJson(url).pipe(
    Effect.retry({ while: (err) => err.status === 401 }),
    Effect.catchAll(Console.error),
  )

// Test case: API returns 401 (triggers multiple retries)
Effect.runFork(program("https://dummyjson.com/auth/products/1?delay=1000"))
/*
Output:
401
401
401
401
...
*/

// Test case: API returns 404 (no retries)
Effect.runFork(program("https://dummyjson.com/-"))
/*
Output:
404
Err [Error]: Not Found
*/

根据错误信息动态调整重试延迟

某些 API 错误(例如 429 Too Many Requests)会带有 Retry-After 响应头,其中指定了重试前需要等待的时长。我们可以根据该值动态调整重试间隔,而不是使用固定延迟。

示例(使用 Retry-After 响应头决定重试延迟)

这种做法让重试延迟能够根据服务器的响应动态调整,在遵循所提供的 Retry-After 值的同时避免不必要的重试。

import { Duration, Effect, Schedule, Data } from "effect"

// Custom error class representing a "Too Many Requests" response
class TooManyRequestsError extends Data.TaggedError("TooManyRequestsError")<{
  readonly retryAfter: number
}> {}

let n = 1
const request = Effect.gen(function* () {
  // Simulate failing a particular number of times
  if (n < 3) {
    const retryAfter = n * 500
    console.log(`Attempt #${n++}, retry after ${retryAfter} millis...`)
    // Simulate retrieving the retry-after header
    return yield* Effect.fail(new TooManyRequestsError({ retryAfter }))
  }
  console.log("Done")
  return "some result"
})

// Retry policy that extracts the retry delay from the error
const policy = Schedule.identity<TooManyRequestsError>().pipe(
  Schedule.addDelay((error) =>
    error._tag === "TooManyRequestsError"
      ? // Wait for the specified retry-after duration
        Duration.millis(error.retryAfter)
      : Duration.zero,
  ),
  // Limit retries to 5 attempts
  Schedule.intersect(Schedule.recurs(5)),
)

const program = request.pipe(Effect.retry(policy))

Effect.runFork(program)
/*
Output:
Attempt #1, retry after 500 millis...
Attempt #2, retry after 1000 millis...
Done
*/

运行周期性任务直到另一个任务完成

有些情况下,我们需要按固定间隔重复执行某个动作,直到另一个耗时更长的任务完成。这种模式常见于轮询机制或周期性日志记录。

示例(运行定时任务直到完成)

import { Effect, Console, Schedule } from "effect"

// Define a long-running effect
// (e.g., a task that takes 5 seconds to complete)
const longRunningEffect = Console.log("done").pipe(Effect.delay("5 seconds"))

// Define an action to run periodically
const action = Console.log("action...")

// Define a fixed interval schedule
const schedule = Schedule.fixed("1.5 seconds")

// Run the action repeatedly until the long-running task completes
const program = Effect.race(Effect.repeat(action, schedule), longRunningEffect)

Effect.runPromise(program)
/*
Output:
action...
action...
action...
action...
done
*/