From 6c29ce47c87bcd9cc248e8358aab89a51d0bd380 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 17 Dec 2023 18:57:20 +0100 Subject: [PATCH] Document and test a pattern for app-wide methods (#7) * Document and test a pattern for app-wide methods You can do this on `ApplicationRecord`: ```ruby class ApplicationRecord < ActiveRecord::Base self.abstract_class = true # We're passing specific queues for monitoring, but you may not need or want them. performs :touch, queue_as: "active_record.touch" performs :update, queue_as: "active_record.update" performs :destroy, queue_as: "active_record.destroy" end ``` * Always UTC time so CI is happy? * Gotta love times on CI --- README.md | 32 +++++++++++++++++ .../active_job/active_record/test_performs.rb | 36 +++++++++++++++++++ test/test_helper.rb | 10 +++++- 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3427d04..2db24f2 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,38 @@ class Post < ActiveRecord::Base end ``` +#### Establishing patterns across your app + +If there's an Active Record method that you'd like any model to be able to run from a background job, you can set them up in your `ApplicationRecord`: + +```ruby +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + + # We're passing specific queues for monitoring, but you may not need or want them. + performs :touch, queue_as: "active_record.touch" + performs :update, queue_as: "active_record.update" + performs :destroy, queue_as: "active_record.destroy" +end +``` + +Then a model could now run things like: + +```ruby +record.touch_later +record.touch_later :reminded_at, time: 5.minutes.from_now # Pass supported arguments to `touch` + +record.update_later reminded_at: 1.year.ago + +# Particularly handy to use on a record with many `dependent: :destroy` associations. +# Plus if anything fails, the transaction will rollback and the job fails, so you can retry it later! +record.destroy_later +``` + +You may not want this for `touch` and `update`, and maybe you'd rather architect your system in such a way that they don't have so many side-effects, but having the option can be handy! + +Also, I haven't tested all the Active Record methods, so please file an issue if you encounter any. + #### Method suffixes `ActiveJob::Performs` supports Ruby's stylistic method suffixes, i.e. ? and ! respectively. diff --git a/test/active_job/active_record/test_performs.rb b/test/active_job/active_record/test_performs.rb index 0c6e6fb..7db5d1e 100644 --- a/test/active_job/active_record/test_performs.rb +++ b/test/active_job/active_record/test_performs.rb @@ -2,6 +2,42 @@ module ActiveJob::ActiveRecord; end class ActiveJob::ActiveRecord::TestPerforms < ActiveSupport::TestCase + setup { @invoice = Invoice.create! } + + test "touch_later" do + assert_changes -> { @invoice.reload.updated_at } do + assert_performed_with job: ApplicationRecord::TouchJob, args: [@invoice] do + @invoice.touch_later + end + end + + time = Time.now.utc.change(usec: 0) + assert_changes -> { @invoice.reload.reminded_at }, to: time do + assert_performed_with job: ApplicationRecord::TouchJob, args: [@invoice, :reminded_at, time: time] do + @invoice.touch_later :reminded_at, time: time + end + end + end + + test "update_later" do + time = Time.now.utc.change(usec: 0) + assert_changes -> { @invoice.reload.reminded_at }, to: time do + assert_performed_with job: ApplicationRecord::UpdateJob, args: [@invoice, reminded_at: time] do + @invoice.update_later reminded_at: time + end + end + end + + test "destroy_later" do + assert_enqueued_with job: ApplicationRecord::DestroyJob, args: [@invoice] do + @invoice.destroy_later + end + perform_enqueued_jobs + assert_raise(ActiveRecord::RecordNotFound) { @invoice.reload } + end +end + +class ActiveJob::ActiveRecord::TestPerformsBulk < ActiveSupport::TestCase setup do Invoice.insert_all [{}, {}, {}, {}, {}] end diff --git a/test/test_helper.rb b/test/test_helper.rb index 845f300..3e5f152 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -23,9 +23,17 @@ class ApplicationJob < ActiveJob::Base; end end end -class Invoice < ActiveRecord::Base +class ApplicationRecord < ActiveRecord::Base include GlobalID::Identification + self.abstract_class = true + + performs :touch, queue_as: "active_record.touch" + performs :update, queue_as: "active_record.update" + performs :destroy, queue_as: "active_record.destroy" +end + +class Invoice < ApplicationRecord performs :deliver_reminder! def deliver_reminder! touch :reminded_at