LevelUp Documentation
Everything you need to know to build jobs using LevelUp
!This documentation is still being written. You can send feedback via email or via Github Issues.
Introduction
» What is it?
If you are building a web app, chances are good you have some jobs to design and execute in order to provide services to your customers. Generally, that means handling computer-based or manual tasks, calls to external services, failures, timers and retries. LevelUp lets you build all of these and compose them to create a runnable job. Concretely, you will define a task graph, where each task contains its own business logic implemented in ruby. Jobs can be performed synchronously in the current thread or asynchronously by background workers. Three methods are available in each state to control the job flow: move_to!(task_name), retry_in!(delay), manual_task!(task_description).
» Why use graphs?
Designing your jobs graphically with tasks and transitions can be more easier than directly writing code, especially for non-technical people. Graphs can be drawn, printed, shared and analysed making it easier to let everyone know what the system is doing at specific points in time. From a developers point of view, it’s clearer to separate the different parts of a job into isolated tasks. Class-based tasks are reusable in multiple jobs to avoid code duplication. For example, you can use the Template design pattern to implement the generic part of a task in a parent class and implement specialized parts in children classes.
Getting Started
» Including LevelUp
Add LevelUp to your Gemfile:
gem 'level_up'
and run bundle install within your app's directory.
» Running Migrations
First, make sure DelayedJob migration is installed. If not:
$ rails g delayed_job:active_record
Install and run LevelUp migration:
$ rake level_up:install:migrations $ rake db:migrate
» Mounting the engine
In your routes file:
# config/routes.rb Rails.application.routes.draw do mount LevelUp::Engine => "/level_up" # other routes ... end
Core Concepts
» Jobs
Being written.
» Tasks
Being written.
» Transitions
Being written.
» Timers
Being written.
» Manual Tasks
Being written.
» Errors
Being written.
» Work Queue
Being written.
Job Models
» Tasks & Transitions
# app/models/hard_job.rb class HardJob < LevelUp::Job # tasks and transitions job do # you must declare a transition from the 'start' state task :start, transitions: :first_task # 'first_task' declaration with one transition to 'second_task' task :first_task, transitions: :second_task # 'second_task' declaration with two transitions task :second_task, transitions: [:third_task, :another_task] # task class overridden task :third_task, class_name: 'CustomTask', transitions: :end # you must declare a transition to the 'end' state task :another_task, transitions: :end end end
» Task Methods
The most simple way to implement tasks is to define instance methods in job classes. The instance method must have the same name that the task it implements.
iTask instance methods take precedence over any task classes.
# app/models/hard_job.rb class HardJob < LevelUp::Job job do # ... end # task definition through methods def first_task # 'first_task' logic goes here end def second_task # 'second_task' logic goes here end def third_task # 'third_task' logic goes here end def another_task # 'another_task' logic goes here end end
» Task Classes
You can also define task logic in a class instead of a method. The class must have the same that the task it implements (camelcased) and be inside a module with the same name that the job it depends. The class must have a run instance method that contains the task logic.
# app/models/hard_job/first_task.rb module HardJob class FirstTask < LevelUp::Task def run # 'first_task' logic goes here end end end
» Flow Control
Inside a task, you can call 3 methods to control the job flow.
+ move_to!(task_name)
Leave the current task and run the specified one:
def first_task # ... # at some point in 'first_task' task definition move_to! :second_task end
+ retry_in!(delay)
Stop the execution and queue a new delayed_job to re-run the current task after the specified delay in seconds:
def second_task # ... # at some point in 'second_task' task definition retry_in! 1.hour end
+ manual_task!(description)
Stop the execution and set the manual_task and manual_task_description attributes to notify that a manual human intervention is needed.
def third_task # ... # at some point in 'third_task' task definition manual_task! "task description goes here" end
You can also raise a StandardError (or a subclass) to stop the execution and set the error attribute. The time, the task and the error backtrace will be saved.
Running Jobs
You can create new jobs in your controllers, models or also in other jobs and start them.
Running Synchronously
Execute the job synchronously in the current thread
sync_job = HardJob.create(key: "job-key") sync_job.boot!
Running Asynchronously
Execute the job asynchronously with a new delayed_job
async_job = HardJob.create(key: "job-key") async_job.boot_async!
Job Statuses
Queued
A job is considered as queued when it references a delayed_job in the queue ready to be consumed by a worker. This happens when you call the boot_async! method on a job.
job = HardJob.create(key: 'job-key') job.boot_async! # the job is now queued and ready to be consumed
Timer
The timer attribute indicates whether or not a delayed_job has been pushed into the queue to be consumed by a worker at a specific time. This happens when you call the retry_in!(delay) method inside a task. The retry_at attribute indicates the time when the job will be ready to be consumed. Before that time, you can manually remove the job from the queue calling its unqueue! method.
# remove the job from the queue before beeing consumed job.unqueue!
Task
The manual_task attribute indicates whether or not there is a manual human task to accomplish. The manual_task_description attribute provides details about what is expected to be done. Inherently, if the manual_task attribute is equal to true, there is no delayed_job in the queue. The job is suspended waiting for a human intervention. This happens when you call the manual_task!(description) method inside a task.
Error
The error attribute indicates whether or not an error has been raised and not rescued when running a task. In this case, the error is rescued at the job level. The failed_in, failed_at and backtrace attributes save the task where the error has been raised, the time and the first five elements of the backtrace error (the backtrace is serialized as an array of strings in the database).
class HardJob < LevelUp::Job job do # ... end def first_task # ... # an error is raised 0 / 0 # the error is rescue at the job level # failed_in => 'first_task' # failed_at => the the when the error has been raised # backtrace => ['ZeroDivisionError: divided by 0', ...] end end
Configuration
» Authentication
LevelUp only provide basic http authentication through Rails 3.1+ http_basic_authenticate_with method. Authentication is disabled by default. To enable it, add these lines in your config/environments/*.rb.
config.level_up.http_authentication = true config.level_up.http_login = "your-login" config.level_up.http_password = "your-password"
» Backtrace Size
When an error is rescued at the job level, the task where the error was raised, the time and a part of the error backtrace is saved as an array of string elements. You can configure the size of the saved backtrace.
# the first ten elements will be saved config.level_up.backtrace_size = 10 # disable backtrace config.level_up.backtrace_size = 0