maintenance_tasks 1.3.0 → 1.7.0

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +288 -45
  3. data/app/controllers/maintenance_tasks/tasks_controller.rb +7 -2
  4. data/app/helpers/maintenance_tasks/application_helper.rb +1 -0
  5. data/app/helpers/maintenance_tasks/tasks_helper.rb +19 -14
  6. data/app/jobs/concerns/maintenance_tasks/task_job_concern.rb +44 -23
  7. data/app/models/maintenance_tasks/application_record.rb +1 -0
  8. data/app/models/maintenance_tasks/csv_collection_builder.rb +33 -0
  9. data/app/models/maintenance_tasks/null_collection_builder.rb +31 -0
  10. data/app/models/maintenance_tasks/progress.rb +8 -3
  11. data/app/models/maintenance_tasks/run.rb +224 -18
  12. data/app/models/maintenance_tasks/runner.rb +26 -7
  13. data/app/models/maintenance_tasks/runs_page.rb +1 -0
  14. data/app/models/maintenance_tasks/task.rb +225 -0
  15. data/app/models/maintenance_tasks/task_data.rb +24 -3
  16. data/app/validators/maintenance_tasks/run_status_validator.rb +2 -2
  17. data/app/views/maintenance_tasks/runs/_arguments.html.erb +22 -0
  18. data/app/views/maintenance_tasks/runs/_csv.html.erb +5 -0
  19. data/app/views/maintenance_tasks/runs/_run.html.erb +18 -1
  20. data/app/views/maintenance_tasks/runs/info/_custom.html.erb +0 -0
  21. data/app/views/maintenance_tasks/runs/info/_errored.html.erb +0 -2
  22. data/app/views/maintenance_tasks/runs/info/_paused.html.erb +2 -2
  23. data/app/views/maintenance_tasks/runs/info/_running.html.erb +3 -5
  24. data/app/views/maintenance_tasks/tasks/_custom.html.erb +0 -0
  25. data/app/views/maintenance_tasks/tasks/_task.html.erb +19 -1
  26. data/app/views/maintenance_tasks/tasks/index.html.erb +2 -2
  27. data/app/views/maintenance_tasks/tasks/show.html.erb +37 -2
  28. data/config/routes.rb +1 -0
  29. data/db/migrate/20201211151756_create_maintenance_tasks_runs.rb +1 -0
  30. data/db/migrate/20210225152418_remove_index_on_task_name.rb +1 -0
  31. data/db/migrate/20210517131953_add_arguments_to_maintenance_tasks_runs.rb +7 -0
  32. data/db/migrate/20211210152329_add_lock_version_to_maintenance_tasks_runs.rb +8 -0
  33. data/lib/generators/maintenance_tasks/install_generator.rb +1 -0
  34. data/lib/generators/maintenance_tasks/templates/task.rb.tt +3 -1
  35. data/lib/maintenance_tasks/cli.rb +11 -4
  36. data/lib/maintenance_tasks/engine.rb +15 -1
  37. data/lib/maintenance_tasks.rb +14 -1
  38. data/lib/patches/active_record_batch_enumerator.rb +23 -0
  39. metadata +15 -11
  40. data/app/models/maintenance_tasks/csv_collection.rb +0 -33
  41. data/app/tasks/maintenance_tasks/task.rb +0 -133
  42. data/app/views/maintenance_tasks/runs/_info.html.erb +0 -16
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MaintenanceTasks
4
+ # Base class that is inherited by the host application's task classes.
5
+ class Task
6
+ extend ActiveSupport::DescendantsTracker
7
+ include ActiveSupport::Callbacks
8
+ include ActiveModel::Attributes
9
+ include ActiveModel::AttributeAssignment
10
+ include ActiveModel::Validations
11
+
12
+ class NotFoundError < NameError; end
13
+
14
+ # The throttle conditions for a given Task. This is provided as an array of
15
+ # hashes, with each hash specifying two keys: throttle_condition and
16
+ # backoff. Note that Tasks inherit conditions from their superclasses.
17
+ #
18
+ # @api private
19
+ class_attribute :throttle_conditions, default: []
20
+
21
+ class_attribute :collection_builder_strategy,
22
+ default: NullCollectionBuilder.new
23
+
24
+ define_callbacks :start, :complete, :error, :cancel, :pause, :interrupt
25
+
26
+ class << self
27
+ # Finds a Task with the given name.
28
+ #
29
+ # @param name [String] the name of the Task to be found.
30
+ #
31
+ # @return [Task] the Task with the given name.
32
+ #
33
+ # @raise [NotFoundError] if a Task with the given name does not exist.
34
+ def named(name)
35
+ task = name.safe_constantize
36
+ raise NotFoundError.new("Task #{name} not found.", name) unless task
37
+ unless task.is_a?(Class) && task < Task
38
+ raise NotFoundError.new("#{name} is not a Task.", name)
39
+ end
40
+ task
41
+ end
42
+
43
+ # Returns a list of concrete classes that inherit from the Task
44
+ # superclass.
45
+ #
46
+ # @return [Array<Class>] the list of classes.
47
+ def available_tasks
48
+ load_constants
49
+ descendants
50
+ end
51
+
52
+ # Make this Task a task that handles CSV.
53
+ #
54
+ # An input to upload a CSV will be added in the form to start a Run. The
55
+ # collection and count method are implemented.
56
+ def csv_collection
57
+ unless defined?(ActiveStorage)
58
+ raise NotImplementedError, "Active Storage needs to be installed\n"\
59
+ "To resolve this issue run: bin/rails active_storage:install"
60
+ end
61
+
62
+ self.collection_builder_strategy =
63
+ MaintenanceTasks::CsvCollectionBuilder.new
64
+ end
65
+
66
+ # Returns whether the Task handles CSV.
67
+ #
68
+ # @return [Boolean] whether the Task handles CSV.
69
+ def has_csv_content?
70
+ collection_builder_strategy.has_csv_content?
71
+ end
72
+
73
+ # Processes one item.
74
+ #
75
+ # Especially useful for tests.
76
+ #
77
+ # @param item the item to process.
78
+ def process(item)
79
+ new.process(item)
80
+ end
81
+
82
+ # Returns the collection for this Task.
83
+ #
84
+ # Especially useful for tests.
85
+ #
86
+ # @return the collection.
87
+ def collection
88
+ new.collection
89
+ end
90
+
91
+ # Returns the count of items for this Task.
92
+ #
93
+ # Especially useful for tests.
94
+ #
95
+ # @return the count of items.
96
+ def count
97
+ new.count
98
+ end
99
+
100
+ # Add a condition under which this Task will be throttled.
101
+ #
102
+ # @param backoff [ActiveSupport::Duration, #call] a custom backoff
103
+ # can be specified. This is the time to wait before retrying the Task,
104
+ # defaulting to 30 seconds. If provided as a Duration, the backoff is
105
+ # wrapped in a proc. Alternatively,an object responding to call can be
106
+ # used. It must return an ActiveSupport::Duration.
107
+ # @yieldreturn [Boolean] where the throttle condition is being met,
108
+ # indicating that the Task should throttle.
109
+ def throttle_on(backoff: 30.seconds, &condition)
110
+ backoff_as_proc = backoff
111
+ backoff_as_proc = -> { backoff } unless backoff.respond_to?(:call)
112
+
113
+ self.throttle_conditions += [
114
+ { throttle_on: condition, backoff: backoff_as_proc },
115
+ ]
116
+ end
117
+
118
+ # Initialize a callback to run after the task starts.
119
+ #
120
+ # @param filter_list apply filters to the callback
121
+ # (see https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)
122
+ def after_start(*filter_list, &block)
123
+ set_callback(:start, :after, *filter_list, &block)
124
+ end
125
+
126
+ # Initialize a callback to run after the task completes.
127
+ #
128
+ # @param filter_list apply filters to the callback
129
+ # (see https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)
130
+ def after_complete(*filter_list, &block)
131
+ set_callback(:complete, :after, *filter_list, &block)
132
+ end
133
+
134
+ # Initialize a callback to run after the task pauses.
135
+ #
136
+ # @param filter_list apply filters to the callback
137
+ # (see https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)
138
+ def after_pause(*filter_list, &block)
139
+ set_callback(:pause, :after, *filter_list, &block)
140
+ end
141
+
142
+ # Initialize a callback to run after the task is interrupted.
143
+ #
144
+ # @param filter_list apply filters to the callback
145
+ # (see https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)
146
+ def after_interrupt(*filter_list, &block)
147
+ set_callback(:interrupt, :after, *filter_list, &block)
148
+ end
149
+
150
+ # Initialize a callback to run after the task is cancelled.
151
+ #
152
+ # @param filter_list apply filters to the callback
153
+ # (see https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)
154
+ def after_cancel(*filter_list, &block)
155
+ set_callback(:cancel, :after, *filter_list, &block)
156
+ end
157
+
158
+ # Initialize a callback to run after the task produces an error.
159
+ #
160
+ # @param filter_list apply filters to the callback
161
+ # (see https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback)
162
+ def after_error(*filter_list, &block)
163
+ set_callback(:error, :after, *filter_list, &block)
164
+ end
165
+
166
+ private
167
+
168
+ def load_constants
169
+ namespace = MaintenanceTasks.tasks_module.safe_constantize
170
+ return unless namespace
171
+ namespace.constants.map { |constant| namespace.const_get(constant) }
172
+ end
173
+ end
174
+
175
+ # The contents of a CSV file to be processed by a Task.
176
+ #
177
+ # @return [String] the content of the CSV file to process.
178
+ def csv_content
179
+ raise NoMethodError unless has_csv_content?
180
+
181
+ @csv_content
182
+ end
183
+
184
+ # Set the contents of a CSV file to be processed by a Task.
185
+ #
186
+ # @param csv_content [String] the content of the CSV file to process.
187
+ def csv_content=(csv_content)
188
+ raise NoMethodError unless has_csv_content?
189
+
190
+ @csv_content = csv_content
191
+ end
192
+
193
+ # Returns whether the Task handles CSV.
194
+ #
195
+ # @return [Boolean] whether the Task handles CSV.
196
+ def has_csv_content?
197
+ self.class.has_csv_content?
198
+ end
199
+
200
+ # The collection to be processed, delegated to the strategy.
201
+ #
202
+ # @return the collection.
203
+ def collection
204
+ self.class.collection_builder_strategy.collection(self)
205
+ end
206
+
207
+ # Placeholder method to raise in case a subclass fails to implement the
208
+ # expected instance method.
209
+ #
210
+ # @param _item [Object] the current item from the enumerator being iterated.
211
+ #
212
+ # @raise [NotImplementedError] with a message advising subclasses to
213
+ # implement an override for this method.
214
+ def process(_item)
215
+ raise NoMethodError, "#{self.class.name} must implement `process`."
216
+ end
217
+
218
+ # Total count of iterations to be performed, delegated to the strategy.
219
+ #
220
+ # @return [Integer, nil]
221
+ def count
222
+ self.class.collection_builder_strategy.count(self)
223
+ end
224
+ end
225
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module MaintenanceTasks
3
4
  # Class that represents the data related to a Task. Such information can be
