[mod] Simplify middleware - don't auto compose

Previously:

  It was possible to provide a vector of middleware fns when creating
  signals or adding handlers, e.g.:

    (event! ::ev-id1 {:middleware [fn1 fn2 ...]}),
    (add-handler! ::handler-id1 <handler-fn> {:middleware [fn1 fn2 ...]})

After this commit:

  Middleware is always expected to be a single fn, or nil.
  A `comp-middleware` util has been added to make it easy to compose multiple
  middleware fns into one.

Motivation:

  The previous (auto-composition) behaviour was nice when adding handlers,
  but incurred a (small but non-trivial) runtime cost when creating signals.

  The actual benefit (convenience) of auto-composition is very small, so
  I've decided to just remove this feature and add the `comp-middleware` util.

  Note that I ruled out the option of doing auto-comp only when adding handlers
  since that've meant an inconsistency and so complexity for little benefit.
This commit is contained in:
Peter Taoussanis 2024-04-29 23:39:33 +02:00
parent 63f488082b
commit 839143167b
4 changed files with 23 additions and 23 deletions

View file

@ -22,7 +22,7 @@ Signal options (shared by all signal creators):
`:sample-rate` - ?rate ∈ℝ[0,1] for signal sampling (0.75 => allow 75% of signals, nil => allow all) `:sample-rate` - ?rate ∈ℝ[0,1] for signal sampling (0.75 => allow 75% of signals, nil => allow all)
`:when` -------- Arb ?form; when present, form must return truthy to allow signal `:when` -------- Arb ?form; when present, form must return truthy to allow signal
`:rate-limit` -- ?spec as given to `taoensso.telemere/rate-limiter`, see its docstring for details `:rate-limit` -- ?spec as given to `taoensso.telemere/rate-limiter`, see its docstring for details
`:middleware` -- ?[(fn [signal])=>modified-signal ...] signal middleware `:middleware` -- Optional (fn [signal]) => ?modified-signal to apply when signal is created
`:trace?` ------ Should tracing be enabled for `:run` form? `:trace?` ------ Should tracing be enabled for `:run` form?
<kvs> ---------- Other arb user-level ?kvs to incl. in signal. Typically NOT included in <kvs> ---------- Other arb user-level ?kvs to incl. in signal. Typically NOT included in

View file

