metosin / sieppari

Small, fast, and complete interceptor library for Clojure/Script
Eclipse Public License 2.0
207 stars 21 forks source link
async clojure clojurescript interceptor metosin-inactive promise

sieppari cljdoc badge

Small, fast, and complete interceptor library for Clojure/Script with built-in support for common async libraries.

Noun Siepata (Intercept)

sieppari, someone or something that intercepts

What it does

Interceptors, like in Pedestal, but with minimal implementation and optimal performance.

The core Sieppari depends on Clojure and nothing else.

If you are new to interceptors, check the Pedestal Interceptors documentation. Sieppari's sieppari.core/execute follows a :request / :response pattern. For Pedestal-like behavior, use sieppari.core/execute-context.

First example

(ns example.simple
  (:require [sieppari.core :as s]))

;; interceptor, in enter update value in `[:request :x]` with `inc`
(def inc-x-interceptor
  {:enter (fn [ctx] (update-in ctx [:request :x] inc))})

;; handler, take `:x` from request, apply `inc`, and return an map with `:y`
(defn handler [request]
  {:y (inc (:x request))})

(s/execute
  [inc-x-interceptor handler]
  {:x 40})
;=> {:y 42}

Async

Any step in the execution pipeline (:enter, :leave, :error) can return either a context map (synchronous execution) or an instance of AsyncContext - indicating asynchronous execution.

By default, clojure deferrables, java.util.concurrent.CompletionStage and js/promise satisfy the AsyncContext protocol.

Using s/execute with async steps will block:

;; async interceptor, in enter double value of `[:response :y]`:
(def multiply-y-interceptor
  {:leave (fn [ctx]
            (future
              (Thread/sleep 1000)
              (update-in ctx [:response :y] * 2)))})

(s/execute
  [inc-x-interceptor multiply-y-interceptor handler]
  {:x 40})
; ... 1 second later:
;=> {:y 84}

Using non-blocking version of s/execute:

(s/execute
  [inc-x-interceptor multiply-y-interceptor handler]
  {:x 40}
  (partial println "SUCCESS:")
  (partial println "FAILURE:"))
; => nil
; prints "SUCCESS: {:y 84}" 1sec later

Blocking on async computation:

(let [respond (promise)
      raise (promise)]
  (s/execute
    [inc-x-interceptor multiply-y-interceptor handler]
    {:x 40}
    respond
    raise) ; returns nil immediately

  (deref respond 2000 :timeout))
; ... 1 second later:
;=> {:y 84}

Any step can return a java.util.concurrent.CompletionStage or js/promise, Sieppari works oob with libraries like Promesa:

;; [funcool/promesa "5.1.0"]`
(require '[promesa.core :as p])

(def chain
  [{:enter #(update-in % [:request :x] inc)}               ;; 1
   {:leave #(p/promise (update-in % [:response :x] / 10))} ;; 4
   {:enter #(p/delay 1000 %)}                              ;; 2
   identity])                                              ;; 3

;; blocking
(s/execute chain {:x 40})
; => {:x 41/10} after after 1sec

;; non-blocking
(s/execute
  chain
  {:x 40}
  (partial println "SUCCESS:")
  (partial println "FAILURE:"))
; => nil
;; prints "SUCCESS: {:x 41/10}" after 1sec

External Async Libraries

To add a support for one of the supported external async libraries, just add a dependency to them and require the respective Sieppari namespace. Currently supported async libraries are:

To extend Sieppari async support to other libraries, just extend the AsyncContext protocol.

core.async

Requires dependency to [org.clojure/core.async "0.4.474"] or higher.

(require '[clojure.core.async :as a])

(defn multiply-x-interceptor [n]
  {:enter (fn [ctx]
            (a/go (update-in ctx [:request :x] * n)))})

(s/execute
  [inc-x-interceptor (multiply-x-interceptor 10) handler]
  {:x 40})
;=> {:y 411}

manifold

Requires dependency to [manifold "0.1.8"] or higher.

(require '[manifold.deferred :as d])

(defn minus-x-interceptor [n]
  {:enter (fn [ctx]
            (d/success-deferred (update-in ctx [:request :x] - n)))})

(s/execute
  [inc-x-interceptor (minus-x-interceptor 10) handler]
  {:x 40})
;=> {:y 31}

Performance

Sieppari aims for minimal functionality and can therefore be quite fast. Complete example to test performance is included.

Silly numbers

Executing a chain of 10 interceptors, which have :enter of clojure.core/identity.

All numbers are execution time lower quantile (not testing the goodness of the async libraries , just the execution overhead sippari interceptors adds)

Executor sync promesa core.async manifold
Pedestal 8.2µs - 92µs -
Sieppari 1.2µs 4.0µs 70µs 110µs
Middleware (comp) 0.1µs - - -

NOTE: running async flows without interceptors is still much faster, e.g. synchronous manifold chain is much faster than via interceptors.

NOTE: Goal is to have a Java-backed and optimized chain compiler into Sieppari, initial tests show it will be near the perf of middleware chain / comp.

Differences to Pedestal

Execution

Errors

Async

Thanks

License

Copyright © 2018-2020 Metosin Oy

Distributed under the Eclipse Public License 2.0.