4
5
  # sourced from a Task or from existing Run records for a Task that was since
@@ -40,7 +41,7 @@ module MaintenanceTasks
40
41
  def available_tasks
41
42
  task_names = Task.available_tasks.map(&:name)
42
43
  available_task_runs = Run.where(task_name: task_names)
43
- last_runs = Run.where(
44
+ last_runs = Run.with_attached_csv.where(
44
45
  id: available_task_runs.select("MAX(id) as id").group(:task_name)
45
46
  )
46
47
 
@@ -73,7 +74,11 @@ module MaintenanceTasks
73
74
  def code
74
75
  return if deleted?
75
76
  task = Task.named(name)
76
- file = task.instance_method(:process).source_location.first
77
+ file = if Object.respond_to?(:const_source_location)
78
+ Object.const_source_location(task.name).first
79
+ else
80
+ task.instance_method(:process).source_location.first
81
+ end
77
82
  File.read(file)
78
83
  end
79
84
 
@@ -129,7 +134,23 @@ module MaintenanceTasks
129
134
 
130
135
  # @return [Boolean] whether the Task inherits from CsvTask.
131
136
  def csv_task?
132
- !deleted? && Task.named(name) < CsvCollection
137
+ !deleted? && Task.named(name).has_csv_content?
138
+ end
139
+
140
+ # @return [Array<String>] the names of parameters the Task accepts.
141
+ def parameter_names
142
+ if deleted?
143
+ []
144
+ else
145
+ Task.named(name).attribute_names
146
+ end
147
+ end
148
+
149
+ # @return [MaintenanceTasks::Task, nil] an instance of the Task class.
150
+ # @return [nil] if the Task file was deleted.
151
+ def new
152
+ return if deleted?
153
+ MaintenanceTasks::Task.named(name).new
133
154
  end
134
155
 
135
156
  private
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module MaintenanceTasks
3
4
  # Custom validator class responsible for ensuring that transitions between
4
5
  # Run statuses are valid.
@@ -48,9 +49,8 @@ module MaintenanceTasks
48
49
  "errored",
49
50
  ],
