solid_queue_guard 1.0.0 → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 690433fe81ac90880ec7fdaa046b7638330eb879776e19343066732fd1f0f3e7
4
- data.tar.gz: 6028bf63b80b0919158a10fb5771b8cdec74518be47b0882cfd39f893bbf10f7
3
+ metadata.gz: e4708571f815d3044a3aa1f952ffe250699e8b695972cf8449b38f53aa1ff4f3
4
+ data.tar.gz: ef314fa6328235a4e8d5174fe42db21a200ebb8ab067b2b6cda125b8a67b433a
5
5
  SHA512:
6
- metadata.gz: 2d9bf3c79fbe0871ed9456eec1cefb22baf52f4949b128cc2a1f8d30dde4bb468bbece87752a33ea06a0e53d12ec25db65e1c1295205ac140643d9cf5dbfc769
7
- data.tar.gz: 0d40a0805708376a56e243977772274a61f6e4a799ca3dfc64dfb362a46217e30258ba1758e1dda7bc3956f63e13895cde6814f446551b4bf271a07b24477e57
6
+ metadata.gz: 292e090e0c051b8f76d792070bfef73a33e4a21ba680f23319d85354b9de145fa1363dbd1b74efd19c3b23cf5833595b8bb45720ba19a9f41ae9a1497161fd19
7
+ data.tar.gz: 64b1b5b2a1d9e5535ba1170f1469cdf3ac693a9f358b4386fb22699c1ad03c37aabd49176b66ca2c51a0cd78c57d2f447c87581a6ee2450ebb1e317d80d6295d
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.1] - 2026-07-09
9
+
10
+ ### Changed
11
+
12
+ - `QueueSchemaCheck` validates Solid Queue tables in `db/queue_schema.rb`, `db/schema.rb`, `db/structure.sql`, and the queue database instead of requiring `db/queue_schema.rb`
13
+ - README documents operational usage: doctor, CI, HTTP health, and runtime process checks
14
+
8
15
  ## [1.0.0] - 2026-07-09
9
16
 
10
17
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- solid_queue_guard (1.0.0)
4
+ solid_queue_guard (1.0.1)
5
5
  actionpack (>= 7.1, < 9.0)
6
6
  activejob (>= 7.1, < 9.0)
7
7
  activerecord (>= 7.1, < 9.0)
data/README.md CHANGED
@@ -49,7 +49,7 @@ Checks:
49
49
  ✅ Active Job adapter is :solid_queue
50
50
  ✅ Queue database configured in database.yml
51
51
  ✅ Solid Queue connects_to queue database (pool: 10)
52
- db/queue_schema.rb exists
52
+ Solid Queue schema tables present
53
53
  ❌ Worker threads: 10, queue DB pool: 5
54
54
  ⚠️ No workers configured for "mailers" queue
55
55
  ⚠️ recurring.yml exists but scheduler may not run
@@ -76,7 +76,7 @@ Runs locally, in CI, or pre-deploy. **No extra infrastructure.**
76
76
  | **Adapter** | Active Job not set to `:solid_queue` |
77
77
  | **Queue database** | Missing `queue` entry in `database.yml` |
78
78
  | **connects_to** | Solid Queue pointing at the wrong DB / pool |
79
- | **Queue schema** | Missing `db/queue_schema.rb` |
79
+ | **Queue schema** | Missing Solid Queue tables in schema files or queue database |
80
80
  | **Thread pool** | `threads` > available queue DB connections |
81
81
  | **Worker coverage** | Queues with no worker assigned |
82
82
  | **Scheduler** | `recurring.yml` tasks without a scheduler |
@@ -178,6 +178,153 @@ Works with **Kamal**, **Heroku**, **Fly.io**, **ECS/Fargate**, **Kubernetes**, *
178
178
 
179
179
  ---
180
180
 
