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