jobbit v0.5.0 Jobbit

Jobbit

Jobbit CI

Run risky tasks asynchronously or synchronously without endangering the calling process.

Jobbit is a thin, mildly opinionated wrapper for Elixir's Task and Task.Supervisor modules and functionality.

Task and Task.Supervisor provide an easy-to-use, extensible, and dependable interface for running one or many supervised or unsupervised asynchronous tasks. If you want to "harness the power of OTP" you should investigate those two modules and what can be achieved with their use.

Installation

Add jobbit to your list of dependencies in mix.exs:

def deps do
  [
    {:jobbit, github: "elbow-jason/jobbit", ref: "b5c686c"},
  ]
end

Usage / Running Tasks

Tasks in Jobbit can be run with a closure:

Jobbit.async(fn -> :ok end)
=> %Jobbit{}

Or with a module, func, and args (similar to apply/3):

Jobbit.async_apply(Kernel, :div, [1, 0])
=> %Jobbit{}

A task can be synchronized:

task = Jobbit.async(fn -> MyClient.send_request(payload) end)
=> %Jobbit{}

task
|> Jobbit.yield(2000) # yield with a custom timeout
|> case do
  {:ok, %SomeResponse{}} -> :request_succeeded
  {:error, :not_authorized} -> :request_returned_an_error
  {:error, %TaskError{}} -> :request_crashed
  {:error, %TimeoutError{}} -> :request_timeout
end

FAQ

How is Jobbit like Task?

  • Both are used to perform asynchronous tasks.

  • Both have yield/2 which waits for results for a certain amount of time (timeout), but does not raise upon timeout.

How is Jobbit like Task.Supervisor?

  • Both are used to perform asynchronous tasks.

  • Both can start caller-unlink, supervised tasks.

  • Both require a Task.Supervisor to be running

    • Note: Jobbit itself can be used as a child_spec callback module instead of Task.Supervisor.
    • Note: Jobbit starts its own task supervisor by default at application startup.

How is Jobbit different than Task?

  • Jobbit never links to the calling process. All the risk is move to the task process.

  • Jobbit only provides one function to (idiomatically) synchronize on a tasks result (via yield/2). Task has also has yield/2 which is similar, but also provides await/2 which will raise if the task times out; yeild/2 will not raise.

  • Jobbit homogenizes results of tasks. With Task yielding can return {:ok, :ok}. Jobbit homogenizes {:ok, :ok} into :ok. This way is much less boilerplate.

How is Jobbit different than Task.Supervisor?

  • Jobbit provides a default supervisor via its application tree.

  • Jobbit is less generalized, but easier to out-of-the-box.

  • Jobbit has fewer functions, and a more focused scope. Jobbit ONLY runs asynchronous, unlinked tasks.

Task Supervision

With Jobbit, tasks are run on a Jobbit task supervisor and the Jobbit.Application starts a default task supervisor (default: Jobbit.DefaultTaskSupervisor) at application startup.

Jobbit implements child_spec/1 and can, therefore, be used as a child's callback module for a supervisor

A supervisor can be added to a supervision tree using like so:

# in `MyApp.SomeSupervisor` or in `MyApp.Application`...
# Note: it's a good idea to `:name` your task supervisor
# (because you need to be able to address it)...

children = [
  {Jobbit, name: MyApp.MyBusinessDomainTaskSupervisor}
]

A custom Jobbit task supervisor can also be started directly via Jobbit.start_link/1.

