Skip to content

Small, fast, and complete interceptor library for Clojure/Script

License

Notifications You must be signed in to change notification settings

metosin/sieppari

Repository files navigation

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:

  • 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.

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.

  • 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.

Differences to Pedestal

Execution

  • io.pedestal.interceptor.chain/execute executes Contexts
  • sieppari.core/execute executes Requests (which are internally wrapped inside a Context for interceptors)

Errors

  • In Pedestal the error handler takes two arguments, the ctx and the exception.
  • In Sieppari the error handlers takes just one argument, the ctx, and the exception is in the ctx under the key :error.
  • In Pedestal the error handler resolves the exception by returning the ctx, and continues the error stage by re-throwing the exception.
  • In Sieppari the error handler resolves the exception by returning the ctx with the :error removed. To continue in the error stage, just return the ctx 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 catches java.lang.Exception. This means that things like out of memory or class loader failures are not captured by Sieppari.

Async

  • Pedestal transfers thread local bindings from call-site into async interceptors.
  • Sieppari does not support this.

Thanks

License

Copyright © 2018-2020 Metosin Oy

Distributed under the Eclipse Public License 2.0.