Skip to content

The pool and Database are safe and Share across workers - #2

Merged
sdogruyol merged 7 commits into
masterfrom
feat/pool-every-core
Nov 2, 2026
Merged

sdogruyol merged 7 commits into
masterfrom
feat/pool-every-core

Conversation

@sdogruyol

Copy link
Copy Markdown
Contributor

Closes #1.

What

Db::Core::Database and its pool are safe across iyi's pool workers (spawn_parallel, Server.serve), and Share by their fields. An app can keep DATABASE = Db.open(...) as a constant and use it from parallel routes.

  • Lock: checkout and release go through one Mutex around the pool's bookkeeping. Connections are opened and closed outside the lock.
  • Waiting: a checkout that finds the pool full parks on its own channel. A release hands its connection, or the room it freed, to the checkout that has waited longest.
  • Close: close closes idle connections at once, and a busy one when its task releases it.
  • Stats: the counts stats reports are atomics, so it takes no lock.
  • Factories: Pool.new, Database.new, Database.open and setup_connection check their blocks where they are written (@[Share]).
  • Constants:
    • DRIVERS sits behind a Mutex.
    • The statement hooks are a Mutex(List(SharedProc)) with an atomic count, so a statement with no hooks takes no lock.
    • LOG is a SharedLogger: std's logger behind a Mutex, with its level in an atomic beside it. Logging off takes no lock.

Breaking

  • Factory, setup and hook blocks may capture only Share values.
  • LOG's type changed. Set the level with Db::Core::LOG.level =, and a backend on Log.for("db").
  • Pooled resources must be class instances.
  • The minimum is now iyi 0.19.1.

Measured

  • The issue's program (PostgreSQL 14 on macOS 26.6, Apple M2 Pro):
    • before: failed at 2, 4 and 12 workers ("two fibers reading one fd", segfaults, wrong answers, hangs);
    • after: 20 of 20 runs passed at 12 workers, twice.
  • One task, SELECT $1::int8: a median of 16,073 q/s before against 15,857 after, inside the noise.
  • 12 workers, logging off: SharedLogger and a plain std logger read the same, within noise.
  • A real web app (Windows 11, 12 workers) needed no change against this branch. It could drop its own SharedDatabase workaround (167 lines → 37) for a plain Db.open constant, with the same throughput.

Tests

New db/parallel_test.iyi, run at 12 workers:

  • the issue's program, 20 times;
  • a full pool with 40 waiting checkouts;
  • close while 32 tasks query;
  • a failed statement returns its connection;
  • a lost connection is dropped.

All 16 test files pass, also from artifacts. A run on iyi 0.19.1 itself, the new minimum, hasn't been made yet.

A Database shared by tasks on every worker needs spawn_parallel (0.19.0)
and the @[Share] defs and SharedProc that check the blocks it keeps
(0.19.1).
Its counts are atomics, and the connections and the last result set it
keeps for the tests are behind a Mutex, each in a Made(T) that is Share on
trust for the test double alone. db#1's program on it, at 12 workers, read
the registry from every worker at once.
v0.3.0's pool kept its bookkeeping with no lock, from when iyi's tasks
shared one thread, and two tasks on different workers were handed one
connection. db#1's program on the dummy driver answered other tasks'
numbers or hung at 2, 4 and 12 workers (Linux, 12 cores).

* Pool: the bookkeeping is a PoolBook behind a std Mutex; a resource goes
  in and out of it in a PoolSlot, Share on trust (owns: true), the one
  claim; opening and closing run outside the lock. A checkout that finds
  the pool full parks on a channel of its own, registered under the same
  lock, and a release hands its resource, or the room it left, to the
  checkout that has waited longest. close closes the idle resources and
  each one out when it is released. stats reads atomics. Pool.new is
  @[Share]: its factory is checked where written and kept as a SharedProc.
* Database: Share by its fields. Database.new, Database.open and
  setup_connection are @[Share]; the setup block is behind a Mutex. Db.open
  wraps the driver's ConnectionBuilder in SharedBuilder, trusted by the
  ConnectionBuilder contract (pg's and mysql's builders are not Share by
  their fields).
* The driver registry is behind a Mutex.
* Statement hooks are SharedProcs in a List behind a Mutex, with an atomic
  count so a statement with none takes no lock.
* LOG is a SharedLogger: std's Logger behind a Mutex, its level in an
  atomic beside it, so a statement with debug off takes no lock.

iyi check of the issue's program (master befaa8bc4): 4 #148 warnings
before (DATABASE, LOG, AROUND_HOOKS, the dummy's last result set), none
after; on pg 0.3.1 the 3 left are pg's (DECODERS, LOG, QUOTED), and none
with pg's perf/statement-cache.
parallel_test sets IyiWorkers.count = 12 and runs the issue's program
through a constant and twenty times through a local, a full pool's
waiting checkouts outnumbering the workers, a close while every worker
queries, and failing and lost connections. manual/pool_concurrency runs
its tasks with spawn_parallel, as crystal's did under preview_mt.
@sdogruyol
sdogruyol merged commit 84ef9ff into master Nov 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pool hands one connection to two tasks on different workers (spawn_parallel)

1 participant