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
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
.
(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}
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
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:
- core.async -
sieppari.async.core-async
, clj & cljs - Manifold -
sieppari.async.manifold
clj
To extend Sieppari async support to other libraries, just extend the AsyncContext
protocol.
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}
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}
Sieppari aims for minimal functionality and can therefore be quite fast. Complete example to test performance is included.
Executing a chain of 10 interceptors, which have :enter
of clojure.core/identity
.
- sync: all steps return the ctx
- promesa: all steps return the ctx in an
promesa.core/promise
- core.async: all step return the ctx in a
core.async
channel - manifold: all step return the ctx in a
manifold.deferred.Deferred
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 | - | - | - |
- MacBook Pro (Retina, 15-inch, Mid 2015), 2.5 GHz Intel Core i7, 16 MB RAM
- Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
- Clojure 1.9.0
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
.
io.pedestal.interceptor.chain/execute
executes Contextssieppari.core/execute
executes Requests (which are internally wrapped inside a Context for interceptors)
- In Pedestal the
error
handler takes two arguments, thectx
and the exception. - In Sieppari the
error
handlers takes just one argument, thectx
, and the exception is in thectx
under the key:error
. - In Pedestal the
error
handler resolves the exception by returning thectx
, and continues the error stage by re-throwing the exception. - In Sieppari the
error
handler resolves the exception by returning thectx
with the:error
removed. To continue in the error stage, just return thectx
with the exception still at:error
. - In Pedestal the exception are wrapped in other exceptions.
- In Sieppari exceptions are not wrapped.
- Pedestal interception execution catches
java.lang.Throwable
for error processing. Sieppari catchesjava.lang.Exception
. This means that things like out of memory or class loader failures are not captured by Sieppari.
- Pedestal transfers thread local bindings from call-site into async interceptors.
- Sieppari does not support this.
- Original idea from Pedestal Interceptors.
Copyright © 2018-2020 Metosin Oy
Distributed under the Eclipse Public License 2.0.