ChoreRunner

An Elixir library for writing and running code chores.

What is a “Chore”?

A “Chore” can really be anything, but most commonly it is just some infrequently, manually run code which achieve a business or development goal.

For example: updating a config value in a database that does not yet have a UI (perhaps due to time constraints) is a great use for a chore. A chore could be created that accepts the desired value and runs the update query.

What problem does this solve?

The alternative to a chore would be a direct prod-shell or prod-db connection, which is inherently insecure and dangerous. Many fast-moving startups or companies are ok with this access for developers, and that’s fine. But many companies have regulations that they must follow, or do not want to take the risk of a developer mistake while working in these environments.

In these cases, ChoreRunner allows the rapid creation, testing, and reviewing of code chores, along with a bundled UI for running them that accepts a variety of input types, with the goal of finding a “sweet spot” of safety and speed when solving such problems.

Shells vs Chores

Installation

Add chore_runner to your deps.

{:chore_runner, "~> 0.1.0"}

Add ChoreRunner to your supervision tree, after your app’s PubSub:

children = [
  {Phoenix.PubSub, [name: MyApp.PubSub]},
  {ChoreRunner, [pubsub: MyApp.PubSub]},
]

Decide your app’s chore namespace and maybe write an example chore:

defmodule MyApp.Chores.MyFirstChore do
  use ChoreRunner.Chore
  # Chore namespace is MyApp.Chores
  def run(_), do: :ok
end

Add the UI to your router, passing otp_app, chore_root, and pubsub keys in the session:

@chore_session %{"otp_app" => :my_app, "chore_root" => MyApp.Chores, "pubsub" => MyApp.PubSub}
scope "/" do
  pipe_through :browser

  live_session :chores, session: @chore_session do
    live "/chores", ChoreRunnerUI.ChoreLive
  end
end

NOTE

Your Phoenix app must be LiveView-enabled for the UI to work properly.

You can now visit your UI at the route specified.

Writing a Chore

The most basic chore module looks like this

defmodule MyApp.Chores.BasicChore do
  use ChoreRunner.Chore

  def run(_) do
  # ...
  end
end

Besides run/1, there are two other callbacks that you can implement

input/0

defmodule MyApp.Chores.BasicChore do
  use ChoreRunner.Chore

  def input do
    [
      string(:my_string),
      int(:my_int),
      float(:my_float),
      bool(:my_bool),
      file(:my_file),
    ]
  end

  def run(_inputs) do
  # ...
  end
end

The input callback lets you define expected inputs to the chore using input functions imported from ChoreRunner.Input. Input not defined in the callback will be discarded when passed to a chore through ChoreRunner.run_chore/2. The 5 supported input types are:

Input functions also accept an optional keyword list of options as a second argument. The supported keys are:

restriction/0

The restriction callback configures each chore’s concurrency restriction. Concurrency restriction potentially prevents a chore from running depending on what chores are currently running. This includes chores running on other nodes. It supports 3 valid return values:

run/1

The meat of your chore will reside in the run/1 callback. When you run a chore through the UI, that calls ChoreRunner.run_chore/2, which eventually calls out to your run/1 callback. It accepts an atom-keyed map of expected input, defined by the input callback. Input presence in the map is not garaunteed, but you are garaunteed to only receive input specified in the callback. Any return value is is stored in the chore struct via the reporter, broadcasted on the pubsub, and also directly passed to the configured chore resolution handler on chore completion.

Chore Reporting

use ChoreRunner.Chore imports the following functions for use in the run/1 callback:

These functions work in both the main chore process, and certain spawned processes such as via Task.async_stream for parellelization. Attempting to call these functions outside of those conditions will result in an exception.