@ -72,6 +72,7 @@
enc/chance enc/chance
enc/rate-limiter enc/rate-limiter
enc/newline enc/newline
enc/comp-middleware
impl/msg-splice impl/msg-splice
impl/msg-skip impl/msg-skip
@ -143,14 +144,13 @@
(comment (with-ctx {:a :A1 :b :B1} (with-ctx+ {:a :A2} *ctx*))) (comment (with-ctx {:a :A1 :b :B1} (with-ctx+ {:a :A2} *ctx*)))
;;;; Middleware ;;;; Signal middleware
(enc/defonce ^:dynamic *middleware* (enc/defonce ^:dynamic *middleware*
"Optional vector of unary middleware fns to apply (sequentially/left-to-right) "Optional (fn [signal]) => ?modified-signal to apply (once) when
to each signal before passing it to handlers. If any middleware fn returns nil, signal is created. When middleware returns nil, skips all handlers.
aborts immediately without calling handlers.
Useful for transforming each signal before handling. Compose multiple middleware fns together with `comp-middleware.
Re/bind dynamic value using `with-middleware`, `binding`. Re/bind dynamic value using `with-middleware`, `binding`.
Modify root (base) value using `set-middleware!`." Modify root (base) value using `set-middleware!`."
@ -159,13 +159,13 @@
#?(:clj #?(:clj
(defmacro set-middleware! (defmacro set-middleware!
"Set `*middleware*` var's root (base) value. See `*middleware*` for details." "Set `*middleware*` var's root (base) value. See `*middleware*` for details."
[root-val] `(enc/set-var-root! *middleware* ~root-val))) [?root-middleware-fn] `(enc/set-var-root! *middleware* ~?root-middleware-fn)))
#?(:clj #?(:clj
(defmacro with-middleware (defmacro with-middleware
"Evaluates given form with given `*middleware*` value. "Evaluates given form with given `*middleware*` value.
See `*middleware*` for details." See `*middleware*` for details."
[init-val form] `(binding [*middleware* ~init-val] ~form))) [?middleware-fn form] `(binding [*middleware* ~?middleware-fn] ~form)))
;;;; Signal creators ;;;; Signal creators
;; - signal! [ opts] ; => allowed? / run result (value or throw) ;; - signal! [ opts] ; => allowed? / run result (value or throw)

View file

@ -632,9 +632,9 @@
'~run-form ~'__run-result ~error-form)] '~run-form ~'__run-result ~error-form)]
;; Final unwrapped signal value visible to users/handler-fns, allow to throw ;; Final unwrapped signal value visible to users/handler-fns, allow to throw
(if-let [call-middleware# ~middleware-form] (if-let [sig-middleware# ~middleware-form]
((sigs/get-middleware-fn call-middleware#) ~'__signal) ; Can throw (sig-middleware# ~'__signal) ; Apply signal middleware, can throw
(do ~'__signal)))))] (do ~'__signal)))))]
;; Could avoid double `run-form` expansion with a fn wrap (>0 cost) ;; Could avoid double `run-form` expansion with a fn wrap (>0 cost)
;; (let [run-fn-form (when run-form `(fn [] (~run-form)))] ;; (let [run-fn-form (when run-form `(fn [] (~run-form)))]

View file

@ -229,10 +229,10 @@
(testing "Call middleware" (testing "Call middleware"
(let [c (enc/counter) (let [c (enc/counter)
[[rv1 _] [sv1]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]})) [[rv1 _] [sv1]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c)))}))
[[rv2 _] [sv2]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))], :allow? false})) [[rv2 _] [sv2]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c))), :allow? false}))
[[rv3 _] [sv3]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]})) [[rv3 _] [sv3]] (with-sigs :raw nil (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c)))}))
[[rv4 _] [sv4]] (with-sigs :raw nil (sig! {:level :info, :middleware [(fn [_] "signal-value")]}))] [[rv4 _] [sv4]] (with-sigs :raw nil (sig! {:level :info, :middleware (fn [_] "signal-value")}))]
[(is (= rv1 0)) (is (sm? sv1 {:m1 1 :m2 2})) [(is (= rv1 0)) (is (sm? sv1 {:m1 1 :m2 2}))
(is (= rv2 3)) (is (nil? sv2)) (is (= rv2 3)) (is (nil? sv2))
@ -260,31 +260,31 @@
(let [c (enc/counter) (let [c (enc/counter)
sv-h1_ (atom nil) sv-h1_ (atom nil)
sv-h2_ (atom nil) sv-h2_ (atom nil)
wh1 (sigs/wrap-handler :hid1 (fn [sv] (reset! sv-h1_ sv)) nil {:async nil, :middleware [#(assoc % :hm1 (c)) #(assoc % :hm2 (c))]}) wh1 (sigs/wrap-handler :hid1 (fn [sv] (reset! sv-h1_ sv)) nil {:async nil, :middleware (tel/comp-middleware #(assoc % :hm1 (c)) #(assoc % :hm2 (c)))})
wh2 (sigs/wrap-handler :hid2 (fn [sv] (reset! sv-h2_ sv)) nil {:async nil, :middleware [#(assoc % :hm1 (c)) #(assoc % :hm2 (c))]})] wh2 (sigs/wrap-handler :hid2 (fn [sv] (reset! sv-h2_ sv)) nil {:async nil, :middleware (tel/comp-middleware #(assoc % :hm1 (c)) #(assoc % :hm2 (c)))})]
;; Note that call middleware output is cached and shared across all handlers ;; Note that call middleware output is cached and shared across all handlers
(binding [impl/*sig-handlers* [wh1 wh2]] (binding [impl/*sig-handlers* [wh1 wh2]]
(let [;; 1x run + 4x handler middleware + 2x call middleware = 7x (let [;; 1x run + 4x handler middleware + 2x call middleware = 7x
rv1 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]}) rv1 (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c)))})
sv1-h1 @sv-h1_ sv1-h1 @sv-h1_
sv1-h2 @sv-h2_ sv1-h2 @sv-h2_
c1 @c c1 @c
;; 1x run ;; 1x run
rv2 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))], :allow? false}) rv2 (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c))), :allow? false})
sv2-h1 @sv-h1_ sv2-h1 @sv-h1_
sv2-h2 @sv-h2_ sv2-h2 @sv-h2_
c2 @c ; 8 c2 @c ; 8
;; 1x run + 4x handler middleware + 2x call middleware = 7x ;; 1x run + 4x handler middleware + 2x call middleware = 7x
rv3 (sig! {:level :info, :run (c), :middleware [#(assoc % :m1 (c)) #(assoc % :m2 (c))]}) rv3 (sig! {:level :info, :run (c), :middleware (tel/comp-middleware #(assoc % :m1 (c)) #(assoc % :m2 (c)))})
sv3-h1 @sv-h1_ sv3-h1 @sv-h1_
sv3-h2 @sv-h2_ sv3-h2 @sv-h2_
c3 @c ; 15 c3 @c ; 15
;; 4x handler middleware ;; 4x handler middleware
rv4 (sig! {:level :info, :middleware [(fn [_] {:my-sig-val? true})]}) rv4 (sig! {:level :info, :middleware (fn [_] {:my-sig-val? true})})
sv4-h1 @sv-h1_ sv4-h1 @sv-h1_
sv4-h2 @sv-h2_ sv4-h2 @sv-h2_
c4 @c] c4 @c]
@ -321,7 +321,7 @@
(tel/with-handler :hid1 (tel/with-handler :hid1
(fn [sv] (force (:data sv)) (reset! sv_ sv)) (fn [sv] (force (:data sv)) (reset! sv_ sv))
{:async nil, :error-fn (fn [x] (reset! error_ x)), :rl-error nil, {:async nil, :error-fn (fn [x] (reset! error_ x)), :rl-error nil,
:middleware [(fn [sv] (if *throwing-handler-middleware?* (ex1!) sv))]} :middleware (fn [sv] (if *throwing-handler-middleware?* (ex1!) sv))}
[(is (->> (sig! {:level :info, :when (ex1!)}) (throws? :ex-info "Ex1")) "`~filterable-expansion/allow` throws at call") [(is (->> (sig! {:level :info, :when (ex1!)}) (throws? :ex-info "Ex1")) "`~filterable-expansion/allow` throws at call")
(is (->> (sig! {:level :info, :inst (ex1!)}) (throws? :ex-info "Ex1")) "`~inst-form` throws at call") (is (->> (sig! {:level :info, :inst (ex1!)}) (throws? :ex-info "Ex1")) "`~inst-form` throws at call")
@ -339,7 +339,7 @@
(testing "Throwing call middleware" (testing "Throwing call middleware"
(reset-state!) (reset-state!)
[(is (true? (sig! {:level :info, :middleware [(fn [_] (ex1!))]}))) [(is (true? (sig! {:level :info, :middleware (fn [_] (ex1!))})))
(is (= @sv_ :nx)) (is (= @sv_ :nx))
(is (sm? @error_ {:handler-id :hid1, :error pex1?}))]) (is (sm? @error_ {:handler-id :hid1, :error pex1?}))])