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

超时

用 Effect 为操作设置时间限制,确保任务在指定时长内完成,并自定义超时时的行为。

在编程中,经常会遇到需要花一些时间才能完成的任务。我们往往希望给这些任务施加一个愿意等待的时长上限。Effect.timeout 函数可以为某个操作加上时间约束,确保它不会无限期地运行下去。

基本用法

timeout

Effect.timeout 函数接收一个 Duration 参数,用来为某个操作设定时间限制。如果该操作超出了这个限制,就会触发 TimeoutException,表示发生了超时。

示例(设置超时)

这里,任务在超时时长内完成,因此结果被成功返回。

import { Effect } from "effect"

const task = Effect.gen(function* () {
  console.log("Start processing...")
  yield* Effect.sleep("2 seconds") // Simulates a delay in processing
  console.log("Processing complete.")
  return "Result"
})

// Sets a 3-second timeout for the task
const timedEffect = task.pipe(Effect.timeout("3 seconds"))

// Output will show that the task completes successfully
// as it falls within the timeout duration
Effect.runPromiseExit(timedEffect).then(console.log)
/*
Output:
Start processing...
Processing complete.
{ _id: 'Exit', _tag: 'Success', value: 'Result' }
*/

如果操作超出了指定的时长,就会抛出 TimeoutException

import { Effect } from "effect"

const task = Effect.gen(function* () {
  console.log("Start processing...")
  yield* Effect.sleep("2 seconds") // Simulates a delay in processing
  console.log("Processing complete.")
  return "Result"
})

// Output will show a TimeoutException as the task takes longer
// than the specified timeout duration
const timedEffect = task.pipe(Effect.timeout("1 second"))

Effect.runPromiseExit(timedEffect).then(console.log)
/*
Output:
Start processing...
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: {
    _id: 'Cause',
    _tag: 'Fail',
    failure: { _tag: 'TimeoutException' }
  }
}
*/

timeoutOption

如果你希望更优雅地处理超时,可以考虑使用 Effect.timeoutOption。这个函数把超时当作普通结果来处理,并将结果包装进一个 Option 中。

示例(把超时当作 Option 处理)

在这个示例中,第一个任务成功完成,而第二个任务超时了。超时任务的结果在 Option 类型中表示为 None

import { Effect } from "effect"

const task = Effect.gen(function* () {
  console.log("Start processing...")
  yield* Effect.sleep("2 seconds") // Simulates a delay in processing
  console.log("Processing complete.")
  return "Result"
})

const timedOutEffect = Effect.all([
  task.pipe(Effect.timeoutOption("3 seconds")),
  task.pipe(Effect.timeoutOption("1 second")),
])

Effect.runPromise(timedOutEffect).then(console.log)
/*
Output:
Start processing...
Processing complete.
Start processing...
[
  { _id: 'Option', _tag: 'Some', value: 'Result' },
  { _id: 'Option', _tag: 'None' }
]
*/

处理超时

当某个操作没有在指定的时长内结束,Effect.timeout 的行为取决于该操作是否「不可中断」(uninterruptible)。

Uninterruptible Effects

不可中断的 effect 指的是:一旦启动,超时机制就无法在执行过程中直接将其停止。 这可能是因为该 effect 内部的操作必须运行到完成,以避免让系统处于不一致的状态。

  1. 可中断的操作:如果操作可以被中断,那么一旦达到超时阈值,它就会立即被终止,并产生一个 TimeoutException

    import { Effect } from "effect"
    
    const task = Effect.gen(function* () {
      console.log("Start processing...")
      yield* Effect.sleep("2 seconds") // Simulates a delay in processing
      console.log("Processing complete.")
      return "Result"
    })
    
    const timedEffect = task.pipe(Effect.timeout("1 second"))
    
    Effect.runPromiseExit(timedEffect).then(console.log)
    /*
     Output:
     Start processing...
     {
       _id: 'Exit',
       _tag: 'Failure',
       cause: {
         _id: 'Cause',
         _tag: 'Fail',
         failure: { _tag: 'TimeoutException' }
       }
     }
     */
  2. 不可中断的操作:如果操作不可中断,它会一直继续到完成为止,然后才会判定 TimeoutException

    import { Effect } from "effect"
    
    const task = Effect.gen(function* () {
      console.log("Start processing...")
      yield* Effect.sleep("2 seconds") // Simulates a delay in processing
      console.log("Processing complete.")
      return "Result"
    })
    
    const timedEffect = task.pipe(
      Effect.uninterruptible,
      Effect.timeout("1 second"),
    )
    
    // Outputs a TimeoutException after the task completes,
    // because the task is uninterruptible
    Effect.runPromiseExit(timedEffect).then(console.log)
    /*
     Output:
     Start processing...
     Processing complete.
     {
       _id: 'Exit',
       _tag: 'Failure',
       cause: {
         _id: 'Cause',
         _tag: 'Fail',
         failure: { _tag: 'TimeoutException' }
       }
     }
     */

