maintenance_tasks 1.4.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +229 -41
  3. data/app/controllers/maintenance_tasks/tasks_controller.rb +2 -1
  4. data/app/helpers/maintenance_tasks/application_helper.rb +1 -0
  5. data/app/helpers/maintenance_tasks/tasks_helper.rb +19 -0
  6. data/app/jobs/concerns/maintenance_tasks/task_job_concern.rb +28 -21
  7. data/app/models/maintenance_tasks/application_record.rb +1 -0
  8. data/app/models/maintenance_tasks/csv_collection_builder.rb +38 -0
  9. data/app/models/maintenance_tasks/no_collection_builder.rb +29 -0
  10. data/app/models/maintenance_tasks/null_collection_builder.rb +38 -0
  11. data/app/models/maintenance_tasks/progress.rb +8 -3
  12. data/app/models/maintenance_tasks/run.rb +157 -13
  13. data/app/models/maintenance_tasks/runner.rb +22 -9
  14. data/app/models/maintenance_tasks/runs_page.rb +1 -0
  15. data/app/models/maintenance_tasks/task.rb +236 -0
  16. data/app/models/maintenance_tasks/task_data.rb +15 -3
  17. data/app/validators/maintenance_tasks/run_status_validator.rb +2 -2
  18. data/app/views/maintenance_tasks/runs/_arguments.html.erb +22 -0
  19. data/app/views/maintenance_tasks/runs/_csv.html.erb +5 -0
  20. data/app/views/maintenance_tasks/runs/_run.html.erb +18 -1
  21. data/app/views/maintenance_tasks/runs/info/_custom.html.erb +0 -0
  22. data/app/views/maintenance_tasks/runs/info/_errored.html.erb +0 -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/show.html.erb +32 -7
  27. data/config/routes.rb +1 -0
  28. data/db/migrate/20201211151756_create_maintenance_tasks_runs.rb +1 -0
  29. data/db/migrate/20210225152418_remove_index_on_task_name.rb +1 -0
  30. data/db/migrate/20210517131953_add_arguments_to_maintenance_tasks_runs.rb +1 -0
  31. data/db/migrate/20211210152329_add_lock_version_to_maintenance_tasks_runs.rb +8 -0
  32. data/lib/generators/maintenance_tasks/install_generator.rb +1 -0
  33. data/lib/generators/maintenance_tasks/task_generator.rb +13 -0
  34. data/lib/generators/maintenance_tasks/templates/no_collection_task.rb.tt +13 -0
  35. data/lib/generators/maintenance_tasks/templates/no_collection_task_test.rb.tt +12 -0
  36. data/lib/generators/maintenance_tasks/templates/task.rb.tt +3 -1
  37. data/lib/generators/maintenance_tasks/templates/task_test.rb.tt +4 -0
  38. data/lib/maintenance_tasks/cli.rb +6 -5
  39. data/lib/maintenance_tasks/engine.rb +15 -1
  40. data/lib/maintenance_tasks.rb +12 -1
  41. metadata +15 -7
  42. data/app/models/maintenance_tasks/csv_collection.rb +0 -33
  43. data/app/tasks/maintenance_tasks/task.rb +0 -137
  44. data/app/views/maintenance_tasks/runs/_info.html.erb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c3c4c94cf355432e0d558e68bfc3f275972164c8ed932d2505b7c6450f0d124
4
- data.tar.gz: 1b80ad4b58b9546d5ab2af77d2d0e64f8bd8b72dc6641ff445d33a67dae03860
3
+ metadata.gz: e66a4f3f2755a31f9dac0125d41d63d5b1b8ca28a4f11642c8648d94dbc2a044
4
+ data.tar.gz: 1cf5d80050866d6e78d4cf392c9ff888953a3d15da8c19f83f511aaaf94640d2
5
5
  SHA512:
