Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

Pool.get is not first-come-first-served: the releasing fiber re-acquires before fibers already waiting

Chiusa
#10,017 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Una pull request collegata è già stata integrata.

  • #10026 di @tim-smart — integrata

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
typescript
Ambito
backend, testing

Direzione di ricerca

Start with packages/effect/src/Pool.ts, especially get, waitForItem, addAvailableFront, and wakeWaiters; trace how a released item is made available and how queued fibers are resumed. The reproduction in the issue provides the failing FIFO expectation; run it against the pool implementation and check that waiting requests are admitted in arrival order without repeated overtaking. The report also notes similar behavior in src/Semaphore.ts, so assess whether the fix needs to cover both.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

What version of Effect is running?

effect 4.0.6 (also 4.0.5; src/Pool.ts is identical in both). The same behaviour reaches @effect/sql-pg, whose pool is built on Pool.

What steps can reproduce the bug?
  1. Create an empty folder with this package.json:

    {
      "name": "effect-pool-fairness-repro",
      "private": true,
      "type": "module",
      "devDependencies": {
        "effect": "4.0.6",
        "@effect/vitest": "4.0.6",
        "vitest": "5.0.5"
      }
    }
    
  2. Add pool.test.ts:

    import { expect, it } from "@effect/vitest"
    import { Effect, Fiber, Pool } from "effect"
    import { TestClock } from "effect/testing"
    
    it.effect("a later request is admitted before the ones already waiting", () =>
      Effect.gen(function*() {
        const pool = yield* Pool.make({ acquire: Effect.succeed("connection"), size: 1 })
        let asked = 0
        const admitted: Array<number> = []
        const use = Effect.scoped(Effect.gen(function*() {
          const ticket = asked++          // ticket = arrival order of the request
          yield* Pool.get(pool)
          admitted.push(ticket)           // record the order in which requests get the item
          yield* Effect.sleep("10 millis")
        }))
        // One fiber uses the item three times in a row (asking at 0, 10 and 20 ms);
        // three other fibers start asking at 1, 2 and 3 ms.
        const chained = yield* Effect.forkChild(Effect.repeat(use, { times: 2 }))
        const others = yield* Effect.forEach([1, 2, 3], (at) => Effect.forkChild(Effect.delay(use, `${at} millis`)))
        yield* TestClock.adjust("100 millis")
        yield* Fiber.join(chained)
        yield* Fiber.joinAll(others)
        expect(admitted).toEqual([0, 1, 2, 3, 4, 5])
      }).pipe(Effect.scoped))
    
  3. Run npm install and npx vitest run pool.test.ts (Bun works the same).

What is the expected behavior?

Requests are admitted in arrival order: [0, 1, 2, 3, 4, 5]. Requests 1, 2 and 3 started waiting at 1-3 ms, before the chained fiber asked again at 10 and 20 ms (tickets 4 and 5).

What do you see instead?
AssertionError: expected [ 0, 4, 5, 3, 1, 2 ] to deeply equal [ 0, 1, 2, 3, 4, 5 ]

It fails on every run. The fiber that releases the item gets it straight back (0, 4, 5), and the fibers that were already waiting are admitted last, and not in their own arrival order either (3, then 1, then 2).

Additional information

Where it comes from, in packages/effect/src/Pool.ts at 4.0.6:

  • get (L462-473) leases state.availableHead whenever an item is available, without checking whether state.waiters is empty.
  • On release, the item goes back to the front (addAvailableFront, L709), and wakeWaiters (L710, L737-751) wakes waiters in a task scheduled for later (currentDispatcher.scheduleTask(..., 0)).
  • Between the release and that task, any fiber that calls get synchronously takes the item. Usually that is the fiber that just released it and is running its next statement.
  • A woken waiter that loses that race goes back through waitForItem (L721-735), which re-adds it at the end of the waiters set. That explains the reversed order among the waiters.

Semaphore has the same shape: take succeeds when free >= n regardless of waiters (src/Semaphore.ts L223), and releaseUnsafe (L267) wakes observers that re-check later. So putting a semaphore in front of the pool does not make it fair.

Impact we measured (@effect/sql-pg pool of 4 connections, about 50 concurrent fibers each running a few consecutive statements):

  • The median acquisition waits 0 ms, because the releasing fiber keeps the connection.
  • p99 is 1.7 s and the worst case 4.2 s.
  • Among acquisitions that waited more than 1 s, a median of 146 later acquisitions were admitted first.

In a micro-benchmark of that pattern (50 fibers × 5 calls, 4 connections), the worst wait per call was about 1.2 s, with up to about 900 overtakes per acquisition. With a first-come gate in front of the pool it drops to about 0.27 s, with at most 1 overtake.

Possible direction: hand a released item (or permit) directly to the oldest waiter, instead of putting it back and waking waiters later, and let get/take use an available item only when nobody is waiting. Keeping waiters in FIFO order, and not re-queueing a woken waiter at the back, would also bound the wait. If the current behaviour is intentional for throughput (the releasing fiber keeps a warm connection), an opt-in fairness: "fifo" option on Pool.make and the SQL pools would cover latency-sensitive callers.

Lingua principale
TypeScript
Stelle
16.7k
Fork
808
Merge medio
11h 33m
PR unite (30g)
855

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di Effect-TS/effect

Tutte le issue di Effect-TS/effect

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.