Effect 怎么避免一个租户占满许可、饿死其他租户?

我让租户 A 先提交 6 个任务,B 和 C 各 2 个,许可只有 1 个。普通信号量的完成顺序是 A1 A2 A3 A4 A5 A6 B1 B2 C1 C2,B 和 C 要等 A 全部跑完。

用 PartitionedSemaphore:它共享一个许可池,但按分区 key 记录等待者,释放许可时在各个等待的分区之间轮转分配,大租户就无法饿死小租户。

普通 Semaphore 是先到先得,基本用法见 Effect 里怎么用 Semaphore 限制并发数?。下面用同一组任务对比两者,再说 API 和边界。

分区信号量的顺序和普通信号量差多少?

import { Effect, PartitionedSemaphore, Semaphore } from "effect"

const run = (guard: (tenant: string) => <A, E, R>(e: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>) =>
  Effect.gen(function*() {
    const order: Array<string> = []
    const job = (tenant: string, i: number) =>
      Effect.sleep("5 millis").pipe(Effect.tap(() => Effect.sync(() => order.push(`${tenant}${i}`))))

    const plan: Array<[string, number]> = []
    for (let i = 1; i <= 6; i++) plan.push(["A", i])
    for (const tenant of ["B", "C"]) for (let i = 1; i <= 2; i++) plan.push([tenant, i])

    yield* Effect.forEach(plan, ([tenant, i]) => guard(tenant)(job(tenant, i)), {
      concurrency: "unbounded",
      discard: true
    })
    return order.join(" ")
  })

const program = Effect.gen(function*() {
  const plain = yield* Semaphore.make(1)
  console.log("plain      ", yield* run(() => plain.withPermits(1)))
  // plain       A1 A2 A3 A4 A5 A6 B1 B2 C1 C2

  const partitioned = yield* PartitionedSemaphore.make<string>({ permits: 1 })
  console.log("partitioned", yield* run((tenant) => partitioned.withPermit(tenant)))
  // partitioned A1 A2 B1 C1 A3 B2 C2 A4 A5 A6
})

Effect.runPromise(program)

普通信号量里 B 和 C 要等 A 的 6 个任务全部跑完;分区信号量里 B1、C1 在 A 的第 3 个任务之前就完成了。它不是严格的 A、B、C 逐个轮转(A2 排在了 B1 前面),我跑出来的顺序就是上面这样,所以文档里的「round-robin」应理解为分区之间轮转的倾向,不要依赖精确顺序。

PartitionedSemaphore 的 API 长什么样?

withPermit(key) 和 withPermits(key, n) 对应普通信号量的同名方法,只是多一个分区 key;withPermitsIfAvailable(n) 是非阻塞的尝试版本,返回 Option;available、capacity 可以查询许可池状态。请求的许可数超过总容量的 take 永远不会完成,这一点要避免。

它和 RateLimiter 是什么关系?

它管的是并发层面的多租户公平,而 Effect 的 RateLimiter 怎么按 key 限速? 管的是速率层面的配额:前者保证一个租户占满资源池时别人不会饿死,后者保证一个租户在单位时间内的总量不超标。需要两者时可以叠加使用,选型思路见 Effect 里限并发、限速率、限节拍该用哪个?。

别忘了它在 4.0.5 里标注为 @stability unstable,API 仍可能变动。

版本是 effect 4.0.5,上面的示例在这个版本上运行过,输出顺序即实际结果。withPermitsIfAvailable、available、capacity 与「超过容量的 take 永远不会完成」是读自类型与文档的描述,我没有为它们单独写示例运行。@stability unstable 的标注读自该版本的 JSDoc,后续版本可能变化。