6
- metadata.gz: 7402e24bfbc370c6af54b441d2b1050f252ee44f51ab11612875fed7fa6822645420422dc33405b1665596e66f85f8fe1f8b1e443d023d451a626913afe6dc98
7
- data.tar.gz: 8f4666aba4abc6f01d77b238de6b6d511d24a5d4932f5ec2499f0e91faefe2e8a3b18435109864c821495b2efde464dd3212a76eae5276b4e622eb1cb6f7da3c
6
+ metadata.gz: ffc019d73a707936a483349862481e5a227e270be3578f859b7d385fa7d997d327082a6f090881dcb83306feaf496d36f03d75421533ac507d7472adc5cad991
7
+ data.tar.gz: ae2ca92ea5b989e11d6ffaa50bd6ce1e5fcd41f735f81e6261ae496b81de2b90e7da2f58fdc04803dc77d44f6c0a8814a86794413f2e7ce374aa557c7e0073fb
data/README.md CHANGED
@@ -57,6 +57,7 @@ Example:
57
57
 
58
58
  ```ruby
59
59
  # app/tasks/maintenance/update_posts_task.rb
60
+
60
61
  module Maintenance
61
62
  class UpdatePostsTask < MaintenanceTasks::Task
62
63
  def collection
@@ -68,7 +69,7 @@ module Maintenance
68
69
  end
69
70
 
70
71
  def process(post)
71
- post.update!(content: 'New content!')
72
+ post.update!(content: "New content!")
72
73
  end
73
74
  end
74
75
  end
@@ -78,8 +79,8 @@ end
78
79
 
79
80
  You can also write a Task that iterates on a CSV file. Note that writing CSV
80
81
  Tasks **requires Active Storage to be configured**. Ensure that the dependency
81
- is specified in your application's Gemfile, and that you've followed the
82
- [setup instuctions][setup].
82
+ is specified in your application's Gemfile, and that you've followed the [setup
83
+ instuctions][setup].
83
84
 
84
85
  [setup]: https://edgeguides.rubyonrails.org/active_storage_overview.html#setup
85
86
 
@@ -95,6 +96,7 @@ The generated task is a subclass of `MaintenanceTasks::Task` that implements:
95
96
 
96
97
  ```ruby
97
98
  # app/tasks/maintenance/import_posts_task.rb
99
+
98
100
  module Maintenance
99
101
  class ImportPostsTask < MaintenanceTasks::Task
100
102
  csv_collection
@@ -112,16 +114,21 @@ title,content
112
114
  My Title,Hello World!
113
115
  ```
114
116
 
117
+ The files uploaded to your Active Storage service provider will be renamed
118
+ to include an ISO8601 timestamp and the Task name in snake case format.
119
+ The CSV is expected to have a trailing newline at the end of the file.
120
+
115
121
  ### Processing Batch Collections
116
122
 
117
123
  The Maintenance Tasks gem supports processing Active Records in batches. This
118
124
  can reduce the number of calls your Task makes to the database. Use
119
- `ActiveRecord::Batches#in_batches` on the relation returned by your collection to specify that your Task should process
120
- batches instead of records. Active Record defaults to 1000 records by batch, but a custom size can be
121
- specified.
125
+ `ActiveRecord::Batches#in_batches` on the relation returned by your collection
126
+ to specify that your Task should process batches instead of records. Active
127
+ Record defaults to 1000 records by batch, but a custom size can be specified.
122
128
 
123
129
  ```ruby
124
130
  # app/tasks/maintenance/update_posts_in_batches_task.rb
131
+
125
132
  module Maintenance
126
133
  class UpdatePostsInBatchesTask < MaintenanceTasks::Task
127
134
  def collection
@@ -147,10 +154,41 @@ your collection, and your Task's progress will be displayed in terms of batches
147
154
  **Important!** Batches should only be used if `#process` is performing a batch
148
155
  operation such as `#update_all` or `#delete_all`. If you need to iterate over