50
51
  # paused -> enqueued occurs when the task is resumed after being paused.
51
- # paused -> cancelling when the user cancels the task after it is paused.
52
52
  # paused -> cancelled when the user cancels the task after it is paused.
53
- "paused" => ["enqueued", "cancelling", "cancelled"],
53
+ "paused" => ["enqueued", "cancelled"],
54
54
  # interrupted -> running occurs when the task is resumed after being
55
55
  # interrupted by the job infrastructure.
56
56
  # interrupted -> pausing occurs when the task is paused by the user while
@@ -0,0 +1,22 @@
1
+ <% if run.arguments.present? %>
2
+ <div class="table-container">
3
+ <h6 class="title is-6">Arguments:</h6>
4
+ <table class="table">
5
+ <tbody>
6
+ <% run.arguments.each do |key, value| %>
7
+ <tr>
8
+ <td class="is-family-monospace"><%= key %></td>
9
+ <td>
10
+ <% next if value.nil? || value.empty? %>
11
+ <% if value.include?("\n") %>
12
+ <pre><%= value %><pre>
13
+ <% else %>
14
+ <code><%= value %><code>
15
+ <% end %>
16
+ </td>
17
+ </tr>
18
+ <% end %>
19
+ </tbody>
20
+ </table>
21
+ </div>
22
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <% if run.csv_file.present? %>
2
+ <div class="block">
3
+ <%= link_to("Download CSV", csv_file_download_path(run)) %>
4
+ </div>
5
+ <% end %>
@@ -1,3 +1,20 @@
1
1
  <div class="box">
