示例
探索在 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.catch(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:
{
message: undefined,
_tag: 'TimeoutError',
'~effect/Cause/TimeoutError': '~effect/Cause/TimeoutError'
}
*/
// Test case: API returning an error response
Effect.runFork(program("https://dummyjson.com/auth/products/1?delay=500"))
/*
Output:
error
error
error
{
message: 'An error occurred in Effect.tryPromise',
cause: Error: ...,
_tag: 'UnknownError',
'~effect/Cause/UnknownError': '~effect/Cause/UnknownError'
}
*/
根据特定错误重试 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.catch(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.max([
Schedule.identity<TooManyRequestsError>().pipe(
Schedule.addDelay(({ output: error }) =>
Effect.succeed(
error._tag === "TooManyRequestsError"
? // Wait for the specified retry-after duration
Duration.millis(error.retryAfter)
: Duration.zero,
),
),
),
// Limit retries to 5 attempts
Schedule.recurs(5),
])
const program = request.pipe(Effect.retry(policy))
const result = await Effect.runPromise(program)
/*
Output:
Attempt #1, retry after 500 millis...
Attempt #2, retry after 1000 millis...
Done
*/
result // => "some result"
周期性运行任务直到另一个任务完成
在某些情况下,我们需要以固定间隔重复执行某个动作,直到另一个运行时间更长的任务完成。这种模式常见于轮询机制或周期性日志记录。
示例(运行一个计划任务直到完成)
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)
const result = await Effect.runPromise(program)
/*
Output:
action...
action...
action...
action...
done
*/
result // => undefined