I've been using AASM to make state machines in my current project and was wondering what's the best way to automatically call events and proceed to the next state?
I am considering 2 ways of doing this:
Setup a background job to periodically check if certain conditions are met, hence call the event to proceed to the next state.
Have a before_save
call a method that tries the next event in succession. With a guard on it, it won't succeed if conditions are met, otherwise, state changes and next time Model is updated, we check for a new event.
I was leaning towards the second option as setting up a background_job queue just to transition events seems like an overkill. I couldn't find best practices regarding this, so I would love to know the best approach and why it is so?
Example
For example we have start_onboarding
and complete_onboarding
events. I don't want to manually call these events, but I want to automatically transition between pending -> in_progress -> completed events.
enum status: {
pending: 1,
in_progress: 2,
completed: 3
}
aasm column: :status, enum: true, whiny_transitions: false do
state :pending, initial: true
state :in_progress
state :completed
event :start_onboarding do
transitions from: :pending, to: :in_progress
end
event :complete_onboarding do
transitions from: :in_progress,
to: :completed,
if: :onboarding_completed?
end
end
In the similar task:
We got rid of:
We come to using:
And the code was looking something like this:
require 'active_record'
require 'aasm'
require 'sidekiq'
class Task < ActiveRecord::Base
include AASM
establish_connection adapter: 'sqlite3', database: 'todo.db'
connection.create_table table_name, force: true do |t|
t.string :name, null: false
t.string :aasm_state, null: false, index: true
t.datetime :expired_at, null: false
end
validates :name, :aasm_state, :expired_at, presence: true
aasm do
state :pending, initial: true
state :in_progress
state :completed
state :expired
event :run do
transitions to: :in_progress
end
event :complete do
transitions to: :completed
end
event :expire do
transitions to: :expired, unless: :completed?
end
end
end
class Task::ExpireJob
include Sidekiq::Worker
def perform task
task.expire!
end
end
class Task::CreationService
def self.create! params
task = Task.create! params
task.run!
Task::ExpireJob.perform_at task.expired_at, task
task
end
def self.complete! task
task.complete!
task
end
end
task = Task::CreationService.create! \
name: 'first',
expired_at: DateTime.now + 30.seconds
p task
p Task::CreationService.complete! task