flight_control 2.0.0.beta.1 → 2.0.0.beta.2

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: 1ad093eeb61f357dc4aead0c094eb413607dfa234cd7cdf4ed15579a0fa0af51
4
- data.tar.gz: a7774ba9273438d984569b4f739795019caaea77d1f1e42b033cd862e0af9231
3
+ metadata.gz: de1aaa590e44b1aafc7e7fce1e61ca8ddd6b472ec507b92a483fdedbc4f93b88
4
+ data.tar.gz: 0b961660c79020fab500de5513623ace40e919dc215ba390b6e01881fbf3d3d6
5
5
  SHA512:
6
- metadata.gz: 9ce93746fa317b44831c7fa4d12bca2590d25a76f4eeb375041df08895f326d1e8c25a414d10da06638b0c072c933c84bdbbe338bbf27a04dd77d0e93e8c9324
7
- data.tar.gz: 986514c76126354436fe67b584289a17cf864314f126c9f7c159f1207e48c7dd3816860b82fde3115cc04ecde18e7e483528ae3b93a7d3f8127c2264f89053c5
6
+ metadata.gz: e0342d2ccd6e3c30b76aa284eca7dcc14d02df7c1f36fcee9820a02b9535fe93775932a684734a12a994c23b7767545957e8469ea3e46ab32245ff7e3852a5d6
7
+ data.tar.gz: dc7d93b5214bed8a8bd122eb5d2918ec2b5841e9ae5ca5aa8cfb3a0b12fea340b7f0c4acc03bab0997aea1b34609377691a2a550f942204bc3e0e02d5f007691
data/CHANGELOG.md CHANGED
@@ -2,7 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [2.0.0] - 2026-07-20
5
+ ## [2.0.0.beta.2] - 2026-09-08
6
+
7
+ Ported from mission_control-jobs (upstream 1.1.0...1.2.0 and later):
8
+
9
+ - Adds `enqueued_at` and `scheduled_at` date-range filters, applied natively by Solid Queue.
10
+ - Adds a `back_to_main_app_path` setting to point the "Back to main app" link somewhere other than the host's root.
11
+ - Routes host-app route helpers used inside the engine through `main_app`, so a base controller redirecting to, say, `new_session_path` keeps working.
12
+ - Loads the engine's stylesheets individually so they also work under Propshaft.
13
+ - Shows the total of pending jobs on the queues page, and how many times a job has been retried.
14
+ - Truncates long job arguments in job lists.
15
+ - Orders failed jobs by when they failed rather than by job id.
16
+ - Submits the job filters on `change` instead of debouncing keystrokes.
17
+ - Swaps only the backtrace when toggling between clean and full.
18
+ - Disables Turbo's prefetching of links on hover.
19
+ - Redirects to the queues index when showing a queue that no longer exists.
20
+ - Shows the raw error when a failed execution's error can't be parsed.
21
+ - Fixes `config.active_job.default_page_size` not being applied.
22
+ - Fixes the "Expires" tooltip of blocked jobs rendering its markup as text.
23
+ - Fixes `JobProxy#duration` when `scheduled_at` isn't set.
24
+ - Avoids N+1 count queries when listing Solid Queue queues.
25
+
26
+ ## [2.0.0.beta.1] - 2026-07-20
6
27
 
7
28
  - Drops support for Resque and Async adapters to focus on Solid Queue.
8
29
  - Drops support for Rails versions below 8.1.
data/README.md CHANGED
@@ -107,6 +107,23 @@ If you do this, you can disable the default HTTP Basic Authentication using the
107
107
  config.flight_control.http_basic_auth_enabled = false
