Class: RuboCop::Cop::DevDoc::Rails::NoPerformLaterInModel

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/dev_doc/rails/no_perform_later_in_model.rb

Overview

Avoid perform_later/deliver_later calls inside model files.

Rationale

Enqueuing a job or mailer is execution-flow control — deciding when a side effect fires — and its home is the calling controller or job entry point (see the orchestration best-practice doc), because:

  • It is prone to conflicts between transaction and perform_later (the job may run before the transaction commits and read stale data — see DevDoc/Rails/NoDeliverLaterInTransaction).
  • Flow control belongs to the Controller, not the Model. This governs the trigger only: the mail itself stays composed in the Mailer (parameters, templates — see the controller best-practice doc); the controller merely calls deliver_later. Reuse across controllers goes in a controller concern — DRY alone never justifies pushing a trigger into a model.
  • The exception: an enqueue inseparable from a record state change (e.g. a throttle stamp and its send that must never be split across a boundary) stays in the model behind an explicitly named notify_*/send_* method, so the side effect is obvious at the call site.

The boundary test: is the enqueue bound to a state change on this record, or is ordering already enforced by data flow (the caller can only enqueue with values that exist after the domain call returns)? The latter is flow control — trigger it from the controller.

❌  (in app/models/order.rb)
def finalize
save!
OrderJob.perform_later(self)
end

✔️ State-coupled enqueue behind an explicit `notify_*` name; the
side effect is obvious at the call site, and reviewers
immediately see that this must not be called inside a
`transaction` block.
def notify_finalized
update!(finalize_notified_at: Time.current)
OrderJob.perform_later(self)
end

NOTE: This cop flags any perform_later in app/models/**/*.rb, including the legitimate "explicitly named method" case above. The cop is intentionally conservative — disable per-line with a rubocop comment stating the justification when you have deliberately followed the naming convention.

Examples:

# bad (in app/models/order.rb)
def finalize
  save!
  OrderJob.perform_later(self)
end

# good — state-coupled enqueue behind an explicitly named method
def notify_finalized
  update!(finalize_notified_at: Time.current)
  OrderJob.perform_later(self)
end

Constant Summary collapse

MSG =
'Avoid `%<method>s` in model files — it enqueues an ActiveJob (mailer deliveries ' \
'included), with the same transaction/stale-data hazard as `perform_later`. Call it ' \
'from the controller, or use an explicit method name to signal the side effect.'.freeze
RESTRICT_ON_SEND =

deliver_later is ActiveJob under the hood (MailDeliveryJob) and enqueue is the primitive perform_later wraps — omitting either would leave an everyday spelling of the same async enqueue.

%i[perform_later perform_all_later deliver_later deliver_later! enqueue].freeze

Instance Method Summary collapse

Instance Method Details

#on_send(node) ⇒ Object



74
75
76
# File 'lib/rubocop/cop/dev_doc/rails/no_perform_later_in_model.rb', line 74

def on_send(node)
  add_offense(node.loc.selector, message: format(MSG, method: node.method_name))
end