periodically
Redis-backed Ruby library for periodically running tasks, whose execution depends can depend on a custom lambda block (e.g. weekly syncs). The job execution won't be guaranteed to happen exactly as the condition is fulfilled, hence periodically is best for nonfrequent and noncritical jobs.
Example usecases:
- Sync a Rails model's data once per week
- Achievable by e.g. a
last_syncedvalue in the database
- Achievable by e.g. a
- Launch a non-important sync operation depending on a specific condition (e.g. NULL value in some database column)
- Achievable by checking the column against NULL inside the
oncondition
- Achievable by checking the column against NULL inside the
Getting started with Rails
Add gem to Gemfile and install
gem 'periodically' && bundle install
Add an initializer (e.g. config/initializers/periodically.rb)
require "periodically"
Periodically.start
In Rails, Periodically jobs are only registered when the class is loaded.
In production mode Rails (by default) eagerly loads all classes, meaning that everything is fine.
However, in development mode you might want to either disable eager mode with config.eager_load = false
Utilize Periodically in e.g. a Model
# app/models/item.rb
class Item < ApplicationRecord
include Periodically::Model
periodically :refresh_price,
on: -> { Item.where("last_synced < ?", 7.days.ago) }
private
def refresh_price
self.price = PriceFetcher.fetch(item_id)
save!
end
end
Execution model
Periodically launches a single background thread, which executes registered queries every x seconds. If a pending query is found, the registered callback method is called in the same thread. Hence, a blocking callback method will also block execution of other pending queries.
By default everything happens in the same process as the main Rails web server.
To parallelize processing a bit, you can do bundle exec periodically to start a new process for jobs. (Remember to remove Periodically.start from initializer!)
API
Terminology
- Job: something enqueued to be called using the
periodicallymethod - Instance job: a single Job execution concerning a specific instance of the class
Inside a Model
Definitions
# Add Periodically context to this class
include Periodically::Model
# Enqueue a Periodically job
periodically :update_method, # call instance method "update_method" for found instances
on: -> { where("last_synced < ?", 7.days.ago) }, # Condition that must be true for update method to be called
min_class_interval: 5.minutes, # (Optional) The minimum interval between calls to this specific class (TODO not implemented)
max_retries: 25, # (Optional) Maximum number of retries. Periodically uses exponential backoff
instance_id: -> { cache_key_with_version }, # (Optional) Returns this instance's unique identifying key. Used for e.g. deferring jobs and marking them as erroring (TODO not implemented)
Update method return values
Job method's return value or raised exception determines further executions of that specific instance job.
# As referred to by a previous `periodically` call
def update_method
# Let's retrieve a normal value from the model instance
status = my_column_status
# No-op
# Since we don't update `last_synced`, this method will get called again without much delay!
return if status == "pending"
# Log error and defer execution
# This unique instance will be deferred for later execution (using exponential backoff) and the error logged
raise "something went wrong" if status == "error"
# Update checked delay
# Updates the property we check against, thus making this instance not pass the Periodically condition
# Note that this line is normal Rails code: Periodically conditions are database/anything-agnostic
update(last_synced: Time.now)
end
The job method's return value can be used to defer further execution of the same model instance, even if it still passes the condition: return Periodically::Defer(60.minutes)
Dashboard
(When implemented :D) Dashboard contains recently succeeded executions, failed executions (with stacktrace) and deferred executions.
Why not Sidekiq?
With Sidekiq you can achieve something almost similar by combining a scheduled job that enqueues further unique jobs based on the condition.
However, there are few advantages Periodically has for the specific usecase of per-instance non-critical jobs:
- Improved backpressure handling. Due to knowing the conditions, we are able to track the number of pending jobs at all times. This enables early warnings for the developers in case of job buildup. (TODO)
- Better observability. We know exactly how many items fulfill the condition and how many don't; therefore, we can visualize success rates and current status as a percentage of the total. (TODO)
- Cleaner per-instance retrying. If we start executing a job, but suddenly want to defer execution by some time in Sidekiq, it is definitely doable with scheduled jobs. However, this may entrap you in a "scheduled unique job" hell: if some job keeps getting mistakenly deferred, it might be hard to find out about this behavior without some complex job tracking logic. In contrast, Periodically delivers this functionality for free due to more explicit control over job scheduling and rescheduling. (TODO)
- More clever polling. Since we know the exact condition for new periodic jobs, we can deduce the next execution time and sleep accordingly. (TODO)
- Easier priority escalation. Periodically selects jobs in order of the given condition and maintains no queue of its own; therefore it is trivial to prioritize certain jobs by adding a new query condition.
Importantly, Sidekiq and Periodically aim to solve different problems. Nothing prevents one from using both at the same time.