2
- <%= render 'maintenance_tasks/runs/info', run: run, with_status: true %>
2
+ <h5 class="title is-5">
3
+ <%= time_tag run.created_at, title: run.created_at %>
4
+ <%= status_tag run.status %>
5
+ </h5>
6
+
7
+ <%= progress run %>
8
+
9
+ <div class="content">
10
+ <%= render "maintenance_tasks/runs/info/#{run.status}", run: run %>
11
+ </div>
12
+
13
+ <div class="content" id="custom-content">
14
+ <%= render "maintenance_tasks/runs/info/custom", run: run %>
15
+ </div>
16
+
17
+ <%= render "maintenance_tasks/runs/csv", run: run %>
18
+ <%= tag.hr if run.csv_file.present? && run.arguments.present? %>
19
+ <%= render "maintenance_tasks/runs/arguments", run: run %>
3
20
  </div>
@@ -3,8 +3,6 @@
3
3
  <%= time_ago run.ended_at %>.
4
4
  </p>
5
5
 
6
- </p>
7
-
8
6
  <div class="card">
9
7
  <header class="card-header">
10
8
  <p class="card-header-title">
@@ -1,7 +1,7 @@
1
1
  <p>
2
2
  Ran for <%= time_running_in_words run %> until paused,
3
- <% if run.estimated_completion_time %>
4
- <%= estimated_time_to_completion(run) %> remaining.
3
+ <% if (time_to_completion = run.time_to_completion) %>
4
+ <%= distance_of_time_in_words(time_to_completion) %> remaining.
5
5
  <% else %>