108
108
  ```
109
109
 
110
+ Your base controller's code runs inside the engine, whose route helpers take precedence. Route helpers of your app that Flight Control doesn't define itself, like `new_session_path` from Rails' authentication generator, still work as they are, for example from a `before_action` that redirects to the login page. Helpers that both your app and Flight Control define, like `root_path`, resolve to Flight Control's; use `main_app.root_path` to reach your app's.
111
+
112
+ #### Authentication via route constraints
113
+
114
+ You can also keep authentication out of Flight Control's controllers altogether and restrict access when mounting the engine, using [route constraints](https://guides.rubyonrails.org/routing.html#advanced-constraints). For example, with the sessions created by Rails' authentication generator:
115
+
116
+ ```ruby
117
+ # config/routes.rb
118
+ Rails.application.routes.draw do
119
+ constraints ->(request) { Session.find_by(id: request.cookie_jar.signed[:session_id])&.user&.admin? } do
120
+ mount FlightControl::Engine, at: "/jobs"
121
+ end
122
+ end
123
+ ```
124
+
125
+ Requests that don't satisfy the constraint never reach the engine. As with a custom base controller, disable HTTP Basic Authentication if you don't want both.
126
+
110
127
  ### Other configuration settings
111
128
 
112
129
  Besides `base_controller_class`, you can also set the following for `FlightControl` or `config.flight_control`:
@@ -116,6 +133,7 @@ Besides `base_controller_class`, you can also set the following for `FlightContr
116
133
  - `internal_query_count_limit`: in count queries, the maximum number of records that will be counted if the adapter needs to limit these queries. True counts above this number will be returned as `INFINITY`. This keeps count queries fast—defaults to `500,000`
