mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-09 08:10:08 +09:00
Load WAL checkpointer outside Zeitwerk reload; harden fork stop
Keep the module under lib/rails_ext, use before_worker_boot, shorten the SQLite busy timeout under stop's join, and PASSIVE-checkpoint on exit for short-lived writers. Co-authored-by: Thomas Klemm <github@tklemm.eu>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
# 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.
|
||||
# Loaded from lib/rails_ext (autoload_lib ignore list) so reloads do not orphan
|
||||
# the contender thread. Non-test processes start here; Puma/Resque stop before
|
||||
# fork and start again in the child.
|
||||
require Rails.root.join("lib/rails_ext/sqlite_wal_checkpoint")
|
||||
|
||||
Rails.application.config.after_initialize do
|
||||
SqliteWalCheckpoint.start unless Rails.env.test?
|
||||
end
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ Membership.disconnect_all
|
||||
# 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 }
|
||||
before_worker_boot { SqliteWalCheckpoint.start }
|
||||
|
||||
Signal.trap :SIGPROF do
|
||||
Thread.list.each do |t|
|
||||
|
||||
@@ -21,6 +21,7 @@ module SqliteWalCheckpoint
|
||||
return if @thread&.alive?
|
||||
|
||||
@stop = false
|
||||
install_exit_checkpoint
|
||||
@thread = Thread.new { run(interval) }
|
||||
@thread.report_on_exception = false
|
||||
end
|
||||
@@ -33,6 +34,7 @@ module SqliteWalCheckpoint
|
||||
def stop
|
||||
@stop = true
|
||||
thread = @mutex&.synchronize { @thread }
|
||||
# Join longer than busy_handler_timeout so before_fork does not race a PRAGMA.
|
||||
thread&.join(2)
|
||||
@mutex&.synchronize { @thread = nil if @thread && !@thread.alive? }
|
||||
end
|
||||
@@ -63,6 +65,7 @@ module SqliteWalCheckpoint
|
||||
@lock_path = nil
|
||||
@database_path_override = nil
|
||||
@stop = false
|
||||
@exit_checkpoint_installed = false
|
||||
end
|
||||
|
||||
private
|
||||
@@ -113,7 +116,8 @@ module SqliteWalCheckpoint
|
||||
|
||||
result = nil
|
||||
SQLite3::Database.new(path) do |database|
|
||||
database.busy_handler_timeout = 5_000
|
||||
# Keep below stop's join timeout so before_fork can finish cleanly.
|
||||
database.busy_handler_timeout = 1_000
|
||||
result = yield database
|
||||
end
|
||||
result
|
||||
@@ -148,5 +152,14 @@ module SqliteWalCheckpoint
|
||||
ensure
|
||||
@lock_file = nil
|
||||
end
|
||||
|
||||
# Best-effort PASSIVE for short-lived console/rake writers that exit before
|
||||
# the contender acquires the flock. Does not touch lock ownership.
|
||||
def install_exit_checkpoint
|
||||
return if @exit_checkpoint_installed
|
||||
|
||||
@exit_checkpoint_installed = true
|
||||
at_exit { checkpoint rescue nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -35,6 +35,24 @@ class SqliteWalCheckpointTest < ActiveSupport::TestCase
|
||||
assert_empty checkpoint_threads
|
||||
end
|
||||
|
||||
test "stop before start again mimics a fork-safe worker boot" do
|
||||
db_path = build_wal_database(rows: 10)
|
||||
SqliteWalCheckpoint.database_path_override = db_path
|
||||
|
||||
first = SqliteWalCheckpoint.start(interval: 0.05, enabled: true)
|
||||
wait_until { first.name == "sqlite-wal-checkpoint" }
|
||||
SqliteWalCheckpoint.stop
|
||||
assert_not first.alive?
|
||||
|
||||
second = SqliteWalCheckpoint.start(interval: 0.05, enabled: true)
|
||||
wait_until { second.name == "sqlite-wal-checkpoint" }
|
||||
assert second.alive?
|
||||
|
||||
SqliteWalCheckpoint.stop
|
||||
assert_not second.alive?
|
||||
assert_equal :checkpointed, SqliteWalCheckpoint.tick
|
||||
end
|
||||
|
||||
test "tick checkpoints through the elected lock holder" do
|
||||
db_path = build_wal_database(rows: 50)
|
||||
SqliteWalCheckpoint.database_path_override = db_path
|
||||
|
||||
Reference in New Issue
Block a user