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

SQLite withTransaction: interrupting a queued transaction as it gets the connection leaks the connection

Chiusa
#9,957 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Una pull request collegata è già stata integrata.

  • #9962 di @effect-bot — integrata

Valutazione

Difficoltà
4/5
Tempo stimato
1-2 giorni
Idoneità per principianti
35/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Ferma
Stack tecnologico
node.js, sqlite, typescript
Ambito
backend, database

Direzione di ricerca

Read makeSqliteAcquirers and makeWithTransaction, especially the lease and its interruptibility around Semaphore.take and finalizer registration; the issue also identifies sql.reserve as a possible affected entry point. Use the supplied lease reproduction to check whether interruption can leak a permit, then verify that a later permit acquisition completes after the holder releases it. An open linked pull request (#9962) is already addressing this issue.

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

Descrizione

What version of Effect is running?
  • [email protected] and @effect/[email protected] on Node 26.12.1 (Linux x64), unmodified published packages.
  • makeSqliteAcquirers and makeWithTransaction are unchanged on main since [email protected].
  • Introduced by #9517 (the fix for #9514), which is first released in 4.0.6.
What steps can reproduce the bug?

Fiber A runs a statement, so it holds the SQLite connection. Fiber B calls withTransaction and queues behind it. B is interrupted around the moment A hands it the connection. MaxOpsBeforeYield and a few yieldNows place the interrupt; a budget sweep finds the window deterministically.

// repro.ts - node repro.ts
import { SqliteClient } from "@effect/sql-sqlite-node"
import { Effect, Fiber, Scheduler } from "effect"
import * as SqlClient from "effect/sql/SqlClient"

// One run: B queues behind A for the connection and is interrupted around the hand-off.
const attempt = (budget: number, lead: number) =>
  Effect.gen(function*() {
    const sql = yield* SqlClient.SqlClient
    const holder = yield* Effect.forkChild(sql`SELECT 1`)
    const queued = yield* Effect.forkChild(
      sql.withTransaction(sql`SELECT 2`).pipe(Effect.provideService(Scheduler.MaxOpsBeforeYield, budget))
    )
    for (let i = 0; i < lead; i++) yield* Effect.yieldNow
    yield* Fiber.interrupt(queued)
    yield* Fiber.join(holder)
    // Every later statement waits for the connection forever.
    return yield* sql`SELECT 3`.pipe(Effect.timeoutOption("300 millis"), Effect.map((o) => o._tag === "Some"))
  }).pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:" })))

const program = Effect.gen(function*() {
  for (let budget = 3; budget <= 64; budget++) {
    for (let lead = 0; lead <= 3; lead++) {
      if (!(yield* attempt(budget, lead))) return `wedged at budget=${budget} lead=${lead}`
    }
  }
  return "never wedged"
})

Effect.runPromise(program).then(console.log)
What is the expected behavior?

An interrupted withTransaction that never started its transaction leaves the connection free, and SELECT 3 runs. The script prints never wedged.

What do you see instead?

wedged at budget=21 lead=1, on every run. From then on every statement on that client waits for the connection until the layer is torn down. In a server, a client abort that lands in this window wedges the database until a restart.

Additional information

The permit leaks in the lease that makeSqliteAcquirers builds:

const lease = Effect.uninterruptibleMask((restore) =>
  Effect.flatMap(Effect.scope, (scope) =>
    restore(semaphore.take(1)).pipe(
      Effect.andThen(Scope.addFinalizer(scope, semaphore.release(1))),
      Effect.andThen(available)
    ))
)

Semaphore.take adds to taken inside the region restore makes interruptible, and the release is registered only after that region ends. If the fiber yields between the two and is interrupted there, the permit is taken but never released. Before #9517, makeWithTransaction called the acquirer while uninterruptible, so this restore restored an uninterruptible region and the window couldn't open; #9517 wraps the acquirer in the transaction's own restore, which opens it. sql.reserve, which uses the same lease, can probably hit it from any interruptible caller.

The lease alone reproduces it, without SQL:

import { Effect, Fiber, Scheduler, Scope, Semaphore } from "effect"

// The lease from makeSqliteAcquirers, called from a fiber that can be interrupted.
const lease = (sem: Semaphore.Semaphore) =>
  Effect.uninterruptibleMask((restore) =>
    Effect.flatMap(Effect.scope, (scope) =>
      restore(sem.take(1)).pipe(Effect.andThen(Scope.addFinalizer(scope, sem.release(1))))
    )
  )

const attempt = (budget: number, lead: number) =>
  Effect.gen(function*() {
    const sem = yield* Semaphore.make(1)
    const holder = yield* Effect.forkChild(sem.withPermit(Effect.yieldNow))
    const queued = yield* Effect.forkChild(
      Effect.scoped(lease(sem)).pipe(Effect.provideService(Scheduler.MaxOpsBeforeYield, budget))
    )
    for (let i = 0; i < lead; i++) yield* Effect.yieldNow
    yield* Fiber.interrupt(queued)
    yield* Fiber.join(holder)
    return yield* sem.withPermit(Effect.void).pipe(Effect.timeoutOption("50 millis"), Effect.map((o) => o._tag === "Some"))
  })

const program = Effect.gen(function*() {
  for (let budget = 3; budget <= 64; budget++) {
    for (let lead = 0; lead <= 3; lead++) {
      if (!(yield* attempt(budget, lead))) return `permit leaked at budget=${budget} lead=${lead}`
    }
  }
  return "never leaked"
})

Effect.runPromise(program).then(console.log)

This prints permit leaked at budget=6 lead=2. Semaphore.withPermits doesn't have the problem: it waits for a free permit interruptibly, then takes it after the interruptible region has ended. Taking the permit the same way in the lease, or making take hand the permit back when the fiber is interrupted before take returns, would close the window.

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.