6
6
  processed <%= pluralize run.tick_count, 'item' %> so far.
7
7
  <% end %>
@@ -1,5 +1,3 @@
1
- <p>
2
- <% if run.estimated_completion_time %>
3
- <%= estimated_time_to_completion(run).capitalize %> remaining.
4
- <% end %>
5
- </p>
1
+ <% if (time_to_completion = run.time_to_completion) %>
2
+ <p>Running for <%= time_running_in_words(run) %>. <%= distance_of_time_in_words(time_to_completion).capitalize %> remaining.</p>
3
+ <% end %>
@@ -4,5 +4,23 @@
4
4
  <%= status_tag(task.status) %>
5
5
  </h3>
6
6
 
7
- <%= render 'maintenance_tasks/runs/info', run: task.last_run, with_status: false if task.last_run %>
7
+ <% if (run = task.last_run) %>
8
+ <h5 class="title is-5">
9
+ <%= time_tag run.created_at, title: run.created_at %>
10
+ </h5>
11
+
12
+ <%= progress run %>
13
+
14
+ <div class="content">
15
+ <%= render "maintenance_tasks/runs/info/#{run.status}", run: run %>
16
+ </div>
17
+
18
+ <div class="content" id="custom-content">
19
+ <%= render "maintenance_tasks/runs/info/custom", run: run %>
20
+ </div>
21
+
22
+ <%= render "maintenance_tasks/runs/csv", run: run %>
23
+ <%= tag.hr if run.csv_file.present? && run.arguments.present? %>
24
+ <%= render "maintenance_tasks/runs/arguments", run: run %>
25
+ <% end %>
8
26
  </div>
@@ -2,8 +2,8 @@
2
2
  <div class="content is-large">
3
3
  <h3 class="title is-3"> The MaintenanceTasks gem has been successfully installed! </h3>
4
4
  <p>
5
- Any new Tasks will show up here. To start writing your first Task,
6
- run <code>rails generate maintenance_tasks:task my_task</code>.
5
+ Any new Tasks will show up here. To start writing your first Task,
6
+ run <code>bin/rails generate maintenance_tasks:task my_task</code>.
7
7
  </p>
8
8
  </div>
9
9
  <% else %>
@@ -4,10 +4,29 @@
4
4
  <%= @task %> <%= status_tag(@task.status) %>
5
5
  </h1>
6
6
 
7
- <%= render 'maintenance_tasks/runs/info', run: @task.last_run, with_status: false if @task.last_run %>
7
+ <% last_run = @task.last_run %>
8
+ <% if last_run %>
9
+ <h5 class="title is-5">
10
+ <%= time_tag last_run.created_at, title: last_run.created_at %>
11
+ </h5>
12
+
13
+ <%= progress last_run %>
14
+
15
+ <div class="content">
16
+ <%= render "maintenance_tasks/runs/info/#{last_run.status}", run: last_run %>
17
+ </div>
18
+
19
+ <div class="content" id="custom-content">
20
+ <%= render "maintenance_tasks/runs/info/custom", run: last_run %>
21
+ </div>
22
+
23
+ <%= render "maintenance_tasks/runs/csv", run: last_run %>
24
+ <%= tag.hr if last_run.csv_file.present? %>
25
+ <%= render "maintenance_tasks/runs/arguments", run: last_run %>
26
+ <%= tag.hr if last_run.arguments.present? %>
27
+ <% end %>
8
28
 
9
29
  <div class="buttons">
10
- <% last_run = @task.last_run %>
11
30
  <% if last_run.nil? || last_run.completed? %>
12
31
  <%= form_with url: run_task_path(@task), method: :put do |form| %>