超时时断开连接

Effect.disconnect 函数提供了一种更灵活的方式,用来处理不可中断 effect 中的超时。它允许不可中断的 effect 在后台继续完成,而主控制流则像已经发生超时一样继续向下执行。

两者的区别如下:

不使用 Effect.disconnect 时:

  • 不可中断的 effect 会忽略超时,继续执行直到完成,之后才会判定超时错误。
  • 这可能导致超时条件的识别被延迟,因为系统必须等待该 effect 完成。

使用 Effect.disconnect 时:

  • 允许不可中断的 effect 在后台继续运行,独立于主控制流。
  • 主控制流会立即识别出超时,并带着超时错误或替代逻辑继续执行,而不必等待该 effect 完成。
  • 当该 effect 中的操作虽然被标记为不可中断,却不需要阻塞程序继续执行时,这种方式尤其有用。

示例(运行不可中断任务并设置超时,同时在后台完成)

考虑这样一个场景:启动了一个长时间运行的数据处理任务,而你希望即使数据处理耗时过长,系统也能保持响应:

import { Effect } from "effect"

const longRunningTask = Effect.gen(function* () {
  console.log("Start heavy processing...")
  yield* Effect.sleep("5 seconds") // Simulate a long process
  console.log("Heavy processing done.")
  return "Data processed"
})

const timedEffect = longRunningTask.pipe(
  Effect.uninterruptible,
  // Allows the task to finish in the background if it times out
  Effect.disconnect,
  Effect.timeout("1 second"),
)

Effect.runPromiseExit(timedEffect).then(console.log)
/*
Output:
Start heavy processing...
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: {
    _id: 'Cause',
    _tag: 'Fail',
    failure: { _tag: 'TimeoutException' }
  }
}
Heavy processing done.
*/

在这个示例中,系统在一秒后检测到了超时,但长时间运行的任务会在后台继续执行并完成,不会阻塞程序的流程。

自定义超时行为

除了基础的 Effect.timeout 函数之外,还有若干变体可供使用,让你能够自定义发生超时时的行为。

timeoutFail

Effect.timeoutFail 函数允许你在发生超时时产生一个特定的错误。

示例(自定义超时错误)

import { Effect, Data } from "effect"

const task = Effect.gen(function* () {
  console.log("Start processing...")
  yield* Effect.sleep("2 seconds") // Simulates a delay in processing
  console.log("Processing complete.")
  return "Result"
})

class MyTimeoutError extends Data.TaggedError("MyTimeoutError")<{}> {}

const program = task.pipe(
  Effect.timeoutFail({
    duration: "1 second",
    onTimeout: () => new MyTimeoutError(), // Custom timeout error
  }),
)

Effect.runPromiseExit(program).then(console.log)
/*
Output:
Start processing...
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: {
    _id: 'Cause',
    _tag: 'Fail',
    failure: MyTimeoutError { _tag: 'MyTimeoutError' }
  }
}
*/

timeoutFailCause

Effect.timeoutFailCause 让你可以定义一个在发生超时时抛出的特定 defect。当你希望把超时当作代码中的异常情况来处理时,这会很有帮助。

示例(超时时抛出自定义 defect)

import { Effect, Cause } from "effect"

const task = Effect.gen(function* () {
  console.log("Start processing...")
  yield* Effect.sleep("2 seconds") // Simulates a delay in processing
  console.log("Processing complete.")
  return "Result"
})

const program = task.pipe(
  Effect.timeoutFailCause({
    duration: "1 second",
    onTimeout: () => Cause.die("Timed out!"), // Custom defect for timeout
  }),
)

Effect.runPromiseExit(program).then(console.log)
/*
Output:
Start processing...
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: { _id: 'Cause', _tag: 'Die', defect: 'Timed out!' }
}
*/

timeoutTo

Effect.timeout 相比,Effect.timeoutTo 提供了更大的灵活性,允许你分别为成功和超时的操作定义不同的结果。当你希望根据操作是否按时完成来自定义结果时,这会很有用。

示例(用 Either 处理成功与超时)

import { Effect, Either } from "effect"

const task = Effect.gen(function* () {
  console.log("Start processing...")
  yield* Effect.sleep("2 seconds") // Simulates a delay in processing
  console.log("Processing complete.")
  return "Result"
})

const program = task.pipe(
  Effect.timeoutTo({
    duration: "1 second",
    onSuccess: (result): Either.Either<string, string> => Either.right(result),
    onTimeout: (): Either.Either<string, string> => Either.left("Timed out!"),
  }),
)

Effect.runPromise(program).then(console.log)
/*
Output:
Start processing...
{
  _id: "Either",
  _tag: "Left",
  left: "Timed out!"
}
*/