Reitit
The Ancient Art of Data-Driven
tommi@metosin.fi
Clojure/North
Me(tosin)
Sinclair Spectrum in 1983
...
Co-founded Metosin 2012
20 developers, Tampere & Helsinki / Finland
Projects, Startups and Design
FP, mostly Clojure/Script
https://github.com/metosin
ClojuTRE 2019 (8th)
September 26-27th 2019, Helsinki
FP & Clojure, 300++ on last two years
Basic flights & hotels for speakers
Free Student & Diversity tickets
Call For Speakers Open
https://clojutre.org
This talk
Routing and dispatching
Reitit, the library
Reitit, the framework
Performance
Takeaways
Routing in Clojure/Script
Lot's of options: Ataraxy, Bide, Bidi, Compojure,
Keechma, Pedestal, Silk, Secretary, ...
Something we need in mostly all the (web)apps
Mapping from path -> match
Mapping from name -> match (reverse routing)
HTTP (both server & browser)
Messaging
Others
Dispatching in Clojure/Script
Here: composing end executing functionality in
the request-response pipeline
Common patterns
Middleware (Ring, nREPL, ..)
Interceptors (Pedestal, Re-Frame, ..)
Controllers (Keechma)
Promises
Sync & Async
New Routing Library?
More data-driven
Leveraging clojure.spec
Built for performance
Friendly & explicit API
Reach: browser, JVM, Node, Others
Both routing and dispatching
The Yellow (Hiccup) Castle of Routing?
Reitit, the Library
reitit-core
(c) https://www.artstation.com/kinixuki
Basics
reitit.core/router to create a router
Matching by path or by name (reverse routing)
Functions to browse the route trees
(defprotocol Router
(router-name [this])
(routes [this])
(compiled-routes [this])
(options [this])
(route-names [this])
(match-by-path [this path])
(match-by-name [this name] [this name path-params]))
(require '[reitit.core :as r])
(def router
(r/router
[["/ping" ::ping]
["/users/:id" ::user]]))
(r/match-by-path router "/ping")
;#Match{:template "/ping"
; :data {:name :user/ping}
; :result nil
; :path-params {}
; :path "/ping"}
(r/match-by-name router ::ping)
;#Match{:template "/ping"
; :data {:name :user/ping}
; :result nil
; :path-params {}
; :path "/ping"}
Route Syntax (~hiccup)
Paths are concatenated, route data meta-merged
(r/router
["/api" {:interceptors [::api]}
["/ping" ::ping]
["/admin" {:roles #{:admin}}
["/users" ::users]
["/db" {:interceptors [::db]
:roles ^:replace #{:db-admin}}]]]))
(r/router
[["/api/ping" {:interceptors [::api]
:name ::ping}]
["/api/admin/users" {:interceptors [::api]
:roles #{:admin}
:name ::users}]
["/api/admin/db" {:interceptors [::api ::db]
:roles #{:db-admin}}]])
Programming routes
nil s and nested sequences are flattened
(defn router [dev?]
(r/router
[["/command/"
(for [mutation [:olipa :kerran :avaruus]]
[(name mutation) mutation])]
(if dev? ["/dev-tools" :dev-tools])]))
(-> (router true) (r/routes))
;[["/command/olipa" {:name :olipa}]
; ["/command/kerran" {:name :kerran}]
; ["/command/avaruus" {:name :avaruus}]
; ["/dev-tools" {:name :dev-tools}]]
Composing routes
It's just data, merge trees
Single route tree allows to optimize the whole
Merging & nesting routers
Empty route fragments, e.g. :<> in Reagent
(-> (r/router
[["" {:interceptors [::cors]}
["/api" ::api]
["/ipa" ::ipa]]
["/ping" ::ping]])
(r/routes))
;[["/api" {:interceptors [::cors], :name ::api}]
; ["/ipa" {:interceptors [::cors], :name ::ipa}]
; ["/ping" {:name ::ping}]]
Route Conflict Resolution
Multiple sources (code/EDN/DB) for routes?
Are all routes still reachable?
Reitit fails-fast by default on path & name conflicts
(require '[reitit.core :as r])
(require '[reitit.dev.pretty :as pretty])
(r/router
[["/ping"]
["/:user-id/orders"]
["/bulk/:bulk-id"]
["/public/*path"]
["/:version/status"]]
{:exception pretty/exception})
Route Conflict Error
Community-driven Clojure
error formatter?
(c) https://www.artstation.com/kinixuki
What is in the route data?
Can be anything, the Router doesn't care.
Returned on successful Match
Can be queried from a Router
Build your own interpreters for the data
A Route First Architecture: Match -> Data -> React
(r/match-by-path router "/api/admin/db")
;#Match{:template "/api/admin/db",
; :data {:interceptors [::api ::db]
; :roles #{:db-admin}},
; :result nil,
; :path-params {},
; :path "/api/admin/db"}
Example use cases
Authorization via :roles
Frontend components via :view
Dispatching via :middleware and :interceptors
Stateful dispatching with :controllers
Coercion via :parameters and :coercion
Run requests in a server io-pool with :server/io
Deploy endpoints separately to :server/target
Frontend Example
https://router.vuejs.org/guide/essentials/nested-routes.html
(require '[reitit.frontend :as rf])
(require '[reitit.frontend.easy :as rfe])
(defn frontpage-view [match] ...)
(defn topics-view [match] ... (rf/nested-view match)
(defn topic-view [match] ...)
(def router
(rf/router
[["/" {:name :frontpage
:views [frontpage-view]}]
["/topics"
{:controllers [load-topics]
:views [topics-view]}
["" {:name :topics}]
["/:id"
{:name :topic
:parameters {:path {:id int?}}
:controllers [load-topic]
:views [topic-view]}]]]))
(rfe/start! router ...)
Route Data Validation
Route Data can be anything -> data spaghetti?
Leverage clojure.spec at router creation
Define and enforce route data specs
Fail-fast, missing, extra or misspelled keys
With help of spec-tools and spell-spec
Closed specs coming to Spec2 (TBD)
Invalid Route Data Example
(require '[reitit.spec :as spec])
(require '[clojure.spec.alpha :as s])
(s/def ::role #{:admin :user})
(s/def ::roles (s/coll-of ::role :into #{}))
(r/router
["/api/admin" {::roles #{:adminz}}]
{:validate spec/validate
:exception pretty/exception})
Route Data Error
Closed Functional Specs
Failing Fast
During static analysis (e.g. linter)
At compile-time (macros, defs)
At development-time (schema/spec annos)
At creation-time
At runtime
At runtime special condition
(c) https://www.artstation.com/kinixuki
Coercion (Déjà-vu)
Coercion is a process of transforming values between
domain (e.g. JSON->EDN, String->EDN)
Route data keys :parameters & :coercion
Utilities to apply coercion in reitit.coercion
Implementation for Schema & clojure.spec <-- !!!
(defprotocol Coercion
"Pluggable coercion protocol"
(-get-name [this])
(-get-options [this])
(-get-apidocs [this specification data])
(-compile-model [this model name])
(-open-model [this model])
(-encode-error [this error])
(-request-coercer [this type model])
(-response-coercer [this model]))
Reitit, the Framework
(c) https://www.artstation.com/kinixuki
Framework
You call the library, but the framework calls you
Reitit ships with multiple Routing Frameworks
reitit-ring Middleware-dispatch for Ring
reitit-http Async Interceptor-dispatch for http
reitit-pedestal Reitit Pedestal
reitit-frontend (Keechma-style) Controllers,
History & Fragment Router, helpers
metosin/reitit-ring
(c) https://www.artstation.com/kinixuki
Ring Routing
A separate lightweight module
Routing based on path and :request-method
Adds support for :middleware dispatch
chain is executed after a match
ring-handler to create a ring-compatible handler
Supports Both sync & async
Supports Both JVM & Node
No magic, no default middleware
Ring Application
(require '[reitit.ring :as ring])
(def app
(ring/ring-handler
(ring/router
["/api" {:middleware [wrap-api wrap-roles]}
["/ping" {:get ping-handler]}
["/users" {:middleware [db-middleware]
:roles #{:admin} ;; who reads this?
:get get-users
:post add-user}]])
(ring/create-default-handler)))
(app {:uri "/api/ping" :request-method :get})
; {:status 200, :body "pong"}
Accessing route data
Match and Router are injected into the request
Components can read these at request time and
do what ever they want, "Ad-hoc extensions"
Pattern used in Kekkonen and in Yada
(defn wrap-roles [handler]
;; roles injected via session-middleware
(fn [{:keys [roles] :as request}]
;; read the route-data at request-time
(let [required (-> request (ring/get-match) :data :roles)]
(if (and (seq required)
(not (set/subset? required roles)))
{:status 403, :body "forbidden"}
(handler request)))))
Middleware as data
Ring middleware are opaque functions
Reitit adds a first class values, Middleware records
Recursive IntoMiddleware protocol to expand to
Attach documentation, specs, requirements, ...
Can be used in place of middleware functions
Zero runtime penalty
(defn roles-middleware []
{:name ::roles-middleware
:description "Middleware to enforce roles"
:requires #{::session-middleware}
:spec (s/keys :opt-un [::roles])
:wrap wrap-roles})
Compiling middleware
Each middleware knows the endpoint it's mounted to
We can pass the route data in at router creation time
Big win for optimizing chains
(def roles-middleware
{:name ::roles-middleware
:description "Middleware to enforce roles"
:requires #{::session-middleware}
:spec (s/keys :opt-un [::roles])
:compile (fn [{required :roles} _]
;; unmount if there are no roles required
(if (seq required)
(fn [handler]
(fn [{:keys [roles] :as request}]
(if (not (set/subset? required roles))
{:status 403, :body "forbidden"}
(handler request))))))})
Partial Specs Example
"All routes under /account should require a role"
;; look ma, not part of request processing!
(def roles-defined
{:name ::roles-defined
:description "requires a ::role for the routes"
:spec (s/keys :req-un [::roles])})
["/api" {:middleware [roles-middleware]} ;; behavior
["/ping"] ;; unmounted
["/account" {:middleware [roles-defined]} ;; :roles mandatory
["/admin" {:roles #{:admin}}] ;; ok
["/user" {:roles #{:user}}] ;; ok
["/manager"]]] ;; fail!
Middleware chain as data
Each endpoint has it's own vector of middleware
Documents of what is in the chain
Chain can be manipulated at router creation time
Reordering
Completing
Interleaving
Interleaving a request diff console printer:
reitit.ring.middleware.dev/print-request-diffs
Data definitions (as data)
The core coercion for all (OpenAPI) paramerer &
response types ( :query , :body , header , :path etc)
Separerate Middleware to apply request & response
coercion and format coercion errors
Separate modules, spec coercion via spec-tools
["/plus/:y"
{:get {:parameters {:query {:x int?},
:path {:y int?}}
:responses {200 {:body {:total pos-int?}}}
:handler (fn [{:keys [parameters]}]
;; parameters are coerced
(let [x (-> parameters :query :x)
y (-> parameters :path :y)]
{:status 200
:body {:total (+ x y)}}))}}]
Everything as data.
(c) https://www.artstation.com/kinixuki
Interceptors
metosin/reitit-http
e.g. going async
(c) https://www.artstation.com/kinixuki
Going Async
Interceptors are much better fit for async
reitit-http uses the Interceptor model
Requires an Interceptor Executor
reitit-pedestal or reitit-sieppari
Sieppari supports core.async , Manifold and Promesa
Pedestal is more proven, supports core.async
Target Both JVM & Node (via Sieppari)
Http Application
(require '[reitit.http :as http])
(require '[reitit.interceptor.sieppari :as sieppari]
(def app
(http/ring-handler
(http/router
["/api" {:interceptors [api-interceptor
roles-interceptor]}
["/ping" ping-handler]
["/users" {:interceptors [db-interceptor]
:roles #{:admin}
:get get-users
:post add-user}]])
(ring/create-default-handler)
{:executor sieppari/executor}))
(app {:uri "/api/ping" :request-method :get})
; {:status 200, :body "pong"}
Example Interceptor
(defn coerce-request-interceptor
"Interceptor for pluggable request coercion.
Expects a :coercion of type `reitit.coercion/Coercion`
and :parameters from route data, otherwise does not mount."
[]
{:name ::coerce-request
:spec ::rs/parameters
:compile (fn [{:keys [coercion parameters]} opts]
(cond
;; no coercion, skip
(not coercion) nil
;; just coercion, don't mount
(not parameters) {}
;; mount
:else
(let [coercers (coercion/request-coercers coercion parameters opts)]
{:enter (fn [ctx]
(let [request (:request ctx)
coerced (coercion/coerce-request coercers request)
request (impl/fast-assoc request :parameters coerced)]
(assoc ctx :request request)))})))})
DEMO
(c) https://www.artstation.com/kinixuki
Route-driven frameworks
Routing and dispatching is separated, middleware (or
interceptors) are applied only after a match
Each endpoint has a unique dispatch chain
Each component can be compiled and optimized against
the endpoint at creation time
Components can define partial route data specs that
only effect the routes they are mounted to.
We get both Performance & Correctness
... this is Kinda Awesome.
(c) https://www.artstation.com/kinixuki
Performance
(c) https://www.artstation.com/kinixuki
Performance
how can we make compojure-api faster?
we moved from Clojure to GO because of perf
How fast are the current Clojure libraries?
How fast can we go with Java/Clojure?
Measuring Performance
Always measure
Both micro & macro benchmarks
In the end, the order of magnitude matters
Lot's of good tools, some favourites:
clojure.core/time
criterium
com.clojure-goes-fast/clj-async-profiler
com.clojure-goes-fast/clj-java-decompiler
https://github.com/wg/wrk
(def defaults {:keywords? true})
(time
(dotimes [_ 10000]
(merge defaults {})))
; "Elapsed time: 4.413803 msecs"
(require '[criterium.core :as cc])
(cc/quick-bench
(merge defaults {}))
; Evaluation count : 2691372 in 6 samples of 448562 calls.
; Execution time mean : 230.346208 ns
; Execution time std-deviation : 10.355077 ns
; Execution time lower quantile : 221.101397 ns ( 2.5%)
; Execution time upper quantile : 245.331388 ns (97.5%)
; Overhead used : 1.881561 ns
(require '[clj-async-profiler.core :as prof])
(prof/serve-files 8080) ;; serve the svgs here
(prof/profile
(dotimes [_ 40000000] ;; ~10sec period
(merge defaults {})))
Reitit performance
Designed group up to be performant
Perf suite to see how performance evolves
Measured against Clojure/JavaScript/GO Routers
Performance toolbox:
Optimized routing algorithms
Separation of creation & request time
The Usual Suspects
Routing algorithms
When a router is created, route tree is inspected
and a best possible routing algorith is chosen
No regexps, use linear-router as a last effort
lookup-router , single-static-path-router
trie-router , linear-router
mixed-router , quarantine-router
trie-router
For non-conflicting trees with wildcards
First insert data into Trie AST, then compile it into
fast lookup functions using a TrieCompiler
On JVM, backed by a fast Java-based Radix-trie
(require '[reitit.trie :as trie])
(-> [["/v2/whoami" 1]
["/v2/users/:user-id/datasets" 2]
["/v2/public/projects/:project-id/datasets" 3]
["/v1/public/topics/:topic" 4]
["/v1/users/:user-id/orgs/:org-id" 5]
["/v1/search/topics/:term" 6]
["/v1/users/:user-id/invitations" 7]
["/v1/users/:user-id/topics" 9]
["/v1/users/:user-id/bookmarks/followers" 10]
["/v2/datasets/:dataset-id" 11]
["/v1/orgs/:org-id/usage-stats" 12]
["/v1/orgs/:org-id/devices/:client-id" 13]
["/v1/messages/user/:user-id" 14]
["/v1/users/:user-id/devices" 15]
["/v1/public/users/:user-id" 16]
["/v1/orgs/:org-id/errors" 17]
["/v1/public/orgs/:org-id" 18]
["/v1/orgs/:org-id/invitations" 19]
["/v1/users/:user-id/device-errors" 22]]
(trie/insert)
(trie/compile)
(trie/pretty))
["/v"
[["1/"
[["users/" [:user-id ["/" [["device" [["-errors" 22]
["s" 15]]]
["orgs/" [:org-id 5]]
["bookmarks/followers" 10]
["invitations" 7]
["topics" 9]]]]]
["orgs/" [:org-id ["/" [["devices/" [:client-id 13]]
["usage-stats" 12]
["invitations" 19]
["errors" 17]]]]]
["public/" [["topics/" [:topic 4]]
["users/" [:user-id 16]]
["orgs/" [:org-id 18]]]]
["search/topics/" [:term 6]]
["messages/user/" [:user-id 14]]]]
["2/"
[["public/projects/" [:project-id ["/datasets" 3]]]
["users/" [:user-id ["/datasets" 2]]]
["datasets/" [:dataset-id 11]]
["whoami" 1]]]]]
Optimization Log
160ns (httprouter/GO)
3000ns (clojure, interpreted)
... (clueless poking)
990ns (clojure-trie)
830ns (faster decode params)
560ns (java-segment-router)
490ns (java-segment-router, no injects)
440ns (java-segment-router, no injects, single-wild-optimization)
305ns (trie-router, no injects)
281ns (trie-router, no injects, optimized)
277ns (trie-router, no injects, switch-case)
273ns (trie-router, no injects, direct-data)
256ns (trie-router, pre-defined parameters)
237ns (trie-router, single-sweep wild-params)
191ns (trie-router, record parameters)
Initial Segment Trie
(defn- segment
([] (segment {} #{} nil nil))
([children wilds catch-all match]
(let [children' (impl/fast-map children)
wilds? (seq wilds)]
^{:type ::segment}
(reify
Segment
(-insert [_ [p & ps] d]
(if-not p
(segment children wilds catch-all d)
(let [[w c] ((juxt impl/wild-param impl/catch-all-param) p)
wilds (if w (conj wilds w) wilds)
catch-all (or c catch-all)
children (update children (or w c p) #(-insert (or % (segment)) ps d))]
(segment children wilds catch-all match))))
(-lookup [_ [p & ps] path-params]
(if (nil? p)
(when match (assoc match :path-params path-params))
(or (-lookup (impl/fast-get children' p) ps path-params)
(if (and wilds? (not (str/blank? p))) (some #(-lookup (impl/fast-get children' %) ps
(if catch-all (-catch-all children' catch-all path-params p ps)))))))))
Flamegraph it away
Optimizing the Java Trie
Java Trie
Set of matchers defined by the TrieCompiler
Order of magnitude faster than the original impl
@Override
public Match match(int i, int max, char[] path) {
boolean hasPercent = false;
boolean hasPlus = false;
if (i < max && path[i] != end) {
int stop = max;
for (int j = i; j < max; j++) {
final char c = path[j];
hasPercent = hasPercent || c == '%';
hasPlus = hasPlus || c == '+';
if (c == end) {
stop = j;
break;
}
}
final Match m = child.match(stop, max, path);
if (m != null) {
m.params = m.params.assoc(key, decode(new String(path, i, stop - i), hasPercent, hasPlus));
}
return m;
}
return null;
}
The Usual Suspects
Persistent Data Structures -> Records, Reify
Multimethods -> Protocols
Map Destructuring -> Manually
Unroll recursive functions ( assoc-in , ...)
Too generic functions ( walk , zip , ...)
Dynamic Binding
Manual inlining
Regexps
The Numbers
(c) https://www.artstation.com/kinixuki
RESTful api test
50+ routes, mostly wildcards
Reitit is orders of magnitude faster
220ns vs 22000ns, actually matters for busy sites
Looking out of the box
https://github.com/julienschmidt/httprouter
One of the fastest router in GO
(and source of many of the optimizations in reitit)
In Github api test, Reitit is ~40% slower
That's good! Still work to do.
Web Server Performance in
Clojure?
(c) https://www.artstation.com/kinixuki
Current Status
Most Clojure libraries don't even try to be fast
And that's totally ok for most apps
Compojure+ring-defaults vs reitit, with same response
headers, simple json echo
;; 10198tps
;; http :3000/api/ping
;; wrk -d ${DURATION:="30s"} http://127.0.0.1:3000/api/ping
(http/start-server defaults-app {:port 3000})
;; 48084tps
;; http :3002/api/ping
;; wrk -d ${DURATION:="30s"} http://127.0.0.1:3002/api/ping
(http/start-server reitit-app {:port 3002})
=> that's 5x more requests per sec.
TechEmpower Benchmark
TechEmpower Benchmark
TechEmpower Benchmark
Web Stacks
We know Clojure is a great tool for building web stuff
We can build a REALLY fast server stack for Clojure
aleph or immutant-nio as web server (nio, zero-copy)
reitit -based routing
Fast formatters like jsonista for JSON
next.jdbc (or porsas ) for database access
Good tools for async values & streams
lot's of other important components
Simple tools on top, the (coastal) castles?
Clojure Web & Data Next?
We have Ring, Pedestal, Yada & friends
We have nice templates & examples
Ring2 Spec? Spec for interceptors?
New performant reference architecture?
Bigger building blocks for rapid prototypes?
Making noice that Clojure is kinda awesome?
Community built Error Formatter?
Data-driven tools & inventories?
clojure.spec as data?
(c) https://www.artstation.com/kinixuki
Reitit bubbin' under
Support for OpenAPI3
JSON Schema validation
Spec2 support (when it's out)
Developer UI with remote debugger
More Batteries & Guides (frontend)
(Help make next.jdbc java-fast)
Sieppari.next
(c) https://www.artstation.com/kinixuki
Wrap-up
Reitit is a new routing library & framework
Embrace data-driven design, all the way down
Clojure is a Dynamic Language -> fail fast
clojure.spec to validate & transform data
Understand performance, test on your libraries
We can build beautiful & fast things with Clojure
Try out https://github.com/metosin/reitit
Chatting on #reitit in Slack
(c) https://www.artstation.com/kinixuki
 
 
 
 
Thanks.
@ikitommi
 
 
background artwork (c) Anna Dvorackova
https://www.artstation.com/kinixuki
(c) https://www.artstation.com/kinixuki

Reitit - Clojure/North 2019

  • 1.
    Reitit The Ancient Artof Data-Driven tommi@metosin.fi Clojure/North
  • 2.
    Me(tosin) Sinclair Spectrum in1983 ... Co-founded Metosin 2012 20 developers, Tampere & Helsinki / Finland Projects, Startups and Design FP, mostly Clojure/Script https://github.com/metosin
  • 3.
    ClojuTRE 2019 (8th) September26-27th 2019, Helsinki FP & Clojure, 300++ on last two years Basic flights & hotels for speakers Free Student & Diversity tickets Call For Speakers Open https://clojutre.org
  • 4.
    This talk Routing anddispatching Reitit, the library Reitit, the framework Performance Takeaways
  • 5.
    Routing in Clojure/Script Lot'sof options: Ataraxy, Bide, Bidi, Compojure, Keechma, Pedestal, Silk, Secretary, ... Something we need in mostly all the (web)apps Mapping from path -> match Mapping from name -> match (reverse routing) HTTP (both server & browser) Messaging Others
  • 6.
    Dispatching in Clojure/Script Here:composing end executing functionality in the request-response pipeline Common patterns Middleware (Ring, nREPL, ..) Interceptors (Pedestal, Re-Frame, ..) Controllers (Keechma) Promises Sync & Async
  • 7.
    New Routing Library? Moredata-driven Leveraging clojure.spec Built for performance Friendly & explicit API Reach: browser, JVM, Node, Others Both routing and dispatching The Yellow (Hiccup) Castle of Routing?
  • 9.
    Reitit, the Library reitit-core (c)https://www.artstation.com/kinixuki
  • 10.
    Basics reitit.core/router to createa router Matching by path or by name (reverse routing) Functions to browse the route trees (defprotocol Router (router-name [this]) (routes [this]) (compiled-routes [this]) (options [this]) (route-names [this]) (match-by-path [this path]) (match-by-name [this name] [this name path-params]))
  • 11.
    (require '[reitit.core :asr]) (def router (r/router [["/ping" ::ping] ["/users/:id" ::user]])) (r/match-by-path router "/ping") ;#Match{:template "/ping" ; :data {:name :user/ping} ; :result nil ; :path-params {} ; :path "/ping"} (r/match-by-name router ::ping) ;#Match{:template "/ping" ; :data {:name :user/ping} ; :result nil ; :path-params {} ; :path "/ping"}
  • 12.
    Route Syntax (~hiccup) Pathsare concatenated, route data meta-merged (r/router ["/api" {:interceptors [::api]} ["/ping" ::ping] ["/admin" {:roles #{:admin}} ["/users" ::users] ["/db" {:interceptors [::db] :roles ^:replace #{:db-admin}}]]])) (r/router [["/api/ping" {:interceptors [::api] :name ::ping}] ["/api/admin/users" {:interceptors [::api] :roles #{:admin} :name ::users}] ["/api/admin/db" {:interceptors [::api ::db] :roles #{:db-admin}}]])
  • 13.
    Programming routes nil sand nested sequences are flattened (defn router [dev?] (r/router [["/command/" (for [mutation [:olipa :kerran :avaruus]] [(name mutation) mutation])] (if dev? ["/dev-tools" :dev-tools])])) (-> (router true) (r/routes)) ;[["/command/olipa" {:name :olipa}] ; ["/command/kerran" {:name :kerran}] ; ["/command/avaruus" {:name :avaruus}] ; ["/dev-tools" {:name :dev-tools}]]
  • 14.
    Composing routes It's justdata, merge trees Single route tree allows to optimize the whole Merging & nesting routers Empty route fragments, e.g. :<> in Reagent (-> (r/router [["" {:interceptors [::cors]} ["/api" ::api] ["/ipa" ::ipa]] ["/ping" ::ping]]) (r/routes)) ;[["/api" {:interceptors [::cors], :name ::api}] ; ["/ipa" {:interceptors [::cors], :name ::ipa}] ; ["/ping" {:name ::ping}]]
  • 15.
    Route Conflict Resolution Multiplesources (code/EDN/DB) for routes? Are all routes still reachable? Reitit fails-fast by default on path & name conflicts (require '[reitit.core :as r]) (require '[reitit.dev.pretty :as pretty]) (r/router [["/ping"] ["/:user-id/orders"] ["/bulk/:bulk-id"] ["/public/*path"] ["/:version/status"]] {:exception pretty/exception})
  • 16.
  • 17.
    Community-driven Clojure error formatter? (c)https://www.artstation.com/kinixuki
  • 18.
    What is inthe route data? Can be anything, the Router doesn't care. Returned on successful Match Can be queried from a Router Build your own interpreters for the data A Route First Architecture: Match -> Data -> React (r/match-by-path router "/api/admin/db") ;#Match{:template "/api/admin/db", ; :data {:interceptors [::api ::db] ; :roles #{:db-admin}}, ; :result nil, ; :path-params {}, ; :path "/api/admin/db"}
  • 19.
    Example use cases Authorizationvia :roles Frontend components via :view Dispatching via :middleware and :interceptors Stateful dispatching with :controllers Coercion via :parameters and :coercion Run requests in a server io-pool with :server/io Deploy endpoints separately to :server/target
  • 20.
    Frontend Example https://router.vuejs.org/guide/essentials/nested-routes.html (require '[reitit.frontend:as rf]) (require '[reitit.frontend.easy :as rfe]) (defn frontpage-view [match] ...) (defn topics-view [match] ... (rf/nested-view match) (defn topic-view [match] ...) (def router (rf/router [["/" {:name :frontpage :views [frontpage-view]}] ["/topics" {:controllers [load-topics] :views [topics-view]} ["" {:name :topics}] ["/:id" {:name :topic :parameters {:path {:id int?}} :controllers [load-topic] :views [topic-view]}]]])) (rfe/start! router ...)
  • 21.
    Route Data Validation RouteData can be anything -> data spaghetti? Leverage clojure.spec at router creation Define and enforce route data specs Fail-fast, missing, extra or misspelled keys With help of spec-tools and spell-spec Closed specs coming to Spec2 (TBD)
  • 22.
    Invalid Route DataExample (require '[reitit.spec :as spec]) (require '[clojure.spec.alpha :as s]) (s/def ::role #{:admin :user}) (s/def ::roles (s/coll-of ::role :into #{})) (r/router ["/api/admin" {::roles #{:adminz}}] {:validate spec/validate :exception pretty/exception})
  • 23.
  • 24.
  • 25.
    Failing Fast During staticanalysis (e.g. linter) At compile-time (macros, defs) At development-time (schema/spec annos) At creation-time At runtime At runtime special condition (c) https://www.artstation.com/kinixuki
  • 26.
    Coercion (Déjà-vu) Coercion isa process of transforming values between domain (e.g. JSON->EDN, String->EDN) Route data keys :parameters & :coercion Utilities to apply coercion in reitit.coercion Implementation for Schema & clojure.spec <-- !!! (defprotocol Coercion "Pluggable coercion protocol" (-get-name [this]) (-get-options [this]) (-get-apidocs [this specification data]) (-compile-model [this model name]) (-open-model [this model]) (-encode-error [this error]) (-request-coercer [this type model]) (-response-coercer [this model]))
  • 27.
    Reitit, the Framework (c)https://www.artstation.com/kinixuki
  • 28.
    Framework You call thelibrary, but the framework calls you Reitit ships with multiple Routing Frameworks reitit-ring Middleware-dispatch for Ring reitit-http Async Interceptor-dispatch for http reitit-pedestal Reitit Pedestal reitit-frontend (Keechma-style) Controllers, History & Fragment Router, helpers
  • 29.
  • 30.
    Ring Routing A separatelightweight module Routing based on path and :request-method Adds support for :middleware dispatch chain is executed after a match ring-handler to create a ring-compatible handler Supports Both sync & async Supports Both JVM & Node No magic, no default middleware
  • 31.
    Ring Application (require '[reitit.ring:as ring]) (def app (ring/ring-handler (ring/router ["/api" {:middleware [wrap-api wrap-roles]} ["/ping" {:get ping-handler]} ["/users" {:middleware [db-middleware] :roles #{:admin} ;; who reads this? :get get-users :post add-user}]]) (ring/create-default-handler))) (app {:uri "/api/ping" :request-method :get}) ; {:status 200, :body "pong"}
  • 32.
    Accessing route data Matchand Router are injected into the request Components can read these at request time and do what ever they want, "Ad-hoc extensions" Pattern used in Kekkonen and in Yada (defn wrap-roles [handler] ;; roles injected via session-middleware (fn [{:keys [roles] :as request}] ;; read the route-data at request-time (let [required (-> request (ring/get-match) :data :roles)] (if (and (seq required) (not (set/subset? required roles))) {:status 403, :body "forbidden"} (handler request)))))
  • 33.
    Middleware as data Ringmiddleware are opaque functions Reitit adds a first class values, Middleware records Recursive IntoMiddleware protocol to expand to Attach documentation, specs, requirements, ... Can be used in place of middleware functions Zero runtime penalty (defn roles-middleware [] {:name ::roles-middleware :description "Middleware to enforce roles" :requires #{::session-middleware} :spec (s/keys :opt-un [::roles]) :wrap wrap-roles})
  • 34.
    Compiling middleware Each middlewareknows the endpoint it's mounted to We can pass the route data in at router creation time Big win for optimizing chains (def roles-middleware {:name ::roles-middleware :description "Middleware to enforce roles" :requires #{::session-middleware} :spec (s/keys :opt-un [::roles]) :compile (fn [{required :roles} _] ;; unmount if there are no roles required (if (seq required) (fn [handler] (fn [{:keys [roles] :as request}] (if (not (set/subset? required roles)) {:status 403, :body "forbidden"} (handler request))))))})
  • 35.
    Partial Specs Example "Allroutes under /account should require a role" ;; look ma, not part of request processing! (def roles-defined {:name ::roles-defined :description "requires a ::role for the routes" :spec (s/keys :req-un [::roles])}) ["/api" {:middleware [roles-middleware]} ;; behavior ["/ping"] ;; unmounted ["/account" {:middleware [roles-defined]} ;; :roles mandatory ["/admin" {:roles #{:admin}}] ;; ok ["/user" {:roles #{:user}}] ;; ok ["/manager"]]] ;; fail!
  • 36.
    Middleware chain asdata Each endpoint has it's own vector of middleware Documents of what is in the chain Chain can be manipulated at router creation time Reordering Completing Interleaving Interleaving a request diff console printer: reitit.ring.middleware.dev/print-request-diffs
  • 38.
    Data definitions (asdata) The core coercion for all (OpenAPI) paramerer & response types ( :query , :body , header , :path etc) Separerate Middleware to apply request & response coercion and format coercion errors Separate modules, spec coercion via spec-tools ["/plus/:y" {:get {:parameters {:query {:x int?}, :path {:y int?}} :responses {200 {:body {:total pos-int?}}} :handler (fn [{:keys [parameters]}] ;; parameters are coerced (let [x (-> parameters :query :x) y (-> parameters :path :y)] {:status 200 :body {:total (+ x y)}}))}}]
  • 39.
    Everything as data. (c)https://www.artstation.com/kinixuki
  • 40.
    Interceptors metosin/reitit-http e.g. going async (c)https://www.artstation.com/kinixuki
  • 41.
    Going Async Interceptors aremuch better fit for async reitit-http uses the Interceptor model Requires an Interceptor Executor reitit-pedestal or reitit-sieppari Sieppari supports core.async , Manifold and Promesa Pedestal is more proven, supports core.async Target Both JVM & Node (via Sieppari)
  • 42.
    Http Application (require '[reitit.http:as http]) (require '[reitit.interceptor.sieppari :as sieppari] (def app (http/ring-handler (http/router ["/api" {:interceptors [api-interceptor roles-interceptor]} ["/ping" ping-handler] ["/users" {:interceptors [db-interceptor] :roles #{:admin} :get get-users :post add-user}]]) (ring/create-default-handler) {:executor sieppari/executor})) (app {:uri "/api/ping" :request-method :get}) ; {:status 200, :body "pong"}
  • 43.
    Example Interceptor (defn coerce-request-interceptor "Interceptorfor pluggable request coercion. Expects a :coercion of type `reitit.coercion/Coercion` and :parameters from route data, otherwise does not mount." [] {:name ::coerce-request :spec ::rs/parameters :compile (fn [{:keys [coercion parameters]} opts] (cond ;; no coercion, skip (not coercion) nil ;; just coercion, don't mount (not parameters) {} ;; mount :else (let [coercers (coercion/request-coercers coercion parameters opts)] {:enter (fn [ctx] (let [request (:request ctx) coerced (coercion/coerce-request coercers request) request (impl/fast-assoc request :parameters coerced)] (assoc ctx :request request)))})))})
  • 44.
  • 45.
    Route-driven frameworks Routing anddispatching is separated, middleware (or interceptors) are applied only after a match Each endpoint has a unique dispatch chain Each component can be compiled and optimized against the endpoint at creation time Components can define partial route data specs that only effect the routes they are mounted to. We get both Performance & Correctness ... this is Kinda Awesome. (c) https://www.artstation.com/kinixuki
  • 46.
  • 47.
    Performance how can wemake compojure-api faster? we moved from Clojure to GO because of perf How fast are the current Clojure libraries? How fast can we go with Java/Clojure?
  • 48.
    Measuring Performance Always measure Bothmicro & macro benchmarks In the end, the order of magnitude matters Lot's of good tools, some favourites: clojure.core/time criterium com.clojure-goes-fast/clj-async-profiler com.clojure-goes-fast/clj-java-decompiler https://github.com/wg/wrk
  • 49.
    (def defaults {:keywords?true}) (time (dotimes [_ 10000] (merge defaults {}))) ; "Elapsed time: 4.413803 msecs" (require '[criterium.core :as cc]) (cc/quick-bench (merge defaults {})) ; Evaluation count : 2691372 in 6 samples of 448562 calls. ; Execution time mean : 230.346208 ns ; Execution time std-deviation : 10.355077 ns ; Execution time lower quantile : 221.101397 ns ( 2.5%) ; Execution time upper quantile : 245.331388 ns (97.5%) ; Overhead used : 1.881561 ns
  • 50.
    (require '[clj-async-profiler.core :asprof]) (prof/serve-files 8080) ;; serve the svgs here (prof/profile (dotimes [_ 40000000] ;; ~10sec period (merge defaults {})))
  • 51.
    Reitit performance Designed groupup to be performant Perf suite to see how performance evolves Measured against Clojure/JavaScript/GO Routers Performance toolbox: Optimized routing algorithms Separation of creation & request time The Usual Suspects
  • 52.
    Routing algorithms When arouter is created, route tree is inspected and a best possible routing algorith is chosen No regexps, use linear-router as a last effort lookup-router , single-static-path-router trie-router , linear-router mixed-router , quarantine-router
  • 53.
    trie-router For non-conflicting treeswith wildcards First insert data into Trie AST, then compile it into fast lookup functions using a TrieCompiler On JVM, backed by a fast Java-based Radix-trie
  • 54.
    (require '[reitit.trie :astrie]) (-> [["/v2/whoami" 1] ["/v2/users/:user-id/datasets" 2] ["/v2/public/projects/:project-id/datasets" 3] ["/v1/public/topics/:topic" 4] ["/v1/users/:user-id/orgs/:org-id" 5] ["/v1/search/topics/:term" 6] ["/v1/users/:user-id/invitations" 7] ["/v1/users/:user-id/topics" 9] ["/v1/users/:user-id/bookmarks/followers" 10] ["/v2/datasets/:dataset-id" 11] ["/v1/orgs/:org-id/usage-stats" 12] ["/v1/orgs/:org-id/devices/:client-id" 13] ["/v1/messages/user/:user-id" 14] ["/v1/users/:user-id/devices" 15] ["/v1/public/users/:user-id" 16] ["/v1/orgs/:org-id/errors" 17] ["/v1/public/orgs/:org-id" 18] ["/v1/orgs/:org-id/invitations" 19] ["/v1/users/:user-id/device-errors" 22]] (trie/insert) (trie/compile) (trie/pretty))
  • 55.
    ["/v" [["1/" [["users/" [:user-id ["/"[["device" [["-errors" 22] ["s" 15]]] ["orgs/" [:org-id 5]] ["bookmarks/followers" 10] ["invitations" 7] ["topics" 9]]]]] ["orgs/" [:org-id ["/" [["devices/" [:client-id 13]] ["usage-stats" 12] ["invitations" 19] ["errors" 17]]]]] ["public/" [["topics/" [:topic 4]] ["users/" [:user-id 16]] ["orgs/" [:org-id 18]]]] ["search/topics/" [:term 6]] ["messages/user/" [:user-id 14]]]] ["2/" [["public/projects/" [:project-id ["/datasets" 3]]] ["users/" [:user-id ["/datasets" 2]]] ["datasets/" [:dataset-id 11]] ["whoami" 1]]]]]
  • 56.
    Optimization Log 160ns (httprouter/GO) 3000ns(clojure, interpreted) ... (clueless poking) 990ns (clojure-trie) 830ns (faster decode params) 560ns (java-segment-router) 490ns (java-segment-router, no injects) 440ns (java-segment-router, no injects, single-wild-optimization) 305ns (trie-router, no injects) 281ns (trie-router, no injects, optimized) 277ns (trie-router, no injects, switch-case) 273ns (trie-router, no injects, direct-data) 256ns (trie-router, pre-defined parameters) 237ns (trie-router, single-sweep wild-params) 191ns (trie-router, record parameters)
  • 57.
    Initial Segment Trie (defn-segment ([] (segment {} #{} nil nil)) ([children wilds catch-all match] (let [children' (impl/fast-map children) wilds? (seq wilds)] ^{:type ::segment} (reify Segment (-insert [_ [p & ps] d] (if-not p (segment children wilds catch-all d) (let [[w c] ((juxt impl/wild-param impl/catch-all-param) p) wilds (if w (conj wilds w) wilds) catch-all (or c catch-all) children (update children (or w c p) #(-insert (or % (segment)) ps d))] (segment children wilds catch-all match)))) (-lookup [_ [p & ps] path-params] (if (nil? p) (when match (assoc match :path-params path-params)) (or (-lookup (impl/fast-get children' p) ps path-params) (if (and wilds? (not (str/blank? p))) (some #(-lookup (impl/fast-get children' %) ps (if catch-all (-catch-all children' catch-all path-params p ps)))))))))
  • 58.
  • 59.
  • 60.
    Java Trie Set ofmatchers defined by the TrieCompiler Order of magnitude faster than the original impl @Override public Match match(int i, int max, char[] path) { boolean hasPercent = false; boolean hasPlus = false; if (i < max && path[i] != end) { int stop = max; for (int j = i; j < max; j++) { final char c = path[j]; hasPercent = hasPercent || c == '%'; hasPlus = hasPlus || c == '+'; if (c == end) { stop = j; break; } } final Match m = child.match(stop, max, path); if (m != null) { m.params = m.params.assoc(key, decode(new String(path, i, stop - i), hasPercent, hasPlus)); } return m; } return null; }
  • 61.
    The Usual Suspects PersistentData Structures -> Records, Reify Multimethods -> Protocols Map Destructuring -> Manually Unroll recursive functions ( assoc-in , ...) Too generic functions ( walk , zip , ...) Dynamic Binding Manual inlining Regexps
  • 62.
  • 63.
    RESTful api test 50+routes, mostly wildcards Reitit is orders of magnitude faster 220ns vs 22000ns, actually matters for busy sites
  • 65.
    Looking out ofthe box https://github.com/julienschmidt/httprouter One of the fastest router in GO (and source of many of the optimizations in reitit) In Github api test, Reitit is ~40% slower That's good! Still work to do.
  • 66.
    Web Server Performancein Clojure? (c) https://www.artstation.com/kinixuki
  • 67.
    Current Status Most Clojurelibraries don't even try to be fast And that's totally ok for most apps Compojure+ring-defaults vs reitit, with same response headers, simple json echo ;; 10198tps ;; http :3000/api/ping ;; wrk -d ${DURATION:="30s"} http://127.0.0.1:3000/api/ping (http/start-server defaults-app {:port 3000}) ;; 48084tps ;; http :3002/api/ping ;; wrk -d ${DURATION:="30s"} http://127.0.0.1:3002/api/ping (http/start-server reitit-app {:port 3002}) => that's 5x more requests per sec.
  • 68.
  • 69.
  • 70.
  • 71.
    Web Stacks We knowClojure is a great tool for building web stuff We can build a REALLY fast server stack for Clojure aleph or immutant-nio as web server (nio, zero-copy) reitit -based routing Fast formatters like jsonista for JSON next.jdbc (or porsas ) for database access Good tools for async values & streams lot's of other important components Simple tools on top, the (coastal) castles?
  • 72.
    Clojure Web &Data Next? We have Ring, Pedestal, Yada & friends We have nice templates & examples Ring2 Spec? Spec for interceptors? New performant reference architecture? Bigger building blocks for rapid prototypes? Making noice that Clojure is kinda awesome? Community built Error Formatter? Data-driven tools & inventories? clojure.spec as data?
  • 73.
  • 74.
    Reitit bubbin' under Supportfor OpenAPI3 JSON Schema validation Spec2 support (when it's out) Developer UI with remote debugger More Batteries & Guides (frontend) (Help make next.jdbc java-fast) Sieppari.next (c) https://www.artstation.com/kinixuki
  • 75.
    Wrap-up Reitit is anew routing library & framework Embrace data-driven design, all the way down Clojure is a Dynamic Language -> fail fast clojure.spec to validate & transform data Understand performance, test on your libraries We can build beautiful & fast things with Clojure Try out https://github.com/metosin/reitit Chatting on #reitit in Slack (c) https://www.artstation.com/kinixuki
  • 76.
            Thanks. @ikitommi     background artwork (c)Anna Dvorackova https://www.artstation.com/kinixuki (c) https://www.artstation.com/kinixuki