Effect 的 RequestResolver 怎么设置收集窗口和批大小?

每隔 8ms 才到一个的请求,我实测各自成了一批,5 个请求 5 次查询。N+1 没有消失,只是换了个形式。

用 RequestResolver.setDelay(resolver, "20 millis") 给批一个收集窗口,再用 batchN 限制单批上限。 前者决定等多久,后者决定最多收多少;makeGrouped 则用于批内部必须按维度切开的场景。

RequestResolver 是 Effect 里把一批请求合并成一次查询的机制:调用方用 Effect.request 一个一个发请求,resolver 一次收一批,基本写法见 Effect 怎么把 N+1 查询合并成一批?。默认的批只收同一时刻已经挂起的请求,下面是错开到达时的处理。

请求错开到达时,setDelay 怎么用?

import { Effect, Exit, Request, RequestResolver } from "effect"

class GetAuthor extends Request.Class<{ readonly id: number }, string> {}

const batches: Array<ReadonlyArray<number>> = []
const base = RequestResolver.make<GetAuthor>((entries) =>
  Effect.sync(() => {
    batches.push(entries.map((e) => e.request.id))
    for (const e of entries) e.completeUnsafe(Exit.succeed(`author-${e.request.id}`))
  })
)

// 请求每隔 8ms 到达一个
const staggered = (r: RequestResolver.RequestResolver<GetAuthor>) =>
  Effect.forEach(
    [1, 2, 3, 4, 5],
    (id) =>
      Effect.sleep(`${id * 8} millis`).pipe(Effect.andThen(Effect.request(new GetAuthor({ id }), r))),
    { concurrency: "unbounded", discard: true }
  )

const program = Effect.gen(function*() {
  yield* staggered(base)
  console.log("no delay:", batches.length, JSON.stringify(batches)) // 5 [[1],[2],[3],[4],[5]]
  batches.length = 0

  yield* staggered(RequestResolver.setDelay(base, "20 millis"))
  console.log("delay 20ms:", batches.length, JSON.stringify(batches)) // 2 [[1,2,3],[4,5]]
})

Effect.runPromise(program)

setDelay 给批一个收集窗口:第一个请求到达后等待指定时间,期间到达的请求并入同一批。代价是第一个请求的延迟被抬高了一个窗口,所以窗口要按「可接受的额外延迟」来定,通常是几毫秒到几十毫秒。上面的例子里,20ms 窗口把 5 批压成 2 批([1,2,3] 与 [4,5])。还有 setDelayEffect,窗口可以是任意 Effect,比如等某个信号。

batchN 和 makeGrouped 分别什么时候用?

两个控制批形状的工具:

  • RequestResolver.batchN(resolver, n):单批最多 n 个,超过的拆到后面的批。适合下游接口有 IN (...) 上限的场景。我实测 6 个请求、n 等于 2 时得到 3 批
  • RequestResolver.makeGrouped({ key, resolver }):先按 key 分组,每组各跑一次。适合「同一批里的请求必须发往不同的分片、租户或表」。我实测按 id 奇偶分组时得到 2 批,奇数组与偶数组各一批

一般怎么组合?

最常用的组合是 setDelay 加 batchN:前者决定「等多久」,后者决定「最多收多少」。makeGrouped 则是在「批的内部结构」必须按维度切开时才用。

窗口会给第一个请求加上延迟,延迟极度敏感的路径要么不用,要么窗口设很小。批合并之后重复请求怎么去重,见主文章里的 withCache。

版本是 effect 4.0.5。上面的代码在 strict 加 exactOptionalPropertyTypes 下编译并运行过;batchN 与 makeGrouped 的批次数是我实测的,但这里没有贴出对应代码。