Ablauf is intended to provide a simple way to manage long-running workflows of actions reaching out to multiple different systems. It provides a way to express, run, and inspect workflows. Workflows consist of a series of sequential of parallel steps, with minimal flow control.
Ablauf is interesting if you want to
Most asynchronous workflows rely on basic flow control:
Resulting ASTs for such programs are extremely limited. Syntax trees can be modelled with four node types:
So a simple program like:
(dopar!!
(log!! "a")
(do!!
(log!! "b")
(log!! "c"))
(try!!
(log!! "d")
(rescue!! (log!! "e"))
(finally!! (log!! "f"))))
Would result in the following tree:
This is what ablauf.ast
provides, with a corresponding spec.
Now that a simplistic but sufficient AST exists, comes the question of its execution. It would be trivial to walk the above tree and execute things as they are found. The notion of parallel nodes makes things a bit less obvious.
Long running workflows bring three additional requirements to the table:
The first requirement mandates that workflows should do their best to provide high availability, the second mandates that workflows should be decoupled from their execution environment.
To fulfill the above requirements, it was assumed that workflows would either be processed from a manifold-based single-process environment or from a durable queue, depending on the workflow type. So can a simple AST's execution be separated from its execution environment?
The proposed model here takes inspiration from Continuation Passing Style (CPS), but proposes passing the resulting syntax tree instead of a procedure.
Execution of a program results in feeding the result of previously dispatched actions to a reducer which generates new potential actions to dispatch. This can be provided with the following signatures
make_job :: AST -> Job
restart_job :: Job -> [Results] -> Job -> [Actions]
Here make_job
, generates a workflow ready for execution.
restart_job
given a set of results would yield an updated
workflow and follow-up actions to take. The namespace in ablauf.job
provides exactly this functionality for the AST described above.
This was simplified by clojure.zip
's zippers, a data structure which
stores a tree and the position in that tree, making walking and
storing AST state that much easier. Gérard Huet wrote a
paper
which inspired clojure.zip
, a recommended read.
Workflows consist of a data structure which honors the
:ablauf.job.ast/ast
spec. Functions in the ablauf.job.ast
namespace
provide a simple DSL to produce valid workflows.
Let's say that you want to implement a workflow which creates a git repository, then a declare a CI job and sentry project in parallel, and finally sends a notification to indicate success.
(defn create-deployable [name]
(ast/do!!
(ast/action!! :git/create-repository {:name name})
(ast/dopar!!
(ast/action!! :jenkins/create-build {:name name})
(ast/action!! :sentry/create-project {:name name}))
(ast/action!! :chat/send-message {:message (str "project " name " succesfully created.")})))
Workflow leaves are abstract actions and depend on an action function to be performed. They all require a payload, and produce an output. Throughout the workflow execution, a context map is provided which can be augmented by the result of individual actions.
To pull data out of an action into the context, ablauf.job.ast/with-augment
can be used:
;; Store the created DSN at the `:sentry/dsn` key in the context.
(with-augment [:sentry/dsn :sentry/dsn]
(action!! :sentry/create-project {:name name}))
Workflow runners walk through a workflow and invoke an action function as necessary. Ablauf comes with two bundled runners:
ablauf.job.manifold
: An in-memory processor based on
manifold.ablauf.job.sql
: A SQL-backed processor which decouples job
submission and job running.AST or abstract syntax tree, is the representation of the ablauf program. It can consist of the following nodes:
:ast/leaf
: A node without children. Represents an action the
program should take.:ast/seq
: Represents a list of actions that will be executed
sequentially.:ast/par
: Represents a list of actions that will be executed in
parallel.:ast/try
: A node that contains at most 3 children:
:ast/seq
containing the actions to be tried:ast/seq
containing the actions to be executed if
the ast in form returns an error:ast/seq
containing actions that will be executed
after either forms or rescue completes.An :ast/leaf
, which represents an action performed by the program,
is defined by the following spec
(defmethod spec-by-ast-type :ast/leaf
[_]
(s/keys :req [:ast/action :ast/payload]))
(s/def :ast/action keyword?)
(s/def :ast/payload any?)
The ::ast/action
is a keyword that represents the action to be
performed. A dispatcher matches this keyword to the actual
implementation. By default, the manifold runner uses the
dispatch-action
multimethod to route the action to a function that
knows how to handle it, based on the ::ast/action
key. It is
possible to supply your own dispatcher implementation using the
:action-fn
key in manifold/runner
.
This solution has a few obvious problems:
Storing the full AST tree, including results, for each step will limit the size of workflows that can be created.
Can you improve this documentation? These fine people already did:
Miguel Ping, Pierre-Yves Ritschard, Thiago Dantas, Arnaud Geiser & Wouter DullaertEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close