Search code examples
ruby-on-railsrubyruby-on-rails-5jobs

How to create idempotent jobs on specific period of day range of month?


I have a lot of jobs in my app which have the following conditions:

  • Need to perform an action once, and only once, per month
  • Need to perform the action on a specific day (example: 10 days before end of month)
  • The job will be re-run many times a day, so they must be idempotent and can be re-run many times and only affect specific records, until the next month in which case all records can be effected again

Think of a payment reminder that comes 5 days before the end of the month via SMS.

I can think of a LOT of ways to do this, but honestly every way I try ends up being super confusing with the different time subtractions, .where() clauses, etc.

I'm really looking for a known design pattern or some bulletproof logic that isn't a confusing spaghetti mess of time subtractions, greater thans, less thans, and is confusing to look at (and prone to bugs)

This was my best attempt. Please feel free to help me with something from scratch though... it doesnt have to modify this. I feel like this is way more complicated than it needs to be.

class SMSPaymentReminderJob < Que::Job

  @priority = 50

  def run
    current = Time.now.in_time_zone('America/New_York')
    window_end = current.end_of_month
    window_start = window_end.beginning_of_day - 2.days
    cutoff = current.beginning_of_month.utc # convert to UTC for PG

    return unless (window_start..window_end).cover? current

    User.where(sms_enabled: true)
      .where('"sms_reminder_sent_at" < ? OR "sms_reminder_sent_at" IS NULL', cutoff)
      .find_in_batches(batch_size: 30) do |users|

      # ... code here ...
    end
  end

end

Solution

  • A combination of IceCube (to handle recurring events) and maybe RecurringSelect for a GUI would achieve this with ease-of-use in mind and allow for expansion.

    You can then set up your events such as:

    IceCube::Rule.monthly
    IceCube::Rule.monthly.day_of_month(-10)