13
32
  <% if @task.csv_task? %>
@@ -16,6 +35,22 @@
16
35
  <%= form.file_field :csv_file %>
17
36
  </div>
18
37
  <% end %>
38
+ <% parameter_names = @task.parameter_names %>
39
+ <% if parameter_names.any? %>
40
+ <div class="block">
41
+ <%= form.fields_for :task_arguments, @task.new do |ff| %>
42
+ <% parameter_names.each do |parameter_name| %>
43
+ <div class="field">
44
+ <%= ff.label parameter_name, parameter_name, class: "label is-family-monospace" %>
45
+ <div class="control">
46
+ <%= parameter_field(ff, parameter_name) %>
47
+ </div>
48
+ </div>
49
+ <% end %>
50
+ <% end %>
51
+ </div>
52
+ <% end %>
53
+ <%= render "maintenance_tasks/tasks/custom", form: form %>
19
54
  <div class="block">
20
55
  <%= form.submit 'Run', class: "button is-success", disabled: @task.deleted? %>
21
56
  </div>
data/config/routes.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  MaintenanceTasks::Engine.routes.draw do
3
4
  resources :tasks, only: [:index, :show], format: false do
4
5
  member do
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class CreateMaintenanceTasksRuns < ActiveRecord::Migration[6.0]
3
4
  def change
4
5
  create_table(:maintenance_tasks_runs) do |t|
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class RemoveIndexOnTaskName < ActiveRecord::Migration[6.0]
3
4
  def up
4
5
  change_table(:maintenance_tasks_runs) do |t|
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddArgumentsToMaintenanceTasksRuns < ActiveRecord::Migration[6.0]
4
+ def change
5
+ add_column(:maintenance_tasks_runs, :arguments, :text)
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddLockVersionToMaintenanceTasksRuns < ActiveRecord::Migration[6.0]
4
+ def change
5
+ add_column(:maintenance_tasks_runs, :lock_version, :integer,
6
+ default: 0, null: false)
7
+ end
8
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module MaintenanceTasks
3
4
  # Generator used to set up the engine in the host application. It handles
4
5
  # mounting the engine and installing migrations.
@@ -9,7 +9,9 @@ module <%= tasks_module %>
9
9
  end
10
10
 
11
11
  def process(element)
12
- # The work to be done in a single iteration of the task
12
+ # The work to be done in a single iteration of the task.
13
+ # This should be idempotent, as the same element may be processed more
14
+ # than once if the task is interrupted and resumed.
13
15
  end
14
16
 
15
17
  def count
@@ -25,18 +25,25 @@ module MaintenanceTasks
25
25
  LONGDESC
26
26
 
27
27
  # Specify the CSV file to process for CSV Tasks
28
- option :csv, desc: "Supply a CSV file to be processed by a CSV Task, "\
29
- '--csv "path/to/csv/file.csv"'
28
+ desc = "Supply a CSV file to be processed by a CSV Task, "\
29
+ "--csv path/to/csv/file.csv"
30
+ option :csv, desc: desc
31
+ # Specify arguments to supply to a Task supporting parameters
32
+ desc = "Supply arguments for a Task that accepts parameters as a set of "\
33
+ "<key>:<value> pairs."
34
+ option :arguments, type: :hash, desc: desc
30
35
 
31
36
  # Command to run a Task.
32
37
  #
33
38
  # It instantiates a Runner and sends a run message with the given Task name.
34
39
  # If a CSV file is supplied using the --csv option, an attachable with the
35
- # File IO object is sent along with the Task name to run.
40
+ # File IO object is sent along with the Task name to run. If arguments are
41
+ # supplied using the --arguments option, these are also passed to run.
36
42
  #
37
43
  # @param name [String] the name of the Task to be run.
38
44
  def perform(name)
39
- task = Runner.run(name: name, csv_file: csv_file)
45
+ arguments = options[:arguments] || {}
46
+ task = Runner.run(name: name, csv_file: csv_file, arguments: arguments)
40
47
  say_status(:success, "#{task.name} was enqueued.", :green)
