jeffh / workflow

Data Machines

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Workflow

Leveraging state machines as a faster way to build web services. Requires Java 11+

Problem

Typical web software development needs to handle the following concerns:

  • Implementing new features (hopefully obvious)
  • Reliability of operation & implementation, including upgrades/migrations
  • Observability/Explainability of the system to technical and non-technical people

But today's efforts to address this employ personel & tooling around these issues, instead of trying to address them directly:

  • Microservices: While a logical way to organize teams, there should be efforts to broaden understandability of the system as a whole (for everyone involved). Microservices should be easier, faster, & more reliable to build that an equivalent monolith.
  • "Micro** Tools: Tools tend to give a limit perspective to solve the wider problem. Error tracking, metrics, and log aggregations are a proxy to understanding what happened.
  • Processes: Specifications that are independent from code. There is a gap understanding how a system work for technical and non-technical people that should be narrowed.

Nothing is wrong with those above efforts, but this project attempts to imagine something different.

In short, the goal is make implementations effortless enough to allow people to better understand problems to solve instead of how to execute effectively.

Usage

Note: Nothing here is a stable API. Things are subject to change at this point in time.

(require '[net.jeffhui.workflow.interpreters :refer [->Sandboxed]]) ;; Uses Sci, alternative is ->Naive which uses eval
(require '[net.jeffhui.workflow.api :as api])
(require '[net.jeffhui.workflow.memory :as mem]) ;; in-memory implementation
(require '[clojure.core.async :as async])
(defn make []
(let [statem (mem/make-statem-persistence)]
  (api/effects {:statem      statem
                :execution   (mem/make-execution-persistence statem)
                :scheduler   (mem/make-scheduler)
                :interpreter (->Sandboxed)})))

(def fx (make))
(api/save-statem fx #:state-machine{:id             "order"
                                    :version        1
                                    :start-at       "create"
                                    :execution-mode "async-throughput"
                                    :context        '{:order {:id (str "R" (+ 1000 (rand-int 10000)))}}
                                    :states         '{"create"    {:always [{:name  "created"
                                                                            :state "cart"}]}
                                                      "cart"      {:actions {"add"    {:name    "added"
                                                                                      :state   "cart"
                                                                                      :context (update-in context [:order :line-items] (fnil into []) (repeat (:qty input 1) (:sku input)))}
                                                                            "remove" {:name    "removed"
                                                                                      :state   "cart"
                                                                                      :context (letfn [(sub [a b]
                                                                                                          (let [a (vec a)
                                                                                                                n (count a)]
                                                                                                            (loop [out (transient [])
                                                                                                                  i   0
                                                                                                                  b   (frequencies b)]
                                                                                                              (if (= i n)
                                                                                                                (persistent! out)
                                                                                                                (let [ai (a i)]
                                                                                                                  (if (pos? (b ai 0))
                                                                                                                    (recur out (inc i) (update b ai dec))
                                                                                                                    (recur (conj! out ai) (inc i) b)))))))]
                                                                                                  (update-in context [:order :line-items] (fnil sub []) (repeat (:qty input 1) (:sku input))))}
                                                                            "place"  {:state "submitted"}}}
                                                      "submitted" {:actions {"fraud-approve" {:state "fraud-approved"}
                                                                            "fraud-reject"  {:state "fraud-rejected"}}}

                                                      "fraud-approved" {:always [{:state "released"}]}
                                                      "fraud-rejected" {:actions {"cancel" {:state "canceled"}}}
                                                      "released"       {:always [{:id     "ship"
                                                                                  :name   "ship"
                                                                                  :invoke {:state-machine ["shipment" 1]
                                                                                          :input         {:order (:id (:order ctx))}
                                                                                          :success       {:state   "ship-finished"
                                                                                                          :context {:delivered (:delivered output)}}
                                                                                          :error         {:state "canceled"}}}]}
                                                      "ship-finished"  {:always [{:name  "fulfilled"
                                                                                  :when  (:delivered ctx)
                                                                                  :state "shipped"}
                                                                                {:name  "canceled"
                                                                                  :state "canceled"}]}
                                                      "shipped"        {:end true}
                                                      "canceled"       {:end true}}})
(api/save-statem fx #:state-machin  e{:id             "shipment"
                                    :version        1
                                    :start-at       "created"
                                    :execution-mode "async-throughput"
                                    :context        '{:id        "S1"
                                                      :order     (:order input)
                                                      :delivered false}
                                    :states         '{"created"     {:always [{:name  "fulfilled"
                                                                                :state "outstanding"}]}
                                                      "outstanding" {:always  [{:id     "fetch"
                                                                                :name   "fetched"
                                                                                :invoke {:given (io "http.request.json" :post "https://httpbin.org/anything" {:json-body {"n" (rand-int 10)}})
                                                                                          :if    (<= 200 (:status output) 299)
                                                                                          :then  {:state   "fetched"
                                                                                                  :context {:response {:n (:n (:json (:body output)))}}}
                                                                                          :else  {:state "failed"}}}]
                                                                      :actions {"cancel" {:state "canceled"}}}
                                                      "failed"      {:always [{:name     "retry"
                                                                                :state    "outstanding"
                                                                                :wait-for {:seconds 5}}]}

                                                      "canceled" {:end    true
                                                                  :return {:delivered false}}

                                                      "fetched" {:always [{:name    "deliver"
                                                                            :state   "delivered"
                                                                            :when    (> 3 (:n (:response ctx)))
                                                                            :context {:response nil
                                                                                      :result   (:n (:response ctx))}}
                                                                          {:name     "retry"
                                                                            :state    "outstanding"
                                                                            :context  {:response nil}
                                                                            :wait-for {:seconds 5}}]}

                                                      "delivered" {:end    true
                                                                    :return {:delivered true}}}})


(p/register-execution-handler fx (api/create-execution-handler fx))
(def out (api/start fx "order" nil))
(api/trigger fx (second out) {::api/action "add"
                              ::api/reply? true
                              :sku        "bns12"
                              :qty        1})
(api/trigger fx (second out) {::api/action "place"})
(def res (api/trigger fx (second out) {::api/action "fraud-approve"
                                       ::api/reply? true}))
(async/take! res prn)

License

Copyright © 2021 Jeff Hui

This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0.

Open Research Areas

Here's some extra things to look into:

  • How to maintain high performance, despite more data being generated.
  • How to minimize the amount of need of escape hatches
  • How to share state machine implementations

Development

Running test:

# needs services running
cd test-services
docker up -d
cd ..
# run tests
clojure -X:test

About

Data Machines

License:Other


Languages

Language:Clojure 100.0%