181
+ ## How to use it
182
+
183
+ Solid Queue does not show up in Rails `/up`. **solid_queue_guard** gives you three operational surfaces:
184
+
185
+ | Surface | Command / URL | Best for |
186
+ | ------- | ------------- | -------- |
187
+ | **Doctor** | `bin/rails solid_queue_guard:doctor` | Local pre-deploy, config review |
188
+ | **CI gate** | `SOLID_QUEUE_GUARD_STRICT=1 bin/rails solid_queue_guard:doctor` | Block merges with broken queue config |
189
+ | **HTTP health** | `GET /solid_queue_guard/health` | Production uptime monitors (Kamal, ECS, UptimeRobot) |
190
+
191
+ **Mission Control** shows what is happening. **solid_queue_guard** warns what is dangerous. Use both.
192
+
193
+ ### Local and pre-deploy
194
+
195
+ Run before changing `queue.yml`, `database.yml`, or recurring tasks:
196
+
197
+ ```bash
198
+ bin/rails solid_queue_guard:doctor # config checks (default scope)
199
+ bin/rails solid_queue_guard:report # config + runtime when the queue DB is available
200
+ ```
201
+
202
+ By default the install generator sets `config.enabled = Rails.env.production?`, so in **development** checks are skipped unless you enable them:
203
+
204
+ ```ruby
205
+ SolidQueueGuard.configure { |c| c.enabled = true }
206
+ ```
207
+
208
+ Or run with production config locally when validating deploy readiness.
209
+
210
+ ### CI pipelines
211
+
212
+ Validate **configuration** before deploy. Process/runtime checks usually **skip** in CI because there is no `bin/jobs` supervisor on the runner.
213
+
214
+ ```yaml
215
+ - name: Solid Queue production readiness
216
+ run: SOLID_QUEUE_GUARD_STRICT=1 bin/rails solid_queue_guard:doctor
217
+ ```
218
+
219
+ `STRICT=1` turns warnings into exit code `1`, so the pipeline fails on misconfigured pools, missing worker coverage, or missing schema tables — not only hard failures.
220
+
221
+ Generate a starter workflow:
222
+
223
+ ```bash
224
+ bin/rails generate solid_queue_guard:install:ci
225
+ ```
226
+
227
+ ### Production monitoring
228
+
229
+ Mount the engine and point your load balancer or Kamal health check at `/solid_queue_guard/health`:
230
+
231
+ ```ruby
232
+ # config/routes.rb
233
+ mount SolidQueueGuard::Engine, at: "/solid_queue_guard"
234
+ ```
235
+
236
+ Runtime checks matter here: queue lag, stale heartbeats, dispatcher health, and process topology reflect **live** Solid Queue state.
237
+
238
+ Optional hardening:
239
+
240
+ ```ruby
241
+ config.health_token = ENV["SOLID_QUEUE_GUARD_TOKEN"]
242
+ config.health_cache_ttl = 15.seconds
243
+ config.degraded_http_status = 207 # or :ok (200), 503, etc.
244
+ config.notify_with = [:rails_logger, :slack]
245
+ ```
246
+
247
+ ### Typical flow
248
+
249
+ ```text
250
+ Developer CI Production
251
+ │ │ │
252
+ │ doctor │ doctor --strict │ GET /health (every 30–60s)
253
+ ▼ ▼ ▼
254
+ "pool wrong?" block bad deploy alert: worker dead
255
+ "schema ok?" before merge alert: queue lag
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Runtime process checks
261
+
262
+ Runtime checks query the **queue database** (via `SolidQueue::Record`). If `connects_to` is missing or the DB is unreachable, they **skip** — they do not pass silently as healthy.
263
+
264
+ ### `process_topology` — are the expected roles present?
265
+
266
+ Reads distinct `kind` values from `solid_queue_processes`:
267
+
268
+ | Kind | Role |
269
+ | ---- | ---- |
270
+ | `Supervisor` | Parent process (`bin/jobs`) |
271
+ | `Worker` | Consumes ready jobs |
272
+ | `Dispatcher` | Moves scheduled jobs to ready |
273
+ | `Scheduler` | Runs recurring tasks |
274
+
275
+ | Result | Meaning |
276
+ | ------ | ------- |
277
+ | ⚠️ No processes | Nothing registered — supervisor not running |
278
+ | ⚠️ No Worker | Jobs will not be processed |
279
+ | ⚠️ No Dispatcher | Scheduled work may stall (when recurring/scheduled jobs exist) |
280
+ | ✅ Pass | Expected kinds are present |
281
+
282
+ This check looks at **presence of records**, not heartbeat freshness.
283
+
284
+ ### `stale_process` — are heartbeats fresh?
285
+
286
+ Finds processes where `last_heartbeat_at` is older than `stale_process_threshold` (default **5 minutes**):
287
+
288
+ | Result | Meaning |
289
+ | ------ | ------- |
290
+ | ✅ Pass | All processes reported recently |
291
+ | ❌ Fail | One or more workers/dispatchers look dead or stuck |
292
+
293
+ Use this in production health to catch workers that died after deploy.
294
+
295
+ ### `pidfile` — optional supervisor liveness
296
+
297
+ When `tmp/pids/solid_queue.pid` (or `SOLID_QUEUE_PIDFILE`) exists, verifies the PID is alive. Often **warns in development** where `bin/jobs` is not running. Disable if you do not use pidfiles:
298
+
299
+ ```ruby
300
+ config.disabled_checks = [:pidfile]
301
+ ```
302
+
303
+ ### `puma_plugin_runtime` — Solid Queue inside Puma
304
+
305
+ When `plugin :solid_queue` is in `config/puma.rb`, verifies active processes with recent heartbeats exist. **Skips** when the Puma plugin is not enabled.
306
+
307
+ ### Queue schema detection
308
+
309
+ `QueueSchemaCheck` does **not** require `db/queue_schema.rb`. It validates that all tables for your installed **solid_queue** version exist in any of:
310
+
311
+ - `db/queue_schema.rb`
312
+ - `db/schema.rb`
313
+ - `db/structure.sql`
314
+ - the connected queue database
315
+
316
+ Apps that keep Solid Queue tables only in `structure.sql` (common in Revelo-style repos) pass correctly.
317
+
318
+ ### Where each check type applies
319
+
320
+ | Context | Config checks | Process / runtime checks |
321
+ | ------- | ------------- | ------------------------ |
322
+ | Local `doctor` | ✅ Primary value | ⚠️ Partial without `bin/jobs` |
323
+ | CI `--strict` | ✅ Primary value | ⏭️ Usually skip |
324
+ | Production `/health` | ✅ When DB up | ✅ Primary value |
325
+
326
+ ---
327
+
181
328
  ## Configuration
182
329
 
183
330
  ```ruby
