Stop WAL checkpointer before fork; one thread owns the flock

Unify startup: initializer starts every non-test process, Puma and Resque
stop before fork and start again in the child. Only the contender thread
releases the lock. Drop Puma::CLI / single_puma_process? special cases.

Co-authored-by: Thomas Klemm <github@tklemm.eu>
This commit is contained in:
Cursor Agent
2026-10-07 19:00:40 +00:00
parent 88acc4e026
commit 61631ea5f9
7 changed files with 24 additions and 71 deletions
+3 -2
View File
@@ -9,8 +9,9 @@ default: &default
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 10 } %>
timeout: 5000
default_transaction_mode: immediate
# Checkpoint on a background connection instead (SqliteWalCheckpoint). Every
# non-test writer process runs a contender so this is never left without one.
# Checkpoint on a background connection instead (SqliteWalCheckpoint).
# Non-test processes start a contender; forking servers stop before fork and
# start again in the child so the flock is never inherited.
pragmas:
wal_autocheckpoint: 0
+3 -8
View File
@@ -1,10 +1,5 @@
# Start a checkpoint contender in console, runner, rake and other non-Puma writers.
# Puma skips this path: config/puma.rb and config/puma_dev.rb start after the
# correct process is chosen (worker boot vs single-process), so the master that
# only forks workers never holds the lock alone. Resque starts after_prefork.
# Start a checkpoint contender in every non-test process. Puma and Resque pool
# stop before fork and start again in the child so the flock is never inherited.
Rails.application.config.after_initialize do
next if Rails.env.test?
next if defined?(Puma::CLI)
SqliteWalCheckpoint.start
SqliteWalCheckpoint.start unless Rails.env.test?
end
+6 -12
View File
@@ -33,10 +33,7 @@ pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
# processes).
#
worker_count = (Concurrent.processor_count * 0.666).ceil
# Keep the raw setting: WEB_CONCURRENCY=auto must not be coerced with to_i (that
# is 0 and would look like single-process mode while Puma still forks workers).
configured_workers = ENV.fetch("WEB_CONCURRENCY") { worker_count }
workers configured_workers
workers ENV.fetch("WEB_CONCURRENCY") { worker_count }
ENV["JOB_CONCURRENCY"] ||= worker_count.to_s
@@ -53,14 +50,11 @@ plugin :tmp_restart
# Reset all membership connections
Membership.disconnect_all
# Only the literal 0 is single-process. "auto" and positive counts fork workers;
# those must start the checkpointer after boot so a surviving worker can take
# the lock if the previous leader exits.
if SqliteWalCheckpoint.single_puma_process?(configured_workers)
SqliteWalCheckpoint.start
else
on_worker_boot { SqliteWalCheckpoint.start }
end
# Initializer starts a contender in this process. Stop before fork so workers
# do not inherit the flock; each worker starts its own contender. Single-process
# mode never forks, so the initializer's contender keeps running.
before_fork { SqliteWalCheckpoint.stop }
on_worker_boot { SqliteWalCheckpoint.start }
Signal.trap :SIGPROF do
Thread.list.each do |t|
-2
View File
@@ -1,7 +1,5 @@
require File.expand_path("../config/environment", File.dirname(__FILE__))
SqliteWalCheckpoint.start
Signal.trap :SIGPROF do
Thread.list.each do |t|
puts t