Jobbit.start_link(name: :some_task_sup)
=> {:ok, #PID<0.109.0>}

Or Jobbit.start_link/0:

Jobbit.start_link()
=> {:ok, #PID<0.110.0>}

Configuration

Jobbit can be configured via config/*.exs files.

By default, the :jobbit OTP app will start a default task supervisor called Jobbit.DefaultTaskSupervisor.

The default task supervisor can be configured via the :default_supervisor config value.

Additionally, the entire :jobbit application can instructed not to start by flagging :start_jobbit? with a falsey (nil or false) value.

Note: Jobbit.async/1 and Jobbit.async_apply/3 rely on the default supervisor to be running when they are called. If start_jobbit?: false is set in the config and the :default_supervisor is not set to a running task supervisor these functions will not work.

An example of configuring :jobbit:

config :jobbit,
  start_jobbit?: true,
  default_supervisor: Jobbit.DefaultTaskSupervisor,
  default_supervisor_opts: []

Link to this section Summary

Functions

Runs the given closure as a task on the given supervisor.

Runs the given module, func, and args as a task on the given supervisor.

The child spec for a Jobbit.

Shuts down a Jobbit task.

Starts a Task.Supervisor passing the provided option list.

Synchronously blocks the caller waiting for the Jobbit task to finish.

Link to this section Types

Specs

args() :: [any()]

Specs

closure() :: (() -> any())

Specs

Specs

func_name() :: atom()

Specs

on_start() :: Task.Supervisor.on_start()

Specs

option() :: Task.Supervisor.option()

Specs

result() :: :ok | {:ok, any()} | {:error, any()} | tuple() | {:error, error()}

Specs

shutdown() :: :brutal_kill | :infinity | non_neg_integer()
Link to this type

supervisor()

Specs

supervisor() :: atom() | pid() | {atom(), any()} | {:via, atom(), any()}
Link to this type

supervisor_t()

Specs

supervisor_t() :: Task.Supervisor

Specs

t() :: %Jobbit{task: Task.t()}

Link to this section Functions

Link to this function

async(supervisor \\ default_supervisor(), closure, opts \\ [])

Specs

async(supervisor(), closure(), Keyword.t()) :: t()

Runs the given closure as a task on the given supervisor.

The task runs as an unlinked, asynchronous, task process supervised by the supervisor (default: Jobbit.DefaultTaskSupervisor).

See Task.Supervisor.async_nolink/3 for opts details.

Link to this function

async_apply(supervisor \\ default_supervisor(), module, func, args, opts \\ [])

Specs

async_apply(supervisor(), module(), func_name(), args(), opts :: Keyword.t()) ::
  t()

Runs the given module, func, and args as a task on the given supervisor.

The task runs as an unlinked, asynchronous, task process supervised by the supervisor (default: Jobbit.DefaultTaskSupervisor).

See Task.Supervisor.async_nolink/3 for opts details.

Link to this function

child_spec(jobbit_opts \\ [])

Specs

child_spec([option()]) :: Supervisor.child_spec()

The child spec for a Jobbit.

The child_spec for a Jobbit task supervisor. This child_spec forwards the provided jobbit_opts to Jobbit.start_link/1 when the child_spec is applied as a child by a supervisor.

Link to this function

default_supervisor()

Specs

default_supervisor() :: atom()
Link to this function

shutdown(jobbit, shutdown \\ 5000)

Specs

shutdown(t(), shutdown()) :: result()

Shuts down a Jobbit task.

Link to this function

start_link(opts \\ [])

Specs

start_link([option()]) :: on_start()

Starts a Task.Supervisor passing the provided option list.

Example:

iex> {:ok, pid} = Jobbit.start_link() iex> is_pid(pid) true

iex> {:ok, pid} = Jobbit.start_link(name: :jobbit_test_task_sup) iex> is_pid(pid) true iex> Process.whereis(:jobbit_test_task_sup) == pid true

Link to this function

yield(jobbit, timeout \\ 5000)

Specs

yield(t(), timeout()) :: result()

Synchronously blocks the caller waiting for the Jobbit task to finish.

Easier than Task.yield/2

When yielding with the Task module it is the caller's responsibility to ensure a task does not live beyond it's timeout. The Task documentation recommends the following code:

Task.yield(task, timeout) || Task.shutdown(task)

With the above code, if the caller to Task.yield/2 forgets to call Task.shutdown/1 the running task might never stop. Additionally, the outcome of call above is not very straight forward; there are a multitude of return values (If you are curious take a look at the source code of yield/2).

In Jobbit, yield/2 calls both Task.yield/2 and Task.shutdown/2 and wraps/handles the multitude of result types

Outcomes

Yielding a Jobbit task with yield/2 will result in 1 of 4 outcomes:

  • success: The task finished without crashing the task process. In the case of success, the return value with be either an ok-tuple that the closure/mfa returned, an error-tuple that the closure/mfa returned, or {:ok, returned_value} where returned_value was the return value from the closure.

  • exception: An exception occured while the task was running and the task process crashed. When a exception occurs during task execution the return value is {:error, TaskError.t()}. The TaskError itself is an exception (it can be raised). Also, TaskError wraps the task's exception and stacktrace which can be used to find the cause of the exception or reraised if necessary.

  • timeout: The task took too long to complete and was gracefully shut down. In the case of a timeout, yield/2 returns {:error, TimeoutError.t()}. TimeoutError is an exception (it can be raised) that wraps the timeout value.

  • exit: The task process was terminated with an exit signal e.g. Process.exit(pid, :kill). In the case of a non-exception exit signal, yield/2 returns {:error, ExitError.t()}. ExitError