railwatch 0.8.6 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/lib/railwatch/configuration.rb +8 -1
- data/lib/railwatch/patches/migration_busy_timeout.rb +73 -0
- data/lib/railwatch/patches.rb +11 -0
- data/lib/railwatch/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f9e80ca205e8ad9435148ba6b185c3de1796f8ffe489f1208876f38e21686181
|
|
4
|
+
data.tar.gz: '09028109bb2da10a48db010d19f126a5e406bcbe50d2991ebcc303e4c4d74318'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75c81cb580e0948629948f8be59ccaf81e175239e2d268ae7b8cf9f5f6a42073e930881fc2ac5fe7700806c925684f9defd297f47673a2b2470754d688858554
|
|
7
|
+
data.tar.gz: 8603393804ca3cdd96c5406fb313b1dca1ff7b378c780b7b42765c14d2b76c4e2d0895f8766c0a50fcbe14e0ea5f9ec1381fb7d330737d4ab50b950c76e84e56
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@
|
|
|
6
6
|
filled in by the release commit, which is also the only commit that
|
|
7
7
|
touches lib/railwatch/version.rb and Gemfile.lock. See CONTRIBUTING.md. -->
|
|
8
8
|
|
|
9
|
+
## 0.8.7 (2026-10-06)
|
|
10
|
+
|
|
11
|
+
- A migration of the railwatch databases waits up to 60 s for SQLite's
|
|
12
|
+
write lock instead of the database's own `timeout` (5 s in the generated
|
|
13
|
+
`database.yml`). A deploy migrates while the previous release is still
|
|
14
|
+
serving, and on an embedded install that release's writer keeps
|
|
15
|
+
committing telemetry to the same file: about a second per transaction on
|
|
16
|
+
a warm 15 GB file, several seconds while the deploy's image build has the
|
|
17
|
+
disk. rebulk-system's container entrypoint ran `db:prepare` into that,
|
|
18
|
+
got `SQLite3::BusyException: database is locked` from the first
|
|
19
|
+
migration that needed a write, and crash-looped until the health check
|
|
20
|
+
gave up. Only the connection Active Record migrates with is changed, only
|
|
21
|
+
while it migrates, and only for a database whose `migrations_paths` are
|
|
22
|
+
this gem's; the app's requests keep failing fast. It applies with
|
|
23
|
+
Railwatch disabled, which is how an entrypoint migrates.
|
|
24
|
+
`RAILWATCH_MIGRATION_BUSY_TIMEOUT` (`c.migration_busy_timeout`, seconds)
|
|
25
|
+
sets it; keep it inside the deploy's health-check window.
|
|
26
|
+
|
|
9
27
|
## 0.8.6 (2026-10-06)
|
|
10
28
|
|
|
11
29
|
- An install upgrading from 0.5.0 or older can migrate again. 0.5.1
|
|
@@ -76,7 +76,7 @@ module Railwatch
|
|
|
76
76
|
:buffer_size, :buffer_bytes, :execution_buffer_bytes, :batch_bytes,
|
|
77
77
|
:backpressure,
|
|
78
78
|
:flush_interval, :flush_threshold,
|
|
79
|
-
:connect_timeout, :timeout, :shutdown_timeout,
|
|
79
|
+
:connect_timeout, :timeout, :shutdown_timeout, :migration_busy_timeout,
|
|
80
80
|
:slow_query_threshold_ms, :n_plus_one_threshold,
|
|
81
81
|
:max_view_renders_per_execution, :ignored_cache_key_prefixes,
|
|
82
82
|
:beacon_enabled, :beacon_rate_limit, :beacon_global_rate_limit, :beacon_allowed_origins,
|
|
@@ -187,6 +187,13 @@ module Railwatch
|
|
|
187
187
|
@connect_timeout = env_float("RAILWATCH_CONNECT_TIMEOUT", 1.0)
|
|
188
188
|
@timeout = env_float("RAILWATCH_TIMEOUT", 3.0)
|
|
189
189
|
@shutdown_timeout = env_float("RAILWATCH_SHUTDOWN_TIMEOUT", 2.0)
|
|
190
|
+
# Seconds a migration of the railwatch databases waits for SQLite's
|
|
191
|
+
# write lock (Patches::MigrationBusyTimeout). A deploy migrates while
|
|
192
|
+
# the old release's writer is still committing to the same file; the
|
|
193
|
+
# database's own `timeout` (5 s) is too short to get between its
|
|
194
|
+
# transactions when the disk is busy. Kept under a typical health-check
|
|
195
|
+
# window (Kamal's deploy_timeout defaults to 30 s and is often 90 s).
|
|
196
|
+
@migration_busy_timeout = env_float("RAILWATCH_MIGRATION_BUSY_TIMEOUT", 60.0)
|
|
190
197
|
@slow_query_threshold_ms = env_float("RAILWATCH_SLOW_QUERY_MS", 5.0)
|
|
191
198
|
@n_plus_one_threshold = env_int("RAILWATCH_N_PLUS_ONE_THRESHOLD", 5)
|
|
192
199
|
@max_view_renders_per_execution = 20
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railwatch
|
|
4
|
+
module Patches
|
|
5
|
+
# Gives the migration connection to a Railwatch database a long SQLite
|
|
6
|
+
# busy timeout, for the migration only.
|
|
7
|
+
#
|
|
8
|
+
# A deploy migrates while the previous release is still serving, and on
|
|
9
|
+
# an embedded install that release's writer process is committing
|
|
10
|
+
# telemetry to the same file the whole time. Each of its transactions
|
|
11
|
+
# takes SQLite's single write lock: about a second on a warm 15 GB file
|
|
12
|
+
# (the export sender's claim scans the destination's whole delivery
|
|
13
|
+
# history), several seconds while a deploy's image build has the disk.
|
|
14
|
+
# The migration connection waited only the database's configured
|
|
15
|
+
# `timeout` -- 5 s in the generated database.yml -- and then raised
|
|
16
|
+
# SQLite3::BusyException, which db:prepare reports as a failed migration
|
|
17
|
+
# and the container entrypoint as a failed boot. That is how a deploy of
|
|
18
|
+
# rebulk-system crash-looped eight times without one migration running.
|
|
19
|
+
#
|
|
20
|
+
# The configured timeout is the right one for the app's own requests,
|
|
21
|
+
# which should fail fast rather than queue behind ingest. A migration is
|
|
22
|
+
# the opposite: it runs once, before the server binds, and waiting is the
|
|
23
|
+
# only way through. So only the connection Active Record migrates with is
|
|
24
|
+
# changed, only while it migrates, and only for a database whose
|
|
25
|
+
# migrations_paths include this gem's. The host's own databases keep
|
|
26
|
+
# their timeouts.
|
|
27
|
+
module MigrationBusyTimeout
|
|
28
|
+
def migrate(*, **)
|
|
29
|
+
pool = migration_connection_pool
|
|
30
|
+
raw = Railwatch::Patches::MigrationBusyTimeout.lengthen(pool)
|
|
31
|
+
super
|
|
32
|
+
ensure
|
|
33
|
+
Railwatch::Patches::MigrationBusyTimeout.restore(pool, raw) if raw
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# The raw SQLite connection whose timeout was lengthened, or nil when
|
|
38
|
+
# this is not a Railwatch database (or not SQLite).
|
|
39
|
+
def lengthen(pool)
|
|
40
|
+
return unless railwatch_database?(pool.db_config)
|
|
41
|
+
|
|
42
|
+
raw = pool.lease_connection.raw_connection
|
|
43
|
+
return unless raw.respond_to?(:busy_handler_timeout=)
|
|
44
|
+
|
|
45
|
+
raw.busy_handler_timeout = (Railwatch.config.migration_busy_timeout.to_f * 1000).to_i
|
|
46
|
+
raw
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
Railwatch.debug { "migration busy timeout not applied: #{e.class}: #{e.message}" }
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Back to what database.yml says, so a connection that outlives the
|
|
53
|
+
# migration (db:migrate in a long-lived process, a spec) behaves as
|
|
54
|
+
# configured afterwards.
|
|
55
|
+
def restore(pool, raw)
|
|
56
|
+
timeout = pool.db_config.configuration_hash[:timeout]
|
|
57
|
+
if timeout
|
|
58
|
+
raw.busy_handler_timeout = Integer(timeout)
|
|
59
|
+
else
|
|
60
|
+
raw.busy_handler(nil)
|
|
61
|
+
end
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
Railwatch.debug { "migration busy timeout not restored: #{e.class}: #{e.message}" }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def railwatch_database?(db_config)
|
|
67
|
+
ours = %i[railwatch railwatch_telemetry].map { |name| File.expand_path(Railwatch.migrations_path(name)) }
|
|
68
|
+
Array(db_config.migrations_paths).any? { |path| ours.include?(File.expand_path(path.to_s, Rails.root.to_s)) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/railwatch/patches.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "railwatch/patches/net_http"
|
|
|
4
4
|
require "railwatch/patches/rake_task"
|
|
5
5
|
require "railwatch/patches/runner_command"
|
|
6
6
|
require "railwatch/patches/inertia"
|
|
7
|
+
require "railwatch/patches/migration_busy_timeout"
|
|
7
8
|
|
|
8
9
|
module Railwatch
|
|
9
10
|
module Patches
|
|
@@ -22,6 +23,16 @@ module Railwatch
|
|
|
22
23
|
# From Rails::Engine#load_tasks, which has already required rake.
|
|
23
24
|
def install_rake_task!
|
|
24
25
|
::Rake::Task.prepend(RakeTask) unless ::Rake::Task.ancestors.include?(RakeTask)
|
|
26
|
+
install_migration_busy_timeout!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# db:prepare and db:migrate run as rake tasks, so this goes in with the
|
|
30
|
+
# rake patch. Unlike it, this one is not gated on Railwatch.enabled?: a
|
|
31
|
+
# container entrypoint migrates with Railwatch switched off, and that is
|
|
32
|
+
# exactly the migration that has to wait out the old release's writer.
|
|
33
|
+
def install_migration_busy_timeout!
|
|
34
|
+
tasks = ::ActiveRecord::Tasks::DatabaseTasks.singleton_class
|
|
35
|
+
tasks.prepend(MigrationBusyTimeout) unless tasks.ancestors.include?(MigrationBusyTimeout)
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
# From Rails::Application#load_runner, which RunnerCommand#perform calls
|
data/lib/railwatch/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: railwatch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cole Robertson
|
|
@@ -453,6 +453,7 @@ files:
|
|
|
453
453
|
- lib/railwatch/minitest.rb
|
|
454
454
|
- lib/railwatch/patches.rb
|
|
455
455
|
- lib/railwatch/patches/inertia.rb
|
|
456
|
+
- lib/railwatch/patches/migration_busy_timeout.rb
|
|
456
457
|
- lib/railwatch/patches/net_http.rb
|
|
457
458
|
- lib/railwatch/patches/rake_task.rb
|
|
458
459
|
- lib/railwatch/patches/runner_command.rb
|