117
134
  - `scheduled_job_delay_threshold`: the time duration before a scheduled job is considered delayed. Defaults to `1.minute` (a job is considered delayed if it hasn't transitioned from the `scheduled` status 1 minute after the scheduled time).
118
135
  - `show_console_help`: whether to show the console help. If you don't want the console help message, set this to `false`—defaults to `true`.
136
+ - `back_to_main_app_path`: an optional string path for the "Back to main app" link. Example: `'/admin'`. Defaults to the host app's `root_path` when available.
119
137
  - `backtrace_cleaner`: a backtrace cleaner used for optionally filtering backtraces on the Failed Jobs detail page. Defaults to `Rails::BacktraceCleaner.new`. See the [Advanced configuration](#advanced-configuration) section for how to configure/override this setting on a per application/server basis.
120
138
  - `filter_arguments`: an array of strings representing the job argument keys you want to filter out in the UI. This is useful for hiding sensitive user data. Currently, only root-level hash keys are supported.
121
139
 
@@ -223,6 +241,11 @@ ActiveJob.jobs.finished.where(job_class_name: "SomeJob")
223
241
 
224
242
  # All jobs in progress being run by a given worker
225
243
  ActiveJob.jobs.in_progress.where(worker_id: 42)
244
+
245
+ # Filtering by dates takes a range, which can be open on either end
246
+ ActiveJob.jobs.failed.where(enqueued_at: 2.days.ago..)
247
+ ActiveJob.jobs.scheduled.where(scheduled_at: Time.now..1.hour.from_now)
248
+ ActiveJob.jobs.finished.where(finished_at: ..1.week.ago)
226
249
  ```
227
250
 
228
251
  Some examples of bulk operations:
@@ -28,7 +28,8 @@ class ActiveJob::JobProxy < ActiveJob::Base
28
28
 
29
29
  def duration
30
30
  ended_at = finished_at || failed_at
31
- ended_at - scheduled_at if ended_at && scheduled_at
31
+ started_at = scheduled_at || enqueued_at
32
+ ended_at - started_at if ended_at && started_at
32
33
  end
33
34
 
34
35
  ActiveJob::JobsRelation::STATUSES.each do |status|
@@ -23,9 +23,9 @@ class ActiveJob::JobsRelation
23
23
  include Enumerable
24
24
 
25
25
  STATUSES = %i[pending failed in_progress blocked scheduled finished]
26
- FILTERS = %i[queue_name job_class_name]
26
+ FILTERS = %i[queue_name job_class_name scheduled_at enqueued_at]
27
27
 
28
- PROPERTIES = %i[queue_name status offset_value limit_value job_class_name worker_id recurring_task_id finished_at]
28
+ PROPERTIES = %i[queue_name status offset_value limit_value job_class_name worker_id recurring_task_id finished_at scheduled_at enqueued_at]
29
29
  attr_reader(*PROPERTIES, :default_page_size)
30
30
 
31
31
  delegate :last, :[], :reverse, to: :to_a
@@ -52,13 +52,17 @@ class ActiveJob::JobsRelation
52
52
  # * <tt>:worker_id</tt> - To only include the jobs processed by the provided worker.
53
53
  # * <tt>:recurring_task_id</tt> - To only include the jobs corresponding to runs of a recurring task.
54
54
  # * <tt>:finished_at</tt> - (Range) To only include the jobs finished between the provided range
55
- def where(job_class_name: nil, queue_name: nil, worker_id: nil, recurring_task_id: nil, finished_at: nil)
55
+ # * <tt>:scheduled_at</tt> - (Range) To only include the jobs scheduled between the provided range
56
+ # * <tt>:enqueued_at</tt> - (Range) To only include the jobs enqueued between the provided range
57
+ def where(job_class_name: nil, queue_name: nil, worker_id: nil, recurring_task_id: nil, finished_at: nil, scheduled_at: nil, enqueued_at: nil)
56
58
  # Remove nil arguments to avoid overriding parameters when concatenating +where+ clauses
57
59
  arguments = {job_class_name: job_class_name,
58
60
  queue_name: queue_name&.to_s,
59
61
  worker_id: worker_id,
60
62
  recurring_task_id: recurring_task_id,
61
- finished_at: finished_at}.compact
63
+ finished_at: finished_at,
64
+ scheduled_at: scheduled_at,
65
+ enqueued_at: enqueued_at}.compact
62
66
 
63
67
  clone_with(**arguments)
64
68
  end
@@ -273,7 +277,15 @@ class ActiveJob::JobsRelation
273
277
  end
274
278
 
275
279
  def satisfy_filter?(job)
276
- filters.all? { |property| public_send(property) == job.public_send(property) }
280
+ filters.all? { |property| satisfies_filter?(public_send(property), job.public_send(property)) }
281
+ end
282
+
283
+ def satisfies_filter?(filter_value, job_value)
284
+ if filter_value.is_a?(Range)
285
+ filter_value.cover?(job_value)
286
+ else
287
+ filter_value == job_value
288
+ end
277
289
  end
278
290
 
279
291
  def filters
@@ -5,12 +5,14 @@ module ActiveJob::QueueAdapters::SolidQueueExt
5
5
 
6
6
  def queues
7
7
  queues = SolidQueue::Queue.all
8
- pauses = SolidQueue::Pause.where(queue_name: queues.map(&:name)).index_by(&:queue_name)
8
+ queue_names = queues.map(&:name)
9
+ pauses = SolidQueue::Pause.where(queue_name: queue_names).index_by(&:queue_name)
10
+ sizes = SolidQueue::ReadyExecution.where(queue_name: queue_names).group(:queue_name).count
9
11
 
10
12
  queues.collect do |queue|
11
13
  {
12
14
  name: queue.name,
13
- size: queue.size,
15
+ size: sizes[queue.name] || 0,
14
16
  active: pauses[queue.name].nil?
15
17
  }
16
18
  end
@@ -41,7 +43,7 @@ module ActiveJob::QueueAdapters::SolidQueueExt
41
43
  end
42
44
 
43
45
  def supported_job_filters(*)
44
- [:queue_name, :job_class_name, :finished_at]
46
+ [:queue_name, :job_class_name, :finished_at, :scheduled_at, :enqueued_at]
45
47
  end
46
48
 
47
49
  def jobs_count(jobs_relation)
@@ -129,6 +131,11 @@ module ActiveJob::QueueAdapters::SolidQueueExt
129
131
  message: solid_queue_job.failed_execution.message,
130
132
  backtrace: solid_queue_job.failed_execution.backtrace || []
131
133
  end
134
+ rescue JSON::ParserError
135
+ ActiveJob::ExecutionError.new \
136
+ error_class: "",
137
+ message: solid_queue_job.failed_execution.error_before_type_cast,
138
+ backtrace: []
132
139
  end
133
140
 
134
141
  def dispatch_immediately(job)
@@ -183,7 +190,7 @@ module ActiveJob::QueueAdapters::SolidQueueExt
183
190
  attr_reader :jobs_relation
184
191
 
185
192
  delegate :queue_name, :limit_value, :limit_value_provided?, :offset_value, :job_class_name,
186
- :default_page_size, :worker_id, :recurring_task_id, :finished_at, to: :jobs_relation
193
+ :default_page_size, :worker_id, :recurring_task_id, :finished_at, :scheduled_at, :enqueued_at, to: :jobs_relation
187
194
 
188
195
  def executions
189
196
  execution_class_by_status
@@ -192,6 +199,8 @@ module ActiveJob::QueueAdapters::SolidQueueExt
192
199
  .then { |executions| filter_executions_by_class(executions) }
193
200
  .then { |executions| filter_executions_by_process_id(executions) }
194
201
  .then { |executions| filter_executions_by_task_key(executions) }
202
+ .then { |executions| filter_executions_by_scheduled_at(executions) }
203
+ .then { |executions| filter_executions_by_enqueued_at(executions) }
195
204
  .then { |executions| limit(executions) }
196
205
  .then { |executions| offset(executions) }
197
206
  end
@@ -201,6 +210,8 @@ module ActiveJob::QueueAdapters::SolidQueueExt
201
210
  .then { |jobs| filter_jobs_by_queue(jobs) }
202
211
  .then { |jobs| filter_jobs_by_class(jobs) }
203
212
  .then { |jobs| filter_jobs_by_finished_at(jobs) }
213
+ .then { |jobs| filter_jobs_by_scheduled_at(jobs) }
214
+ .then { |jobs| filter_jobs_by_enqueued_at(jobs) }
204
215
  .then { |jobs| limit(jobs) }
205
216
  .then { |jobs| offset(jobs) }
206
217
  end
@@ -210,9 +221,13 @@ module ActiveJob::QueueAdapters::SolidQueueExt
210
221
  end
211
222
 
212
223
  def order_executions(executions)
213
- # Follow polling order for scheduled executions, the rest newest first
224
+ # Follow polling order for scheduled executions, the rest newest first.
225
+ # Failed executions go by their own id: a job can fail again after being
226
+ # retried, and what's newest is the failure, not the job.
214
227
  if solid_queue_status.scheduled?
215
228
  executions.ordered
229
+ elsif solid_queue_status.failed?
230
+ executions.order(id: :desc)
216
231
  else
217
232
  executions.order(job_id: :desc)
218
233
  end
@@ -286,6 +301,29 @@ module ActiveJob::QueueAdapters::SolidQueueExt
286
301
  finished_at.present? ? jobs.where(finished_at: finished_at) : jobs
287
302
  end
288
303
 
304
+ def filter_executions_by_scheduled_at(executions)
305
+ return executions unless scheduled_at.present?
306
+
307
+ if solid_queue_status.scheduled?
308
+ executions.where(scheduled_at: scheduled_at)
309
+ else
310
+ executions.where(job: {scheduled_at: scheduled_at})
311
+ end
312
+ end
313
+
314
+ def filter_jobs_by_scheduled_at(jobs)
315
+ scheduled_at.present? ? jobs.where(scheduled_at: scheduled_at) : jobs
316
+ end
317
+
318
+ # Jobs are created when enqueued
319
+ def filter_executions_by_enqueued_at(executions)
320
+ enqueued_at.present? ? executions.where(job: {created_at: enqueued_at}) : executions
321
+ end
322
+
323
+ def filter_jobs_by_enqueued_at(jobs)
324
+ enqueued_at.present? ? jobs.where(created_at: enqueued_at) : jobs
325
+ end
326
+
289
327
  def limit(executions_or_jobs)
290
328
  limit_value.present? ? executions_or_jobs.limit(limit_value) : executions_or_jobs
291
329
  end
@@ -38,12 +38,18 @@ module FlightControl
38
38
  end
39
39
  end
40
40
 
41
+ initializer "flight_control.host_route_helpers" do |app|
42
+ ActiveSupport.on_load(:after_routes_loaded) do
43
+ FlightControl::HostRouteHelpers.define_from(app.routes, FlightControl::Engine.routes)
44
+ end
45
+ end
46
+
41
47
  initializer "flight_control.http_basic_auth" do |app|
42
48
  FlightControl.http_basic_auth_user ||= app.credentials.dig(:flight_control, :http_basic_auth_user)
43
49
  FlightControl.http_basic_auth_password ||= app.credentials.dig(:flight_control, :http_basic_auth_password)
44
50
  end
45
51
 
46
- initializer "flight_control.active_job.extensions" do
52
+ initializer "flight_control.active_job.extensions", before: "active_job.set_configs" do
47
53
  ActiveSupport.on_load :active_job do
48
54
  include ActiveJob::Querying
49
55
  include ActiveJob::Executing
@@ -0,0 +1,34 @@
1
+ # Route helpers of the host app that the engine doesn't define itself, routed through +main_app+.
2
+ #
3
+ # Flight Control's controllers inherit from the host app's +ApplicationController+ (see
4
+ # +base_controller_class+), so host code such as a +before_action+ redirecting to
5
+ # +new_session_path+ runs inside the engine, where route helpers resolve against the engine's
6
+ # routes and raise +ActionController::UrlGenerationError+. This module defines, for every route
7
+ # helper the host app has and the engine doesn't, a method delegating to the host app's routes,
8
+ # so that code keeps working unchanged.
9
+ #
10
+ # Helpers defined by both, like +root_path+, keep resolving to the engine's, as in any isolated
11
+ # engine; use +main_app.root_path+ for the host's.
12
+ module FlightControl::HostRouteHelpers
13
+ class << self
14
+ def define_from(host_routes, engine_routes)
15
+ undefine_all
16
+
17
+ (host_routes.named_routes.helper_names - engine_routes.named_routes.helper_names).each do |name|
18
+ define_method(name) { |*args| main_app.public_send(name, *args) }
19
+ defined_helpers << name
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def undefine_all
26
+ defined_helpers.each { |name| remove_method(name) }
27
+ defined_helpers.clear
28
+ end
29
+
30
+ def defined_helpers
31
+ @defined_helpers ||= []
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,3 @@
1
1
  module FlightControl
2
- VERSION = "2.0.0.beta.1"
2
+ VERSION = "2.0.0.beta.2"
3
3
  end
@@ -24,6 +24,7 @@ module FlightControl
24
24
 
25
25
  mattr_accessor :show_console_help, default: true
26
26
  mattr_accessor :backtrace_cleaner
27
+ mattr_accessor :back_to_main_app_path
27
28
 
28
29
  mattr_accessor :importmap, default: Importmap::Map.new
29
30
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flight_control
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.beta.1
4
+ version: 2.0.0.beta.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jury Razumau
@@ -340,6 +340,7 @@ files:
340
340
  - lib/flight_control/errors/incompatible_adapter.rb
341
341
  - lib/flight_control/errors/resource_not_found.rb
342
342
  - lib/flight_control/errors/unsupported_adapter.rb
343
+ - lib/flight_control/host_route_helpers.rb
343
344
  - lib/flight_control/i18n_config.rb
344
345
  - lib/flight_control/identified_by_name.rb
345
346
  - lib/flight_control/identified_elements.rb