@@ -5,18 +5,91 @@ module SolidQueueGuard
5
5
  module Config
6
6
  class QueueSchemaCheck < Base
7
7
  def call
8
- schema_path = rails_root.join('db/queue_schema.rb')
8
+ evaluation = evaluate_schema
9
9
 
10
- if schema_path.exist?
11
- pass('queue_schema', 'db/queue_schema.rb exists')
12
- else
10
+ case evaluation[:status]
11
+ when :pass
12
+ pass(check_id, evaluation[:message], metadata: evaluation[:metadata])
13
+ when :warn
13
14
  warn(
14
- 'queue_schema',
15
- 'db/queue_schema.rb not found',
16
- suggestion: 'Run bin/rails solid_queue:install to generate the queue schema'
15
+ check_id,
16
+ evaluation[:message],
17
+ suggestion: evaluation[:suggestion],
18
+ metadata: evaluation[:metadata]
19
+ )
20
+ else
21
+ failure(
22
+ check_id,
23
+ evaluation[:message],
24
+ suggestion: evaluation[:suggestion],
25
+ metadata: evaluation[:metadata]
17
26
  )
18
27
  end
19
28
  end
29
+
30
+ private
31
+
32
+ def evaluate_schema
33
+ required = Schema::SolidQueueTables.required_tables
34
+ detection = Schema::SolidQueueTables.detected_tables(rails_root: rails_root)
35
+ missing = required - detection[:tables]
36
+ metadata = schema_metadata(detection, missing: missing)
37
+
38
+ return pass_evaluation(detection, required.size, metadata) if missing.empty?
39
+ return partial_evaluation(missing, metadata) if detection[:tables].any?
40
+
41
+ empty_evaluation(metadata)
42
+ end
43
+
44
+ def pass_evaluation(detection, table_count, metadata)
45
+ {
46
+ status: :pass,
47
+ message: success_message(detection, table_count),
48
+ metadata: metadata
49
+ }
50
+ end
51
+
52
+ def partial_evaluation(missing, metadata)
53
+ {
54
+ status: :warn,
55
+ message: "Missing Solid Queue tables: #{missing.join(', ')}",
56
+ suggestion: 'Run bin/rails solid_queue:install or migrate the queue database',
57
+ metadata: metadata
58
+ }
59
+ end
60
+
61
+ def empty_evaluation(metadata)
62
+ {
63
+ status: :fail,
64
+ message: [
65
+ 'No Solid Queue schema tables found in',
66
+ 'db/queue_schema.rb, db/schema.rb, db/structure.sql, or the queue database'
67
+ ].join(' '),
68
+ suggestion: 'Run bin/rails solid_queue:install and migrate the queue database',
69
+ metadata: metadata
70
+ }
71
+ end
72
+
73
+ def success_message(detection, table_count)
74
+ sources = source_descriptions(detection)
75
+ "Solid Queue schema complete (#{table_count}/#{table_count} tables) in #{sources.join(', ')}"
76
+ end
77
+
78
+ def source_descriptions(detection)
79
+ sources = detection[:file_tables].keys
80
+ sources << 'queue database' if detection[:database_tables].any?
81
+ sources.presence || ['schema files']
82
+ end
83
+
84
+ def schema_metadata(detection, missing:)
85
+ {
86
+ required_tables: Schema::SolidQueueTables.required_tables,
87
+ detected_tables: detection[:tables],
88
+ missing_tables: missing,
89
+ schema_sources: detection[:file_tables],
90
+ database_tables: detection[:database_tables]
91
+ }
92
+ end
20
93
  end
