Skip to content

Repository files navigation

intemporal

Continuous Integration

(in)temporal - NOT temporal

A Clojure library in the spirit of temporal.io or uber cadence. Lets you define a function with side effects, and persist/resume the state of the function.

⚠️ use at your own peril, NOT production ready

Two concepts apply:

  • Activities: Either a protocol+impl, or a function. Handles side-effects
  • Workflows: Functions that orchestrate activities, and are resillient. If a process crashes, the workflow should be able to safely resume with at-least-once semantics

Usage

Examples:

Note that when the runtime is javascript, all activities will return a promise. Thus, the use of promesa.core/let is advised

(nsintemporal.demo
(:require [intemporal.core :as intemporal]
[intemporal.store :as store]))
;; Activities are regular functions
(defnactivity-fn [a]
[:processed a])
;; Protocols can also be used as activities
(defprotocolMyActivities
(foo [this a]))
(defrecordMyActivitiesImpl []
MyActivities
(foo [this a] [a :child]))
;; Workflows orchestrate activities via stubs
(defnmy-workflow [i]
(let [act (intemporal/stub #'activity-fn)
pr (intemporal/stub-protocol MyActivities)]
{:activity (act i)
:protocol (foo pr :X)}))
;; Create an engine and run the workflow
(intemporal/with-workflow-engine [engine {:threads2}]
(let [result (intemporal/start-workflow engine
my-workflow [1]
:protocols {MyActivities (->MyActivitiesImpl)})]
(println result)))

Saga / compensations

Create a saga with intemporal/saga, register a compensation for each step after it succeeds with intemporal/add-compensation, and roll back from a catch block with intemporal/compensate. Compensations run in reverse registration order (LIFO). A step that fails before its add-compensation registers nothing to undo. Compensations should themselves call activity stubs so they are durable and replay-safe.

Both real failures and workflow cancellation flow through the catch, so the one idiom rolls back in either case. Catch Exception: the engine's normal control-flow suspensions subclass Error, so they are excluded automatically and propagate to the engine untouched.

(defnbooking-saga [order]
(let [saga (intemporal/saga)
book-hotel (intemporal/stub #'book-hotel)
book-flight (intemporal/stub #'book-flight)
charge-card (intemporal/stub #'charge-card)
cancel-hotel (intemporal/stub #'cancel-hotel)
cancel-flight (intemporal/stub #'cancel-flight)]
(try
(let [h (book-hotel order)
_ (intemporal/add-compensation saga #(cancel-hotel h))]
(let [f (book-flight order)
_ (intemporal/add-compensation saga #(cancel-flight f))]
;; if charge-card throws, the catch runs compensate -> cancel-flight then;; cancel-hotel (LIFO) -> then rethrows so the workflow finalizes :failed
(charge-card order)
:booked))
(catch Exception e
(intemporal/compensate saga)
(throw e)))))

Cancellation is a catchable Exception, so any (catch Exception ...) in a workflow will intercept it — that is what lets a cancelled saga roll back.

In ClojureScript there is no Error/Exception split (everything is a js/Error), so (catch :default e) would also catch suspensions. There, rethrow them explicitly:

 (catch:default e
(when (intemporal/suspension? e) (throw e)) ;; engine control flow
(intemporal/compensate saga)
(throw e))

TODO

  • Activites + Workflows
  • Pass stub options
    • Discard ActivityOptions
    • Protocol options
    • Regular fn options
  • Convert to .cljc
  • Workers + Queues
  • Saga pattern
  • Workflow cancellation
  • OT tracing
  • Signals

About

temporal.io-like but on valium and cbd

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages