Skip to content

Semaphore: non-blocking manual acquisition (takeIfAvailable) #6787

Description

@danielgangl

Problem

Semaphore (v4) offers two acquisition styles:

  • take — manual, pairs with release, but waits for permits
  • withPermitsIfAvailable — fail-fast, but brackets a single Effect: the permit is released when that effect exits

There is no way to acquire a permit fail-fast and keep holding it beyond one effect's lifetime — e.g. for the lifetime of a Scope or a returned Stream.

Use case

An HTTP route serves attachment bytes as a stream. Downloads must be capped per isolate, and a public URL must not build an unbounded waiter queue — an exhausted pool should answer 503 immediately (fail-fast). The permit has to be held until the returned stream completes, long after the opening effect has returned.

With today's API this requires keeping a child fiber alive as the permit owner inside withPermitsIfAvailable, coordinated with the caller through deferreds — ~70 lines of choreography for "take a permit if free, release it when the scope closes":

const holdPermit = pool.withPermitsIfAvailable(1)(
  open.pipe(
    Effect.flatMap(stream => Deferred.succeed(ready, wire(stream))),
    Effect.andThen(Effect.never), // fiber stays alive as the permit owner
  ),
)
// + interruption wiring so closing the stream's scope releases the permit

Proposal

/** Acquires the permits only if immediately available; pairs with `release`. */
export const takeIfAvailable: {
  (permits: number): (self: Semaphore) => Effect.Effect<boolean>
  (self: Semaphore, permits: number): Effect.Effect<boolean>
}

Implementation mirrors the first branch of withPermitsIfAvailable: suspend, free < nfalse, else taken += ntrue.

With that, scope-owned fail-fast holding is ordinary user code:

const acquired = yield* Semaphore.takeIfAvailable(pool, 1)
if (!acquired) return yield* Effect.fail(new PoolExhausted())
yield* Scope.addFinalizer(scope, Semaphore.release(pool, 1))

Optionally, withPermitsScoped / withPermitsScopedIfAvailable sugar could sit on top, but takeIfAvailable alone closes the gap.

Happy to send a PR with tests and docs if the addition is welcome.

🤖 Drafted with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions