Class: Sqlx::Database::DatabasePool

Inherits:
Object
  • Object
show all
Defined in:
runtime/database/sqlite.rb

Overview

Dedicated SQLite connection pool. A single-use object so its instance ivars type concretely: @conns is a DatabaseConnection PtrArray (objects keep their tag, unlike a generic pool's int slot), @free is an IntArray stack of available indices into @conns.

Constant Summary collapse

PRAGMAS =

PRAGMAs ARE PER-CONNECTION, so they run on every handle the pool opens, not once on the first; a file-backed server re-reads its working set through the OS page cache on every request otherwise.

cache_size is NEGATIVE on purpose: sqlite reads a positive value as a page COUNT and a negative one as a KiB budget, so -65536 is 64 MiB regardless of page_size, while 65536 would be 64k PAGES.

An in-memory database ignores these (its pages are already the heap), which is harmless.

[
  "PRAGMA cache_size=-65536",
  # Read pages straight out of the page cache by mapping the file, which
  # drops the read() syscall and its copy for everything that fits.
  "PRAGMA mmap_size=268435456",
  # WAL for readers-alongside-writer. The lock wait is not a pragma: the
  # pool installs a fair busy handler on every connection (see
  # `busy_wait`), because `PRAGMA busy_timeout` sleeps on a back-off ladder
  # that starves waiters under a stream of short writes.
  "PRAGMA journal_mode=WAL",
  # NORMAL, because FULL fsyncs the WAL on EVERY COMMIT. Set rather than
  # left to a library default, so a rebuild cannot move the durability
  # setting silently. NORMAL survives a process crash and risks only the
  # most recent commits on OS or power failure.
  "PRAGMA synchronous=NORMAL",
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(path, n) ⇒ DatabasePool

Returns a new instance of DatabasePool.



352
353
354
355
356
357
358
359
360
361
362
363
# File 'runtime/database/sqlite.rb', line 352

def initialize(path, n)
  @path  = path
  @size  = n
  @conns = []
  @free  = []
  @lock  = Mutex.new
  @cv    = ConditionVariable.new
  # Connections open on first use, not here: a prefork child inherits
  # nothing because the parent never opened a handle before the fork.
  @open_lock = Mutex.new
  @opened    = false
end

Instance Method Details

#available ⇒ Object



435
436
437
438
439
440
441
# File 'runtime/database/sqlite.rb', line 435

def available
  n = 0
  @lock.synchronize do
    n = @free.length
  end
  n
end

#close_all ⇒ void

finalize every cached statement on every connection, then close its handle. A pool that never opened (the prefork parent before the fork) has nothing to close.



492
493
494
495
496
497
498
499
500
# File 'runtime/database/sqlite.rb', line 492

def close_all
  i = 0
  while i < @conns.length
    c = @conns[i]
    c.finalize_all
    SQLite3.sqlite3_close(c.dbh)
    i += 1
  end
end

#conn(idx) ⇒ Object



478
479
480
# File 'runtime/database/sqlite.rb', line 478

def conn(idx)
  @conns[idx]
end

#ensure_open ⇒ void

open this pool's connections once, on first use. The prefork parent calls configure and forks before any database access, so the child inherits an unopened pool object and opens its own handles here. The open lock serializes simultaneous first requests so the pool is not built twice.



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'runtime/database/sqlite.rb', line 372

def ensure_open
  return if @opened

  failure = nil
  conns = []
  # Raising inside `Mutex#synchronize` would skip its unlock on this
  # adapter, so an open error is recorded and raised after the lock.
  @open_lock.synchronize do
    if !@opened
      i = 0
      while i < @size && failure.nil?
        rc = SQLite3.sqlite3_open_v2(@path, SQLite3.db_out, SQLite3::OPEN_URI_RWC, nil)
        if rc != SQLite3::OK
          # Best-effort error surface — sqlite3_errmsg requires a valid
          # db handle, which we don't have on open failure. The numeric
          # rc + path are the only signals we can raise pre-handle.
          failure = "Sqlx::Database.configure: sqlite3_open(" + @path + ") failed (" + rc.to_s + ")"
        else
          dbh = SQLite3.read_ptr(SQLite3.db_out)
          # Deliberately not raising on a refused pragma. Every one of
          # these is an optimization or a politeness; none changes a
          # query's RESULT, so a build that declines one
          # (journal_mode=WAL on a read-only mount, say) should still
          # serve, slower, rather than fail to boot.
          PRAGMAS.each { |p| SQLite3.sqlite3_exec(dbh, p, nil, nil, nil) }
          # The lock wait is a callback, not a pragma; `busy_wait` says
          # why. A refused install is loud: without a handler SQLite
          # answers BUSY immediately, so the pool would be far less
          # available than configured.
          rc = SQLite3.sqlite3_busy_handler(dbh, Sqlx::Database.method(:busy_wait), nil)
          if rc != SQLite3::OK
            failure = "Sqlx::Database.configure: sqlite3_busy_handler failed (" + rc.to_s + ")"
          else
            conns.push(DatabaseConnection.new(dbh))
          end
        end
        i += 1
      end
      if failure.nil?
        @lock.synchronize do
          @conns = conns
          i = 0
          while i < conns.length
            @free.push(i)
            i += 1
          end
          @opened = true
          @cv.broadcast
        end
      end
    end
  end

  if !failure.nil?
    i = 0
    while i < conns.length
      SQLite3.sqlite3_close(conns[i].dbh)
      i += 1
    end
    raise failure
  end
end

#first ⇒ Object



482
483
484
485
# File 'runtime/database/sqlite.rb', line 482

def first
  ensure_open
  @conns[0]
end

#lease ⇒ Integer

a free connection index (LIFO), parking this thread while the pool is empty; release signals it.

Returns:

  • (Integer) —

    the leased index, to hand to conn and release



447
448
449
450
451
452
453
454
455
456
457
# File 'runtime/database/sqlite.rb', line 447

def lease
  ensure_open
  idx = 0
  @lock.synchronize do
    while @free.length == 0
      @cv.wait(@lock)
    end
    idx = @free.delete_at(@free.length - 1)
  end
  idx
end

#release(idx, conn, finished) ⇒ void

return one connection to the free list and wake a waiter. A completed request trims the connection's statement cache first; the release sits in an ensure so a raise while trimming cannot leak the slot.

Parameters:

  • idx (Integer) —

    the freed slot

  • conn (DatabaseConnection) —

    the connection in that slot

  • finished (Boolean) —

    whether the block completed without raising



467
468
469
470
471
472
473
474
475
476
# File 'runtime/database/sqlite.rb', line 467

def release(idx, conn, finished)
  begin
    conn.trim! if finished
  ensure
    @lock.synchronize do
      @free.push(idx)
      @cv.signal
    end
  end
end