41
48
  rescue => error
42
49
  say_status(:error, error.message, :red)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "active_record/railtie"
3
4
 
4
5
  module MaintenanceTasks
@@ -7,10 +8,23 @@ module MaintenanceTasks
7
8
  class Engine < ::Rails::Engine
8
9
  isolate_namespace MaintenanceTasks
9
10
 
10
- initializer "eager_load_for_classic_autoloader" do
11
+ initializer "maintenance_tasks.warn_classic_autoloader" do
12
+ unless Rails.autoloaders.zeitwerk_enabled?
13
+ ActiveSupport::Deprecation.warn(<<~MSG.squish)
14
+ Autoloading in classic mode is deprecated and support will be removed in the next
15
+ release of Maintenance Tasks. Please use Zeitwerk to autoload your application.
16
+ MSG
17
+ end
18
+ end
19
+
20
+ initializer "maintenance_tasks.eager_load_for_classic_autoloader" do
11
21
  eager_load! unless Rails.autoloaders.zeitwerk_enabled?
12
22
  end
13
23
 
24
+ initializer "maintenance_tasks.configs" do
25
+ MaintenanceTasks.backtrace_cleaner = Rails.backtrace_cleaner
26
+ end
27
+
14
28
  config.to_prepare do
15
29
  _ = TaskJobConcern # load this for JobIteration compatibility check
16
30
  unless Rails.autoloaders.zeitwerk_enabled?
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "action_controller"
3
4
  require "action_view"
4
5
  require "active_job"
@@ -7,6 +8,8 @@ require "active_record"
7
8
  require "job-iteration"
8
9
  require "maintenance_tasks/engine"
9
10
 
11
+ require "patches/active_record_batch_enumerator"
12
+
10
13
  # The engine's namespace module. It provides isolation between the host
11
14
  # application's code and the engine-specific code. Top-level engine constants
12
15
  # and variables are defined under this module.
@@ -50,6 +53,16 @@ module MaintenanceTasks
50
53
  # app's config/storage.yml.
51
54
  mattr_accessor :active_storage_service
52
55
 
56
+ # @!attribute backtrace_cleaner
57
+ # @scope class
58
+ #
59
+ # The Active Support backtrace cleaner that will be used to clean the
60
+ # backtrace of a Task that errors.
61
+ #
62
+ # @return [ActiveSupport::BacktraceCleaner, nil] the backtrace cleaner to
63
+ # use when cleaning a Run's backtrace.
64
+ mattr_accessor :backtrace_cleaner
65
+
53
66
  # @private
54
67
  def self.error_handler
55
68
  return @error_handler if defined?(@error_handler)
@@ -61,7 +74,7 @@ module MaintenanceTasks
61
74
  unless error_handler.arity == 3
62
75
  ActiveSupport::Deprecation.warn(
63
76
  "MaintenanceTasks.error_handler should be a lambda that takes three "\
64
- "arguments: error, task_context, and errored_element."
77
+ "arguments: error, task_context, and errored_element."
65
78
  )
66
79
  @error_handler = ->(error, _task_context, _errored_element) do
67
80
  error_handler.call(error)
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: Remove this patch once all supported Rails versions include the changes
4
+ # upstream - https://github.com/rails/rails/pull/42312/commits/a031a43d969c87542c4ee8d0d338d55fcbb53376
5
+ module ActiveRecordBatchEnumerator
6
+ # The primary key value from which the BatchEnumerator starts,
7
+ # inclusive of the value.
8
+ attr_reader :start
9
+
10
+ # The primary key value at which the BatchEnumerator ends,
11
+ # inclusive of the value.
12
+ attr_reader :finish
13
+
14
+ # The relation from which the BatchEnumerator yields batches.
15
+ attr_reader :relation
16
+
17
+ # The size of the batches yielded by the BatchEnumerator.
18
+ def batch_size
19
+ @of
20
+ end
21
+ end
22
+
23
+ ActiveRecord::Batches::BatchEnumerator.include(ActiveRecordBatchEnumerator)