149
156
  individual records, you should define a collection that [returns an
150
- `ActiveRecord::Relation`](#creating-a-task). This uses batching
151
- internally, but loads the records with one SQL query. Conversely, batch
152
- collections load the primary keys of the records of the batch first, and then perform an additional query to load the
153
- records when calling `each` (or any `Enumerable` method) inside `#process`.
157
+ `ActiveRecord::Relation`](#creating-a-task). This uses batching internally, but
158
+ loads the records with one SQL query. Conversely, batch collections load the
159
+ primary keys of the records of the batch first, and then perform an additional
160
+ query to load the records when calling `each` (or any `Enumerable` method)
161
+ inside `#process`.
162
+
163
+ ### Tasks that don't need a Collection
164
+
165
+ Sometimes, you might want to run a Task that performs a single operation, such
166
+ as enqueuing another background job or hitting an external API. The gem supports
167
+ collection-less tasks.
168
+
169
+ Generate a collection-less Task by running:
170
+
171
+ ```bash
172
+ $ bin/rails generate maintenance_tasks:task no_collection_task --no-collection
173
+ ```
174
+
175
+ The generated task is a subclass of `MaintenanceTasks::Task` that implements:
176
+
177
+ * `process`: do the work of your maintenance task
178
+
179
+ ```ruby
180
+ # app/tasks/maintenance/no_collection_task.rb
181
+
182
+ module Maintenance
183
+ class NoCollectionTask < MaintenanceTasks::Task
184
+ no_collection
185
+
186
+ def process
187
+ SomeAsyncJob.perform_later
188
+ end
189
+ end
190
+ end
191
+ ```
154
192
 
155
193
  ### Throttling
156
194
 
@@ -162,6 +200,7 @@ Specify the throttle condition as a block:
162
200
 
163
201
  ```ruby
164
202
  # app/tasks/maintenance/update_posts_throttled_task.rb
203
+
165
204
  module Maintenance
166
205
  class UpdatePostsThrottledTask < MaintenanceTasks::Task
167
206
  throttle_on(backoff: 1.minute) do
@@ -192,15 +231,29 @@ Tasks can define multiple throttle conditions. Throttle conditions are inherited
192
231
  by descendants, and new conditions will be appended without impacting existing
193
232
  conditions.
194
233
 
234
+ The backoff can also be specified as a proc:
235
+
236
+ ```ruby
237
+ # app/tasks/maintenance/update_posts_throttled_task.rb
238
+
239
+ module Maintenance
240
+ class UpdatePostsThrottledTask < MaintenanceTasks::Task
241
+ throttle_on(backoff: -> { RandomBackoffGenerator.generate_duration } ) do
242
+ DatabaseStatus.unhealthy?
243
+ end
244
+ ...
245
+ end
246
+ end
247
+ ```
195
248
  ### Custom Task Parameters
196
249
 
197
250
  Tasks may need additional information, supplied via parameters, to run.
198
- Parameters can be defined as Active Model Attributes in a Task, and then
199
- become accessible to any of Task's methods: `#collection`, `#count`, or
200
- `#process`.
251
+ Parameters can be defined as Active Model Attributes in a Task, and then become
252
+ accessible to any of Task's methods: `#collection`, `#count`, or `#process`.
201
253
 
202
254
  ```ruby
203
255
  # app/tasks/maintenance/update_posts_via_params_task.rb
256
+
204
257
  module Maintenance
205
258
  class UpdatePostsViaParamsTask < MaintenanceTasks::Task
206
259
  attribute :updated_content, :string
@@ -227,6 +280,73 @@ to run. Since arguments are specified in the user interface via text area
227
280
  inputs, it's important to check that they conform to the format your Task
228
281
  expects, and to sanitize any inputs if necessary.
229
282
 
283
+ ### Using Task Callbacks
284
+
285
+ The Task provides callbacks that hook into its life cycle.
286
+
287
+ Available callbacks are:
288
+
289
+ * `after_start`
290
+ * `after_pause`
291
+ * `after_interrupt`
292
+ * `after_cancel`
293
+ * `after_complete`
294
+ * `after_error`
295
+
296
+ ```ruby
297
+ module Maintenance
298
+ class UpdatePostsTask < MaintenanceTasks::Task
299
+ after_start :notify
300
+
301
+ def notify
302
+ NotifyJob.perform_later(self.class.name)
303
+ end
304
+
305
+ # ...
306
+ end
307
+ end
308
+ ```
309
+
310
+ Note: The `after_error` callback is guaranteed to complete,
311
+ so any exceptions raised in your callback code are ignored.
312
+ If your `after_error` callback code can raise an exception,
313
+ you'll need to rescue it and handle it appropriately
314
+ within the callback.
315
+
316
+ ```ruby
317
+ module Maintenance
318
+ class UpdatePostsTask < MaintenanceTasks::Task
319
+ after_error :dangerous_notify
320
+
321
+ def dangerous_notify
322
+ # This error is rescued in favour of the original error causing the error flow.
323
+ raise NotDeliveredError
324
+ end
325
+
326
+ # ...
327
+ end
328
+ end
329
+ ```
330
+
331
+ If any of the other callbacks cause an exception,
332
+ it will be handled by the error handler,
333
+ and will cause the task to stop running.
334
+
335
+ Callback behaviour can be shared across all tasks using an initializer.
336
+
337
+ ```ruby
338
+ # config/initializer/maintenance_tasks.rb
339
+ Rails.autoloaders.main.on_load("MaintenanceTasks::Task") do
340
+ MaintenanceTasks::Task.class_eval do
341
+ after_start(:notify)
342
+
343
+ private
344
+
345
+ def notify; end
346
+ end
347
+ end
348
+ ```
349
+
230
350
  ### Considerations when writing Tasks
231
351
 
232
352
  MaintenanceTasks relies on the queue adapter configured for your application to
@@ -259,7 +379,7 @@ Example:
259
379
  ```ruby
260
380
  # test/tasks/maintenance/update_posts_task_test.rb
261
381
 
262
- require 'test_helper'
382
+ require "test_helper"
263
383
 
264
384
  module Maintenance
265
385
  class UpdatePostsTaskTest < ActiveSupport::TestCase
@@ -268,7 +388,7 @@ module Maintenance
268
388
 
269
389
  Maintenance::UpdatePostsTask.process(post)
270
390
 
271
- assert_equal 'New content!', post.content
391
+ assert_equal "New content!", post.content
272
392
  end
273
393
  end
274
394
  end
@@ -281,20 +401,50 @@ takes a `CSV::Row` as an argument. You can pass a row, or a hash with string
281
401
  keys to `#process` from your test.
282
402
 
283
403
  ```ruby
284
- # app/tasks/maintenance/import_posts_task_test.rb
404
+ # test/tasks/maintenance/import_posts_task_test.rb
405
+
406
+ require "test_helper"
407
+
285
408
  module Maintenance
286
409
  class ImportPostsTaskTest < ActiveSupport::TestCase
287
410
  test "#process performs a task iteration" do
288
411
  assert_difference -> { Post.count } do
289
412
  Maintenance::UpdatePostsTask.process({
290
- 'title' => 'My Title',
291
- 'content' => 'Hello World!',
413
+ "title" => "My Title",
414
+ "content" => "Hello World!",
292
415
  })
293
416
  end
294
417
 
295
418
  post = Post.last
296
- assert_equal 'My Title', post.title
297
- assert_equal 'Hello World!', post.content
419
+ assert_equal "My Title", post.title
420
+ assert_equal "Hello World!", post.content
421
+ end
422
+ end
423
+ end
424
+ ```
425
+
426
+ ### Writing tests for a Task with parameters
427
+
428
+ Tests for tasks with parameters need to instatiate the task class in order to
429
+ assign attributes. Once the task instance is setup, you may test `#process`
430
+ normally.
431
+
432
+ ```ruby
433
+ # test/tasks/maintenance/update_posts_via_params_task_test.rb
434
+
435
+ require "test_helper"
436
+
437
+ module Maintenance
438
+ class UpdatePostsViaParamsTaskTest < ActiveSupport::TestCase
439
+ setup do
440
+ @task = UpdatePostsViaParamsTask.new
441
+ @task.updated_content = "Testing"
442
+ end
443
+
444
+ test "#process performs a task iteration" do
445
+ assert_difference -> { Post.first.content } do
446
+ task.process(Post.first)
447
+ end
298
448
  end
299
449
  end
300
450
  end
@@ -313,20 +463,21 @@ $ bundle exec maintenance_tasks perform Maintenance::UpdatePostsTask
313
463
  To run a Task that processes CSVs from the command line, use the --csv option:
314
464
 
315
465
  ```bash
316
- $ bundle exec maintenance_tasks perform Maintenance::ImportPostsTask --csv 'path/to/my_csv.csv'
466
+ $ bundle exec maintenance_tasks perform Maintenance::ImportPostsTask --csv "path/to/my_csv.csv"
317
467
  ```
318
468
 
319
469
  To run a Task that takes arguments from the command line, use the --arguments
320
- option, passing arguments as a set of <key>:<value> pairs:
470
+ option, passing arguments as a set of \<key>:\<value> pairs:
321
471
 
322
472
  ```bash
323
- $ bundle exec maintenance_tasks perform Maintenance::ParamsTask --arguments post_ids:1,2,3 content:"Hello, World!"
473
+ $ bundle exec maintenance_tasks perform Maintenance::ParamsTask \
474
+ --arguments post_ids:1,2,3 content:"Hello, World!"
324
475
  ```
325
476
 
326
477
  You can also run a Task in Ruby by sending `run` with a Task name to Runner:
327
478
 
328
479
  ```ruby
329
- MaintenanceTasks::Runner.run(name: 'Maintenance::UpdatePostsTask')
480
+ MaintenanceTasks::Runner.run(name: "Maintenance::UpdatePostsTask")
330
481
  ```
331
482
 
332
483
  To run a Task that processes CSVs using the Runner, provide a Hash containing an
@@ -334,8 +485,8 @@ open IO object and a filename to `run`:
334
485
 
335
486
  ```ruby
336
487
  MaintenanceTasks::Runner.run(
337
- name: 'Maintenance::ImportPostsTask'
338
- csv_file: { io: File.open('path/to/my_csv.csv'), filename: 'my_csv.csv' }
488
+ name: "Maintenance::ImportPostsTask",
489
+ csv_file: { io: File.open("path/to/my_csv.csv"), filename: "my_csv.csv" }
339
490
  )
340
491
  ```
341
492
 
@@ -358,8 +509,8 @@ a Task can be in:
358
509
  * **enqueued**: A Task that is waiting to be performed after a user has
359
510
  instructed it to run.
360
511
  * **running**: A Task that is currently being performed by a job worker.
361
- * **pausing**: A Task that was paused by a user, but needs to finish work
362
- before stopping.
512
+ * **pausing**: A Task that was paused by a user, but needs to finish work before
513
+ stopping.
363
514
  * **paused**: A Task that was paused by a user and is not performing. It can be
364
515
  resumed.
365
516
  * **interrupted**: A Task that has been momentarily interrupted by the job
@@ -433,9 +584,10 @@ you can define an error handler:
433
584
 
434
585
  ```ruby
435
586
  # config/initializers/maintenance_tasks.rb
587
+
436
588
  MaintenanceTasks.error_handler = ->(error, task_context, _errored_element) do
437
589
  Bugsnag.notify(error) do |notification|
438
- notification.add_tab(:task, task_context)
590
+ notification.add_metadata(:task, task_context)
439
591
  end
440
592
  end
441
593
  ```
@@ -448,18 +600,18 @@ The error handler should be a lambda that accepts three arguments:
448
600
  * `task_name`: The name of the Task that errored
449
601
  * `started_at`: The time the Task started
450
602
  * `ended_at`: The time the Task errored
603
+
451
604
  Note that `task_context` may be empty if the Task produced an error before any
452
605
  context could be gathered (for example, if deserializing the job to process
453
606
  your Task failed).
454
- * `errored_element`: The element, if any, that was being processed when the
455
- Task raised an exception. If you would like to pass this object to your
456
- exception monitoring service, make sure you **sanitize the object** to avoid
457
- leaking sensitive data and **convert it to a format** that is compatible with
458
- your bug tracker. For example, Bugsnag only sends the id and class name of
459
- Active Record objects in order to protect sensitive data. CSV rows, on the
460
- other hand, are converted to strings and passed raw to Bugsnag, so make sure
461
- to filter any personal data from these objects before adding them to a
462
- report.
607
+ * `errored_element`: The element, if any, that was being processed when the Task
608
+ raised an exception. If you would like to pass this object to your exception
609
+ monitoring service, make sure you **sanitize the object** to avoid leaking
610
+ sensitive data and **convert it to a format** that is compatible with your bug
611
+ tracker. For example, Bugsnag only sends the id and class name of Active
612
+ Record objects in order to protect sensitive data. CSV rows, on the other
613
+ hand, are converted to strings and passed raw to Bugsnag, so make sure to
614
+ filter any personal data from these objects before adding them to a report.
463
615
 
464
616
  #### Customizing the maintenance tasks module
465
617
 
@@ -468,7 +620,8 @@ tasks will be placed.
468
620
 
469
621
  ```ruby
470
622
  # config/initializers/maintenance_tasks.rb
471
- MaintenanceTasks.tasks_module = 'TaskModule'
623
+
624
+ MaintenanceTasks.tasks_module = "TaskModule"
472
625
  ```
473
626
 
474
627
  If no value is specified, it will default to `Maintenance`.
@@ -481,9 +634,11 @@ maintenance tasks in your application.
481
634
 
482
635
  ```ruby
483
636
  # config/initializers/maintenance_tasks.rb
637
+
484
638
  MaintenanceTasks.job = 'CustomTaskJob'
485
639
 
486
640
  # app/jobs/custom_task_job.rb
641
+
487
642
  class CustomTaskJob < MaintenanceTasks::TaskJob
488
643
  queue_as :low_priority
489
644
  end
@@ -491,8 +646,8 @@ end
491
646
 
492
647
  The Job class **must inherit** from `MaintenanceTasks::TaskJob`.
493
648
 
494
- Note that `retry_on` is not supported for custom Job
495
- classes, so failed jobs cannot be retried.
649
+ Note that `retry_on` is not supported for custom Job classes, so failed jobs
650
+ cannot be retried.
496
651
 
497
652
  #### Customizing the rate at which task progress gets updated
498
653
 
@@ -502,6 +657,7 @@ task progress gets persisted to the database. It can be a `Numeric` value or an
502
657
 
503
658
  ```ruby
504
659
  # config/initializers/maintenance_tasks.rb
660
+
505
661
  MaintenanceTasks.ticker_delay = 2.seconds
506
662
  ```
507
663
 
@@ -516,6 +672,7 @@ key, as specified in your application's `config/storage.yml`:
516
672
 
517
673
  ```yaml
518
674
  # config/storage.yml
675
+
519
676
  user_data:
520
677
  service: GCS
521
678
  credentials: <%= Rails.root.join("path/to/user/data/keyfile.json") %>
@@ -531,12 +688,31 @@ internal:
531
688
 
532
689
  ```ruby
533
690
  # config/initializers/maintenance_tasks.rb
691
+
534
692
  MaintenanceTasks.active_storage_service = :internal
535
693
  ```
536
694
 
537
695
  There is no need to configure this option if your application uses only one
538
696
  storage service per environment.
539
697
 
698
+ #### Customizing the backtrace cleaner
699
+
700
+ `MaintenanceTasks.backtrace_cleaner` can be configured to specify a backtrace
701
+ cleaner to use when a Task errors and the backtrace is cleaned and persisted.
702
+ An `ActiveSupport::BacktraceCleaner` should be used.
703
+
704
+ ```ruby
705
+ # config/initializers/maintenance_tasks.rb
706
+
707
+ cleaner = ActiveSupport::BacktraceCleaner.new
708
+ cleaner.add_silencer { |line| line =~ /ignore_this_dir/ }
709
+
710
+ MaintenanceTasks.backtrace_cleaner = cleaner
711
+ ```
712
+
713
+ If none is specified, the default `Rails.backtrace_cleaner` will be used to
714
+ clean backtraces.
715
+
540
716
  ## Upgrading
541
717
 
542
718
  Use bundler to check for and upgrade to newer versions. After installing a new
@@ -548,6 +724,17 @@ $ bin/rails generate maintenance_tasks:install
548
724
 
549
725
  This ensures that new migrations are installed and run as well.
550
726
 
727
+ **What if I've deleted my previous Maintenance Task migrations?**
728
+
729
+ The install command will attempt to reinstall these old migrations and migrating
730
+ the database will cause problems. Use `bin/rails
731
+ maintenance_tasks:install:migrations` to copy the gem's migrations to your
732
+ `db/migrate` folder. Check the release notes to see if any new migrations were
733
+ added since your last gem upgrade. Ensure that these are kept, but remove any
734
+ migrations that already ran.
735
+
736
+ Run the migrations using `bin/rails db:migrate`.
737
+
551
738
  ## Contributing
552
739
 
553
740
  Would you like to report an issue or contribute with code? We accept issues and
@@ -564,6 +751,7 @@ are merged.
564
751
  Once a release is ready, follow these steps:
565
752
 
566
753
  * Update `spec.version` in `maintenance_tasks.gemspec`.
754
+ * Run `bundle install` to bump the `Gemfile.lock` version of the gem.
567
755
  * Open a PR and merge on approval.
568
756
  * Deploy via [Shipit][shipit] and see the new version on
569
757
  <https://rubygems.org/gems/maintenance_tasks>.
@@ -24,11 +24,12 @@ module MaintenanceTasks
24
24
  end
25
25
 
26
26
  # Runs a given Task and redirects to the Task page.
27
- def run
27
+ def run(&block)
28
28
  task = Runner.run(
29
29
  name: params.fetch(:id),
30
30
  csv_file: params[:csv_file],
31
31
  arguments: params.fetch(:task_arguments, {}).permit!.to_h,
32
+ &block
32
33
  )
33
34
  redirect_to(task_path(task))
34
35
  rescue ActiveRecord::RecordInvalid => error
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module MaintenanceTasks
3
4
  # Module for common view helpers.
4
5
  #
@@ -100,5 +100,24 @@ module MaintenanceTasks
100
100
  only_path: true
101
101
  )
102
102
  end
103
+
104
+ # Return the appropriate field tag for the parameter
105
+ def parameter_field(form_builder, parameter_name)
106
+ case form_builder.object.class.attribute_types[parameter_name]
107
+ when ActiveModel::Type::Integer, ActiveModel::Type::Decimal,
108
+ ActiveModel::Type::Float
109
+ form_builder.number_field(parameter_name)
110
+ when ActiveModel::Type::DateTime
111
+ form_builder.datetime_field(parameter_name)
112
+ when ActiveModel::Type::Date
113
+ form_builder.date_field(parameter_name)
114
+ when ActiveModel::Type::Time
115
+ form_builder.time_field(parameter_name)
116
+ when ActiveModel::Type::Boolean
117
+ form_builder.check_box(parameter_name)
118
+ else
119
+ form_builder.text_area(parameter_name, class: "textarea")
120
+ end
121
+ end
103
122
  end
104
123
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module MaintenanceTasks
3
4
  # Concern that holds the behaviour of the job that runs the tasks. It is
4
5
  # included in {TaskJob} and if MaintenanceTasks.job is overridden, it must be
@@ -11,8 +12,8 @@ module MaintenanceTasks
11
12
  before_perform(:before_perform)
12
13
 
13
14
  on_start(:on_start)
14
- on_complete(:on_complete)
15
15
  on_shutdown(:on_shutdown)
16
+ on_complete(:on_complete)
16
17
 
17
18
  after_perform(:after_perform)
18
19
 
@@ -35,6 +36,8 @@ module MaintenanceTasks
35
36
  @enumerator = nil
36
37
 
37
38
  collection_enum = case collection
39
+ when :no_collection
40
+ enumerator_builder.build_once_enumerator(cursor: nil)
38
41
  when ActiveRecord::Relation
39
42
  enumerator_builder.active_record_on_records(collection, cursor: cursor)
40
43
  when ActiveRecord::Batches::BatchEnumerator
@@ -62,9 +65,16 @@ module MaintenanceTasks
62
65
  Array, or CSV.
63
66
  MSG
64
67
  end
68
+ throttle_enumerator(collection_enum)
69
+ end
65
70
 
71
+ def throttle_enumerator(collection_enum)
66
72
  @task.throttle_conditions.reduce(collection_enum) do |enum, condition|
67
- enumerator_builder.build_throttle_enumerator(enum, **condition)
73
+ enumerator_builder.build_throttle_enumerator(
74
+ enum,
75
+ throttle_on: condition[:throttle_on],
76
+ backoff: condition[:backoff].call
77
+ )
68
78
  end
69
79
  end
70
80
 
@@ -81,7 +91,11 @@ module MaintenanceTasks
81
91
  end
82
92
 
83
93
  def task_iteration(input)
84
- @task.process(input)
94
+ if @task.no_collection?
95
+ @task.process
96
+ else
97
+ @task.process(input)
98
+ end
85
99
  rescue => error
86
100
  @errored_element = input
87
101
  raise error
@@ -90,12 +104,11 @@ module MaintenanceTasks
90
104
  def before_perform
91
105
  @run = arguments.first
92
106
  @task = @run.task
93
- if @task.respond_to?(:csv_content=)
107
+ if @task.has_csv_content?
94
108
  @task.csv_content = @run.csv_file.download
95
109
  end
96
- @run.job_id = job_id
97
110
 
98
- @run.running! unless @run.stopping?
111
+ @run.running
99
112
 
100
113
  @ticker = Ticker.new(MaintenanceTasks.ticker_delay) do |ticks, duration|
101
114
  @run.persist_progress(ticks, duration)
@@ -105,26 +118,19 @@ module MaintenanceTasks
105
118
  def on_start
106
119
  count = @task.count
107
120
  count = @enumerator&.size if count == :no_count
108
- @run.update!(started_at: Time.now, tick_total: count)
109
- end
110
-
111
- def on_complete
112
- @run.status = :succeeded
113
- @run.ended_at = Time.now
121
+ @run.start(count)
114
122
  end
115
123
 
116
124
  def on_shutdown
117
- if @run.cancelling?
118
- @run.status = :cancelled
119
- @run.ended_at = Time.now
120
- else
121
- @run.status = @run.pausing? ? :paused : :interrupted
122
- @run.cursor = cursor_position
123
- end
124
-
125
+ @run.job_shutdown
126
+ @run.cursor = cursor_position
125
127
  @ticker.persist
126
128
  end
127
129
 
130
+ def on_complete
131
+ @run.complete
132
+ end
133
+
128
134
  # We are reopening a private part of Job Iteration's API here, so we should
129
135
  # ensure the method is still defined upstream. This way, in the case where
130
136
  # the method changes upstream, we catch it at load time instead of at
@@ -143,7 +149,7 @@ module MaintenanceTasks
143
149
  end
144
150
 
145
151
  def after_perform
146
- @run.save!
152
+ @run.persist_transition
147
153
  if defined?(@reenqueue_iteration_job) && @reenqueue_iteration_job
148
154
  reenqueue_iteration_job(should_ignore: false)
149
155
  end
@@ -164,6 +170,7 @@ module MaintenanceTasks
164
170
  task_context = {}
165
171
  end
166
172
  errored_element = @errored_element if defined?(@errored_element)
173
+ ensure
167
174
  MaintenanceTasks.error_handler.call(error, task_context, errored_element)
168
175
  end
169
176
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module MaintenanceTasks
3
4
  # Base class for all records used by this engine.
4
5
  #