Browse Source

Update Clojure/Aleph (#7308)

Arnaud Geiser 3 years ago
parent
commit
24e7312120

+ 21 - 13
frameworks/Clojure/aleph/README.md

@@ -1,24 +1,32 @@
-# Compojure Benchmarking Test
+# Aleph Benchmarking Test
 
-This is the [Aleph](https://github.com/ztellman/aleph) portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
-
-### JSON Encoding Test
-
-* [JSON test source](hello/src/hello/handler.clj)
+This is the [Aleph](https://github.com/clj-commons/aleph) portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
 
 ## Infrastructure Software Versions
-The dependencies are documented in [project.clj](hello/project.clj),
+The dependencies are documented in [project.clj](project.clj),
 but the main ones are:
 
-* [Aleph 0.4.5-alpha6](https://github.com/ztellman/aleph)
-* [Clojure 1.9.0](http://clojure.org/)
-* [metosin/jsonista 0.2.0](https://github.com/metosin/jsonista), which in turn uses [Jackson](http://jackson.codehaus.org/)
+* [Aleph 0.4.7](https://github.com/clj-commons/aleph)
+* [Clojure 1.11.0](http://clojure.org/)
+* [metosin/jsonista 0.3.5](https://github.com/metosin/jsonista), which in turn uses [Jackson](http://jackson.codehaus.org/)
+* [hiccup 1.0.5](https://github.com/weavejester/hiccup)
+* [porsas 0.0.1-alpha14](https://github.com/arnaudgeiser/porsas)
 
 ## Test URLs
 ### JSON Encoding Test
+`http://localhost:8080/json`
+
+### Single Query Test
+`http://localhost:8080/db`
+
+### Multiple Query Test
+`http://localhost:8080/queries?queries=number`
 
-http://localhost/json
+### Fortune Test
+`http://localhost:8080/fortunes`
 
-### Plaintext Test
+### Database Updates
+`http://localhost:8080/updates?queries=number`
 
-http://localhost/plaintext
+### Plaintext
+`http://localhost:8080/plaintext`

+ 13 - 2
frameworks/Clojure/aleph/aleph.dockerfile

@@ -1,9 +1,20 @@
-FROM clojure:lein-2.8.1
+FROM clojure:openjdk-17-lein-2.9.8
 WORKDIR /aleph
 COPY src src
 COPY project.clj project.clj
 RUN lein uberjar
 
+# HTTP server
 EXPOSE 8080
+# async-profiler HTTP-server
+EXPOSE 8081
+# JMX port
+EXPOSE 9999
 
-CMD ["java", "-server", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AggressiveOpts", "-jar", "target/hello-aleph-standalone.jar"]
+RUN apt update -y
+RUN apt install perl -y
+
+CMD ["java", "-server", "-Xms2G", "-Xmx2G", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-Dvertx.disableMetrics=true", "-Dvertx.threadChecks=false", "-Dvertx.disableContextTimings=true", "-Dvertx.disableTCCL=true", "-Djava.net.preferIPv4Stack=true", "-jar", "target/hello-aleph-standalone.jar"]
+
+# To enable JMX and async-profiler
+#CMD ["java", "-XX:+UnlockDiagnosticVMOptions", "-XX:+DebugNonSafepoints", "-Djdk.attach.allowAttachSelf", "-Dcom.sun.management.jmxremote=true", "-Djava.rmi.server.hostname=0.0.0.0","-Dcom.sun.management.jmxremote.rmi.port=9999" ,"-Dcom.sun.management.jmxremote.port=9999", "-Dcom.sun.management.jmxremote.ssl=false", "-Dcom.sun.management.jmxremote.authenticate=false", "-server", "-Xms2G", "-Xmx2G", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-Dvertx.disableMetrics=true", "-Dvertx.threadChecks=false", "-Dvertx.disableContextTimings=true", "-Dvertx.disableTCCL=true", "-Djava.net.preferIPv4Stack=true", "-jar", "target/hello-aleph-standalone.jar"]

+ 5 - 1
frameworks/Clojure/aleph/benchmark_config.json

@@ -4,10 +4,14 @@
     "default": {
       "json_url": "/json",
       "plaintext_url": "/plaintext",
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
+      "update_url": "/updates?queries=",
+      "fortune_url": "/fortunes",
       "port": 8080,
       "approach": "Realistic",
       "classification": "Micro",
-      "database": "None",
+      "database": "Postgres",
       "framework": "aleph",
       "language": "Clojure",
       "flavor": "None",

+ 5 - 1
frameworks/Clojure/aleph/config.toml

@@ -4,9 +4,13 @@ name = "aleph"
 [main]
 urls.plaintext = "/plaintext"
 urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
 approach = "Realistic"
 classification = "Micro"
-database = "None"
+database = "Postgres"
 database_os = "Linux"
 os = "Linux"
 orm = "Raw"

+ 20 - 6
frameworks/Clojure/aleph/project.clj

@@ -1,10 +1,24 @@
 (defproject hello "aleph"
   :description "JSON/plaintext tests"
-  :dependencies [[org.clojure/clojure "1.9.0"]
-                 [clj-tuple "0.2.2"]
-                 [org.clojure/tools.cli "0.3.7"]
-                 [aleph "0.4.5-alpha6"]
-                 [javax.xml.bind/jaxb-api "2.3.0"]
-                 [metosin/jsonista "0.2.0"]]
+  :dependencies [[org.clojure/clojure "1.11.0"]
+                 [aleph "0.4.7"]
+                 [metosin/jsonista "0.3.5"]
+                 [hiccup "1.0.5"]
+                 [io.netty/netty-transport-native-epoll "4.1.65.Final" :classifier "linux-x86_64"]
+                 [com.github.arnaudgeiser/porsas "0.0.1-alpha14"
+                  :exclusions [io.netty/netty-codec-dns
+                               io.netty/netty-codec
+                               io.netty/netty-buffer
+                               io.netty/netty-common
+                               io.netty/netty-codec-http
+                               io.netty/netty-codec-http2
+                               io.netty/netty-codec-socks
+                               io.netty/netty-handler
+                               io.netty/netty-handler-proxy
+                               io.netty/netty-transport
+                               io.netty/netty-resolver-dns
+                               io.netty/netty-resolver]]
+                 [com.clojure-goes-fast/clj-async-profiler "0.5.1"]]
   :main hello.handler
+  :jvm-opts ^:replace ["-Dclojure.compiler.direct-linking=true"]
   :aot :all)

+ 173 - 30
frameworks/Clojure/aleph/src/hello/handler.clj

@@ -1,45 +1,188 @@
 (ns hello.handler
   (:require
-    [byte-streams :as bs]
-    [clojure.tools.cli :as cli]
-    [aleph.http :as http]
-    [jsonista.core :as json]
-    [clj-tuple :as t])
+   [aleph.http              :as http]
+   [aleph.netty             :as netty]
+   [byte-streams            :as bs]
+   [clj-async-profiler.core :as prof]
+   [hiccup.page             :as hp]
+   [hiccup.util             :as hu]
+   [jsonista.core           :as json]
+   [manifold.deferred       :as d]
+   [porsas.async            :as async])
+  (:import (clojure.lang IDeref)
+           (io.netty.channel ChannelOption)
+           (io.netty.buffer PooledByteBufAllocator)
+           (java.util.function Supplier)
+           (java.util.concurrent ThreadLocalRandom)
+           (porsas.async Context))
   (:gen-class))
 
 (def plaintext-response
-  (t/hash-map
-    :status 200
-    :headers (t/hash-map "content-type" "text/plain; charset=utf-8")
-    :body (bs/to-byte-array "Hello, World!")))
+  {:status 200
+   :headers {"Content-Type" "text/plain"}
+   :body (bs/to-byte-array "Hello, World!")})
 
 (def json-response
-  (t/hash-map
-    :status 200
-    :headers (t/hash-map "content-type" "application/json")))
+  {:status 200
+   :headers {"Content-Type" "application/json"}})
 
-(defn handler [req]
+(def html-response
+  {:status 200
+   :headers {"Content-Type" "text/html; charset=utf-8"}})
+
+(def db-spec
+  {:uri "postgresql://tfb-database:5432/hello_world"
+   :user "benchmarkdbuser"
+   :password "benchmarkdbpass"
+   :size 1})
+
+(defmacro thread-local [& body]
+  `(let [tl# (ThreadLocal/withInitial (reify Supplier (get [_] ~@body)))]
+     (reify IDeref (deref [_] (.get tl#)))))
+
+(def pool
+  "PostgreSQL pool of connections (`PgPool`)."
+  (thread-local (async/pool db-spec)))
+
+(defn random
+  "Generate a random number between 1 and 10'000."
+  []
+  (unchecked-inc (.nextInt (ThreadLocalRandom/current) 10000)))
+
+(defn sanitize-queries-param
+  "Sanitizes the `queries` parameter. Clamps the value between 1 and 500.
+  Invalid (string) values become 1."
+  [request]
+  (let [queries (-> request
+                    :query-string
+                    (subs 8))
+        n (try (Integer/parseInt queries)
+               (catch Exception _ 1))] ; default to 1 on parse failure
+    (cond
+      (< n 1) 1
+      (> n 500) 500
+      :else n)))
+
+(def ^Context
+  query-mapper
+  "Map each row into a record."
+  (async/context {:row (async/rs->compiled-record)}))
+
+(defn query-one-random-world
+  "Query a random world on the database.
+  Return a `CompletableFuture`."
+  []
+  (async/query-one query-mapper
+                   @pool
+                   ["SELECT id, randomnumber FROM world WHERE id=$1" (random)]))
+
+(defn update-world
+  "Update a world on the database.
+  Return a `CompletableFuture`."
+  [{:keys [randomNumber id]}]
+  (async/query @pool
+               ["UPDATE world SET randomnumber=$1 WHERE id=$2" randomNumber id]))
+
+(defn run-queries
+  "Run a number of `queries` on the database to fetch a random world.
+  Return a `manifold.deferred`."
+  [queries]
+  (apply d/zip
+         (take queries
+               (repeatedly query-one-random-world))))
+
+(defn query-fortunes
+  "Query the fortunes on database.
+  Return a `CompletableFuture`."
+  []
+  (async/query query-mapper @pool ["SELECT id, message from FORTUNE"]))
+
+(defn get-fortunes
+  "Fetch the full list of Fortunes from the database, sort them by the fortune
+  message text.
+  Return a `CompletableFuture` with the results."
+  []
+  (d/chain (query-fortunes)
+           (fn [fortunes]
+             (sort-by :message
+                      (conj fortunes
+                            {:id 0
+                             :message "Additional fortune added at request time."})))))
+
+(defn update-and-persist
+  "Fetch a number of `queries` random world from the database.
+  Compute a new `randomNumber` for each of them a return a `CompletableFuture`
+  with the updated worlds."
+  [queries]
+  (d/chain' (run-queries queries)
+            (fn [worlds]
+              (let [worlds' (mapv #(assoc % :randomNumber (random)) worlds)]
+                (d/chain' (apply d/zip (mapv update-world worlds'))
+                          (fn [_] worlds'))))))
+
+(defn fortunes-hiccup
+  "Render the given fortunes to simple HTML using Hiccup."
+  [fortunes]
+  (hp/html5
+   [:head
+    [:title "Fortunes"]]
+   [:body
+    [:table
+     [:tr
+      [:th "id"]
+      [:th "message"]]
+     (for [x fortunes]
+       [:tr
+        [:td (:id x)]
+        [:td (hu/escape-html (:message x))]])]]))
+
+(defn handler
+  "Ring handler representing the different tests."
+  [req]
   (let [uri (:uri req)]
     (cond
       (.equals "/plaintext" uri) plaintext-response
-      (.equals "/json" uri) (assoc json-response
-                              :body (json/write-value-as-bytes (t/hash-map :message "Hello, World!")))
+      (.equals "/json" uri)      (assoc json-response
+                                        :body (json/write-value-as-bytes {:message "Hello, World!"}))
+      (.equals "/db" uri)        (-> (query-one-random-world)
+                                     (d/chain (fn [world]
+                                                (assoc json-response
+                                                       :body (json/write-value-as-bytes world)))))
+      (.equals "/queries" uri)   (-> (sanitize-queries-param req)
+                                     (run-queries)
+                                     (d/chain (fn [worlds]
+                                                (assoc json-response
+                                                       :body (json/write-value-as-bytes worlds)))))
+      (.equals "/fortunes" uri)  (d/chain' (get-fortunes)
+                                           fortunes-hiccup
+                                           (fn [body]
+                                             (assoc html-response :body body)))
+      (.equals "/updates" uri)   (-> (sanitize-queries-param req)
+                                     (update-and-persist)
+                                     (d/chain (fn [worlds]
+                                                (assoc json-response
+                                                       :body (json/write-value-as-bytes worlds)))))
       :else {:status 404})))
 
 ;;;
 
-(defn -main [& args]
-
-  (let [[{:keys [help port]} _ banner]
-        (cli/cli args
-          ["-p" "--port" "Server port"
-           :default 8080
-           :parse-fn #(Integer/parseInt %)]
-          ["-h" "--[no-]help"])]
-
-    (when help
-      (println banner)
-      (System/exit 0))
-
-    (aleph.netty/leak-detector-level! :disabled)
-    (http/start-server handler {:port port, :executor :none})))
+(defn -main [& _]
+  (netty/leak-detector-level! :disabled)
+  (http/start-server handler {:port 8080
+                              :raw-stream? true
+                              :epoll? true
+                              :executor :none
+                              :bootstrap-transform (fn [bootstrap]
+                                                     (.option bootstrap ChannelOption/ALLOCATOR PooledByteBufAllocator/DEFAULT)
+                                                     (.childOption bootstrap ChannelOption/ALLOCATOR PooledByteBufAllocator/DEFAULT))
+                              :pipeline-transform (fn [pipeline]
+                                                    (.remove pipeline "continue-handler"))})
+  ;; Uncomment to enable async-profiler
+  #_
+  (do
+    (prof/profile-for 60
+                      #_
+                      {:transform (fn [s]
+                                    (when-not (re-find #"(writev|__libc|epoll_wait|write|__pthread)" s)
+                                      s))})
+    (prof/serve-files 8081)))