21
94
  end
22
95
  end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueGuard
4
+ module Schema
5
+ # @api private
6
+ class SolidQueueTables
7
+ SCHEMA_FILES = %w[
8
+ db/queue_schema.rb
9
+ db/schema.rb
10
+ db/structure.sql
11
+ ].freeze
12
+
13
+ RUBY_CREATE_TABLE_PATTERN = /
14
+ create_table
15
+ \s+
16
+ (?:
17
+ ["'](?<quoted>solid_queue_[a-z_]+)["']
18
+ |
19
+ :(?<symbol>solid_queue_[a-z_]+)
20
+ )
21
+ /ix
22
+
23
+ SQL_CREATE_TABLE_PATTERN = /
24
+ CREATE \ TABLE
25
+ (?: \ IF \ NOT \ EXISTS )?
26
+ \s+
27
+ (?:[\w.]+\.)?
28
+ (?:
29
+ "(?<quoted>solid_queue_[a-z_]+)"
30
+ |
31
+ (?<unquoted>solid_queue_[a-z_]+)
32
+ )
33
+ /ix
34
+
35
+ class << self
36
+ def required_tables
37
+ @required_tables ||= tables_from_gem_template || default_required_tables
38
+ end
39
+
40
+ def detected_tables(rails_root: Rails.root)
41
+ file_tables = {}
42
+ tables = Set.new
43
+
44
+ SCHEMA_FILES.each do |relative_path|
45
+ path = rails_root.join(relative_path)
46
+ next unless path.exist?
47
+
48
+ tables_in_file = parse_file(path)
49
+ next if tables_in_file.empty?
50
+
51
+ file_tables[relative_path] = tables_in_file.sort
52
+ tables.merge(tables_in_file)
53
+ end
54
+
55
+ database_tables = tables_in_queue_database
56
+ tables.merge(database_tables)
57
+
58
+ {
59
+ tables: tables.sort,
60
+ file_tables: file_tables,
61
+ database_tables: database_tables.sort
62
+ }
63
+ end
64
+
65
+ def missing_tables(rails_root: Rails.root)
66
+ required_tables - detected_tables(rails_root: rails_root)[:tables]
67
+ end
68
+
69
+ def tables_from_gem_template
70
+ spec = Gem.loaded_specs['solid_queue']
71
+ return nil unless spec
72
+
73
+ template = File.join(
74
+ spec.full_gem_path,
75
+ 'lib/generators/solid_queue/install/templates/db/queue_schema.rb'
76
+ )
77
+ return nil unless File.exist?(template)
78
+
79
+ parse_ruby_schema(File.read(template))
80
+ end
81
+
82
+ def default_required_tables
83
+ %w[
84
+ solid_queue_blocked_executions
85
+ solid_queue_claimed_executions
86
+ solid_queue_failed_executions
87
+ solid_queue_jobs
88
+ solid_queue_pauses
89
+ solid_queue_processes
90
+ solid_queue_ready_executions
91
+ solid_queue_recurring_executions
92
+ solid_queue_recurring_tasks
93
+ solid_queue_scheduled_executions
94
+ solid_queue_semaphores
95
+ ].freeze
96
+ end
97
+
98
+ private
99
+
100
+ def parse_file(path)
101
+ content = path.read
102
+ path.extname == '.sql' ? parse_sql_schema(content) : parse_ruby_schema(content)
103
+ end
104
+
105
+ def parse_ruby_schema(content)
106
+ content.scan(RUBY_CREATE_TABLE_PATTERN).flatten.compact.uniq
107
+ end
108
+
109
+ def parse_sql_schema(content)
110
+ content.scan(SQL_CREATE_TABLE_PATTERN).flatten.compact.uniq
111
+ end
112
+
113
+ def tables_in_queue_database
114
+ return [] unless defined?(SolidQueue::Record)
115
+
116
+ pool = SolidQueue::Record.connection_pool
117
+ return [] unless pool&.with_connection(&:active?)
118
+
119
+ SolidQueue::Record.connection.tables.grep(/\Asolid_queue_/)
120
+ rescue StandardError
121
+ []
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueueGuard
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue_guard
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rafael Pissardo
@@ -337,6 +337,7 @@ files:
337
337
  - lib/solid_queue_guard/recommendations/topology.rb
338
338
  - lib/solid_queue_guard/report.rb
339
339
  - lib/solid_queue_guard/runner.rb
340
+ - lib/solid_queue_guard/schema/solid_queue_tables.rb
340
341
  - lib/solid_queue_guard/tasks.rb
341
342
  - lib/solid_queue_guard/version.rb
342
343
  - solid_queue_guard.gemspec