From 9f2e7f04ad12ff9f87dea9341ffa3ef39f27491d Mon Sep 17 00:00:00 2001 From: David Thomas Hume Date: Thu, 13 Mar 2014 08:26:23 +0000 Subject: [PATCH 1/3] Added basic perforate benchmark based on the "things" resource from test_get_put.clj --- benchmarks/things_bench.clj | 56 ++++++++++++++++++++++++++++++++ project.clj | 8 ++++- src/liberator/core.clj | 6 ++-- src/liberator/representation.clj | 2 +- src/liberator/util.clj | 4 +-- 5 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 benchmarks/things_bench.clj diff --git a/benchmarks/things_bench.clj b/benchmarks/things_bench.clj new file mode 100644 index 0000000..5f26417 --- /dev/null +++ b/benchmarks/things_bench.clj @@ -0,0 +1,56 @@ +(ns things-bench + (:require [liberator.core :refer (defresource request-method-in)] + [liberator.representation :refer (ring-response)] + [perforate.core :refer :all] + [ring.mock.request :refer (request header)]) + (:import [java.security MessageDigest])) + +(defresource thing-resource + [things] + ;; early lookup + :service-available? (fn [ctx] {::r (get @things (get-in ctx [:request :uri]))}) + :method-allowed? (request-method-in :get :put :delete) + ;; lookup media types of the requested resource + :available-media-types #(if-let [m (get-in % [::r :media-type])] [m]) + ;; the resource exists if a value is stored in @things at the uri + ;; store the looked up value at key ::r in the context + :exists? #(get % ::r) + ;; ...it existed if the stored value is nil (and not some random + ;; Objeced we use as a setinel) + :existed? #(nil? (get @things (get-in % [:request :uri]) (Object.))) + ;; use the previously stored value at ::r + :handle-ok #(get-in % [::r :content]) + ;; update the representation + :put! #(dosync + (alter things assoc-in + [(get-in % [:request :uri])] + {:content (get-in % [:request :body]) + :media-type (get-in % [:request :headers "content-type"] + "application/octet-stream") + :last-modified (java.util.Date.)})) + ;; ...store a nil value to marke the resource as gone + :delete! #(dosync (alter things assoc (get-in % [:request :uri]) nil)) + :last-modified #(get-in % [::r :last-modified])) + +(defgoal things-bench "'Things' Resource benchmarks") + +(defcase things-bench "Things" + [] + (let [t-r (thing-resource (ref nil))] + (let [resp (t-r (request :get "/r1"))] + (-> resp :status (= 404) assert)) + (let [resp (t-r (-> (request :put "/r1") + (assoc :body "r1") + (header "content-type" "text/plain")))] + (-> resp :status (= 201) assert)) + (let [resp (t-r (-> (request :get "/r1")))] + (-> resp :status (= 200) assert) + (-> resp :body (= "r1") assert) + (-> resp (get-in [:headers "Content-Type"]) + (= "text/plain;charset=UTF-8") + assert)) + (let [resp (t-r (-> (request :delete "/r1")))] + (-> resp :status (= 204) assert) + (-> resp :body nil? assert)) + (let [resp (t-r (request :get "/r1"))] + (-> resp :status (= 410) assert)))) diff --git a/project.clj b/project.clj index cb91a0c..b58c005 100644 --- a/project.clj +++ b/project.clj @@ -16,7 +16,8 @@ :url "https://github.com/clojure-liberator/liberator"} :plugins [[lein-midje "3.1.3" :exclusions [leiningen-core]] - [lein-ring "0.8.10" :exclusions [org.clojure/clojure]]] + [lein-ring "0.8.10" :exclusions [org.clojure/clojure]] + [perforate "0.3.3"]] :profiles {:dev {:dependencies [[ring/ring-jetty-adapter "1.2.1" :exclusions [joda-time]] [ring-mock "0.1.2"] @@ -33,6 +34,11 @@ :source-paths ["src"] :test-paths ["test"] + :perforate + {:environments [{:name :core + :profiles [:dev :1.5] + :namespaces [things-bench]}]} + :ring {:handler examples.server/handler :adapter {:port 8000}} diff --git a/src/liberator/core.clj b/src/liberator/core.clj index 0700ad2..6a8390c 100644 --- a/src/liberator/core.clj +++ b/src/liberator/core.clj @@ -206,7 +206,7 @@ (defmethod to-location clojure.lang.APersistentMap [this] this) -(defmethod to-location java.net.URL [url] (to-location (.toString url))) +(defmethod to-location java.net.URL [^java.net.URL url] (to-location (.toString url))) (defmethod to-location nil [this] this) @@ -310,7 +310,7 @@ (defdecision modified-since? (fn [context] - (let [last-modified (gen-last-modified context)] + (let [^java.util.Date last-modified (gen-last-modified context)] [(and last-modified (.after last-modified (::if-modified-since-date context))) {::last-modified last-modified}])) method-delete? @@ -346,7 +346,7 @@ (defdecision unmodified-since? (fn [context] - (let [last-modified (gen-last-modified context)] + (let [^java.util.Date last-modified (gen-last-modified context)] [(and last-modified (.after last-modified (::if-unmodified-since-date context))) diff --git a/src/liberator/representation.clj b/src/liberator/representation.clj index 4baed21..030c7fb 100644 --- a/src/liberator/representation.clj +++ b/src/liberator/representation.clj @@ -179,7 +179,7 @@ (render-seq-generic data (assoc-in context [:representation :media-type] "application/json")))) -(defn in-charset [string charset] +(defn in-charset [^String string ^String charset] (if (and charset (not (.equalsIgnoreCase charset "UTF-8"))) (java.io.ByteArrayInputStream. (.getBytes string (java.nio.charset.Charset/forName charset))) diff --git a/src/liberator/util.clj b/src/liberator/util.clj index 4538e78..640bd4e 100644 --- a/src/liberator/util.clj +++ b/src/liberator/util.clj @@ -24,7 +24,7 @@ nil (as-date [this] nil)) -(defn http-date-format [] +(defn ^SimpleDateFormat http-date-format [] (let [df (new SimpleDateFormat "EEE, dd MMM yyyy HH:mm:ss z" Locale/US)] @@ -32,7 +32,7 @@ df))) (defn relative-date [future] - (Date. (+ (System/currentTimeMillis) future))) + (Date. (long (+ (System/currentTimeMillis) future)))) (defn http-date [date] (format "%s" (.format (http-date-format) date))) From 4ccd31304a0091a1563779a4f8e02d9fa89d701e Mon Sep 17 00:00:00 2001 From: David Thomas Hume Date: Sun, 16 Mar 2014 10:24:54 +0000 Subject: [PATCH 2/3] Added core.async resource support; resource functions may now return a channel, in which case resource execution will return a default response containing a :body whose value is a channel onto which the eventual response will be placed. --- benchmarks/things_bench.clj | 93 ++++++++----- project.clj | 6 +- src/liberator/async.clj | 100 ++++++++++++++ src/liberator/core.clj | 253 +++++++++++++++++++++--------------- test/test_async.clj | 54 ++++++++ 5 files changed, 368 insertions(+), 138 deletions(-) create mode 100644 src/liberator/async.clj create mode 100644 test/test_async.clj diff --git a/benchmarks/things_bench.clj b/benchmarks/things_bench.clj index 5f26417..0afdc33 100644 --- a/benchmarks/things_bench.clj +++ b/benchmarks/things_bench.clj @@ -1,47 +1,50 @@ (ns things-bench - (:require [liberator.core :refer (defresource request-method-in)] + (:require [liberator.async :refer (go?)] + [liberator.core :refer (resource request-method-in)] [liberator.representation :refer (ring-response)] [perforate.core :refer :all] [ring.mock.request :refer (request header)]) (:import [java.security MessageDigest])) -(defresource thing-resource - [things] - ;; early lookup - :service-available? (fn [ctx] {::r (get @things (get-in ctx [:request :uri]))}) - :method-allowed? (request-method-in :get :put :delete) - ;; lookup media types of the requested resource - :available-media-types #(if-let [m (get-in % [::r :media-type])] [m]) - ;; the resource exists if a value is stored in @things at the uri - ;; store the looked up value at key ::r in the context - :exists? #(get % ::r) - ;; ...it existed if the stored value is nil (and not some random - ;; Objeced we use as a setinel) - :existed? #(nil? (get @things (get-in % [:request :uri]) (Object.))) - ;; use the previously stored value at ::r - :handle-ok #(get-in % [::r :content]) - ;; update the representation - :put! #(dosync - (alter things assoc-in - [(get-in % [:request :uri])] - {:content (get-in % [:request :body]) - :media-type (get-in % [:request :headers "content-type"] - "application/octet-stream") - :last-modified (java.util.Date.)})) - ;; ...store a nil value to marke the resource as gone - :delete! #(dosync (alter things assoc (get-in % [:request :uri]) nil)) - :last-modified #(get-in % [::r :last-modified])) +(defn things-resource + [base things] + (resource + base + ;; early lookup + :service-available? (fn [ctx] {::r (get @things (get-in ctx [:request :uri]))}) + :method-allowed? (request-method-in :get :put :delete) + ;; lookup media types of the requested resource + :available-media-types #(if-let [m (get-in % [::r :media-type])] [m]) + ;; the resource exists if a value is stored in @things at the uri + ;; store the looked up value at key ::r in the context + :exists? #(get % ::r) + ;; ...it existed if the stored value is nil (and not some random + ;; Objeced we use as a setinel) + :existed? #(nil? (get @things (get-in % [:request :uri]) (Object.))) + ;; use the previously stored value at ::r + :handle-ok #(get-in % [::r :content]) + ;; update the representation + :put! #(dosync + (alter things assoc-in + [(get-in % [:request :uri])] + {:content (get-in % [:request :body]) + :media-type (get-in % [:request :headers "content-type"] + "application/octet-stream") + :last-modified (java.util.Date.)})) + ;; ...store a nil value to marke the resource as gone + :delete! #(dosync (alter things assoc (get-in % [:request :uri]) nil)) + :last-modified #(get-in % [::r :last-modified]))) (defgoal things-bench "'Things' Resource benchmarks") -(defcase things-bench "Things" - [] - (let [t-r (thing-resource (ref nil))] +(defn run-things + [{:keys [read-response resource-fn]}] + (let [t-r (comp read-response resource-fn)] (let [resp (t-r (request :get "/r1"))] (-> resp :status (= 404) assert)) (let [resp (t-r (-> (request :put "/r1") - (assoc :body "r1") - (header "content-type" "text/plain")))] + (assoc :body "r1") + (header "content-type" "text/plain")))] (-> resp :status (= 201) assert)) (let [resp (t-r (-> (request :get "/r1")))] (-> resp :status (= 200) assert) @@ -54,3 +57,29 @@ (-> resp :body nil? assert)) (let [resp (t-r (request :get "/r1"))] (-> resp :status (= 410) assert)))) + +(defcase things-bench "Things Sync" + [] + (run-things {:read-response identity + :resource-fn + (things-resource + {:authorized? + (request-method-in :get :put :delete)} + (ref nil))})) + +(defcase things-bench "Things Async" + [] + (let [things (ref nil)] + (run-things {:read-response #(-> % :body clojure.core.async/ req request handler)] + (if (and response (async-response resp)) + (update-in resp [:body] (partial async/map< response)) + resp))))) + +(defmacro bind-map keys vec) + bind-gs (vec (for [k bind-keys] (gensym)))] + `(let* [do-body# (fn ~bind-keys ~@body) + ~@(mapcat (fn [gs k] [gs (get bind-map k)]) + bind-gs bind-keys)] + (if (some channel? ~bind-gs) + (go? + ( {} + (set-header-maybe "Content-Type" + (str (:media-type representation) + (when-let [charset (:charset representation)] (str ";charset=" charset)))) + (set-header-maybe "Content-Language" (:language representation)) + (set-header-maybe "Content-Encoding" + (let [e (:encoding representation)] + (if-not (= "identity" e) e))) + (set-header-maybe "Vary" (build-vary-header representation)))} + ;; Finally the result of the handler. We allow the handler to + ;; override the status and headers. + ;; + ;; The rules about who should take responsibility for encoding + ;; the response are defined in the BodyResponse protocol. + ((:as-response resource) handler-response context))) + +(defn build-default-handler-response + [name status {:keys [message] :as context}] + (do + (log! :handler (keyword name) "(default implementation)") + {:status status + :headers {"Content-Type" "text/plain"} + :body (if (fn? message) (message context) message)})) + +(defn finalize-response + [name status etag last-modified location handler-response + {:keys [request resource] :as context}] + (let [response (merge-with combine - ;; Status {:status status} - ;; ETags - (when-let [etag (gen-etag context)] + (when etag {:headers {"ETag" etag}}) - ;; Last modified - (when-let [last-modified (gen-last-modified context)] + (when last-modified {:headers {"Last-Modified" (http-date last-modified)}}) - ;; 201 created required a location header to be send - (when (= 201 status) - (if-let [f (or (get context :location) - (get resource :location))] - {:headers {"Location" (str ((make-function f) context))}})) - - (if-let [handler (get resource (keyword name))] - (do - (log! :handler (keyword name)) - ;; Content negotiations - (merge-with - merge - {:headers - (-> {} - (set-header-maybe "Content-Type" - (str (:media-type representation) - (when-let [charset (:charset representation)] (str ";charset=" charset)))) - (set-header-maybe "Content-Language" (:language representation)) - (set-header-maybe "Content-Encoding" - (let [e (:encoding representation)] - (if-not (= "identity" e) e))) - (set-header-maybe "Vary" (build-vary-header representation)))} - ;; Finally the result of the handler. We allow the handler to - ;; override the status and headers. - (let [handler-response (handler context) - ring-response ((:as-response resource) handler-response context)] - ring-response))) - + (when location + {:headers {"Location" (str location)}}) + ;; Handler + (if handler-response + (build-handler-response handler-response context) ;; If there is no handler we just return the information we ;; have so far. - (let [message (get context :message)] - (do (log! :handler (keyword name) "(default implementation)") - {:status status - :headers {"Content-Type" "text/plain"} - :body (if (fn? message) (message context) message)}))))] + (build-default-handler-response name status context)))] (cond - (or (= :options (:request-method request)) (= 405 (:status response))) - (merge-with merge {:headers {"Allow" (build-allow-header resource)}} response) - (= :head (:request-method request)) - (dissoc response :body) - :else response))) + (or (= :options (:request-method request)) (= 405 (:status response))) + (merge-with merge {:headers {"Allow" (build-allow-header resource)}} response) + + (= :head (:request-method request)) + (dissoc response :body) + + :else response))) + +(defn run-handler [name status message + {:keys [resource request representation] :as context}] + (let [context (merge {:status status :message message} context)] + (response + [maybe-e] + (if (instance? ProtocolException maybe-e) + {:status 400 + :headers {"Content-Type" "text/plain"} + :body (.getMessage maybe-e) + ::throwable maybe-e} + maybe-e)) + ;; resources are a map of implementation methods (defn run-resource [request kvs] (try - (service-available? {:request request - :resource - (map-values make-function (merge default-functions kvs)) - :representation {}}) - + (let [response + (async? {:request request + :resource + (map-values make-function (merge default-functions kvs)) + :representation {}})] + (if (channel? response) + {:body (map< exception->response response)} + response)) (catch ProtocolException e ; this indicates a client error - {:status 400 - :headers {"Content-Type" "text/plain"} - :body (.getMessage e) - ::throwable e}))) ; ::throwable gets picked up by an error renderer + (exception->response e)))) ; ::throwable gets picked up by an error renderer (defn get-options diff --git a/test/test_async.clj b/test/test_async.clj new file mode 100644 index 0000000..2ffb593 --- /dev/null +++ b/test/test_async.clj @@ -0,0 +1,54 @@ +(ns test-async + (:use clojure.test) + (:use liberator.core) + (:require [clojure.core.async :refer ( Date: Tue, 25 Mar 2014 17:23:25 +0000 Subject: [PATCH 3/3] Fixed up trace support to work in async mode --- src/liberator/async.clj | 8 +++--- src/liberator/dev.clj | 59 ++++++++++++++++++++++++++++------------- src/liberator/graph.clj | 2 +- src/liberator/trace.css | 4 +-- src/liberator/trace.svg | 24 ++++++++++++----- 5 files changed, 65 insertions(+), 32 deletions(-) diff --git a/src/liberator/async.clj b/src/liberator/async.clj index 7b1662f..6728e93 100644 --- a/src/liberator/async.clj +++ b/src/liberator/async.clj @@ -72,14 +72,14 @@ channel)." v#))) (defmacro bind-map keys vec) bind-gs (vec (for [k bind-keys] (gensym)))] - `(let* [do-body# (fn ~bind-keys ~@body) + `(let [do-body# (fn ~bind-keys ~@body) ~@(mapcat (fn [gs k] [gs (get bind-map k)]) bind-gs bind-keys)] - (if (some channel? ~bind-gs) + (if (or ~@(for [gs bind-gs] `(channel? ~gs))) (go? (; rel=x-liberator-trace" (trace-url id))) + resp)) + (defn- wrap-trace-ui [handler] (let [base-url (with-slash mount-url)] (routes @@ -196,24 +206,40 @@ (ANY base-url [] list-handler) (fn [req] (let [resp (handler req)] - (if-let [id (get-in resp [:headers trace-id-header])] - (update-in resp [:headers "Link"] - #(if %1 [%1 %2] %2) - (format "; rel=x-liberator-trace" (trace-url id))) - resp)))))) + (if (async-response resp) + (update-in resp [:body] + (partial map< wrap-trace-ui-response)) + (wrap-trace-ui-response resp))))))) + +(defn- wrap-trace-header-response + [resp] + (if-let [id (get-in resp [:headers trace-id-header])] + (let [[_ [_ _ l]] (log-by-id id)] + (assoc-in resp [:headers "X-Liberator-Trace"] + (map #(clojure.string/join " " %) l))) + resp)) (defn- wrap-trace-header [handler] (fn [req] (let [resp (handler req)] - (if-let [id (get-in resp [:headers trace-id-header])] - (let [[_ [_ _ l]] (log-by-id id)] - (assoc-in resp [:headers "X-Liberator-Trace"] - (map #(clojure.string/join " " %) l))) - resp)))) + (if (async-response resp) + (update-in resp [:body] (partial map< wrap-trace-header-response)) + (wrap-trace-header-response resp))))) (defn- cond-wrap [fn expr wrapper] (if expr (wrapper fn) fn)) +(defn wrap-trace-response + [request-log request response] + (if-not (empty? @request-log) + (do + (save-log! *current-id* + [(Date.) + (select-keys request [:request-method :uri :headers]) + @request-log]) + (assoc-in response [:headers trace-id-header] *current-id*)) + response)) + (defn wrap-trace "Wraps a ring handler such that a request trace is generated. @@ -228,13 +254,10 @@ (binding [*current-id* (next-id)] (core/with-logger (core/atom-logger request-log) (let [resp (handler request)] - (if-not (empty? @request-log) - (do - (save-log! *current-id* - [(Date.) - (select-keys request [:request-method :uri :headers]) - @request-log]) - (assoc-in resp [:headers trace-id-header] *current-id*)) - resp)))))) + (if (async-response resp) + (->> (partial wrap-trace-response request-log request) + (partial map<) + (update-in resp [:body])) + (wrap-trace-response request-log request resp))))))) (cond-wrap (some #{:ui} opts) wrap-trace-ui) (cond-wrap (some #{:header} opts) wrap-trace-header))) diff --git a/src/liberator/graph.clj b/src/liberator/graph.clj index 68e4b45..2b7d630 100644 --- a/src/liberator/graph.clj +++ b/src/liberator/graph.clj @@ -81,7 +81,7 @@ (concat (rank-handler-groups handlers)) (concat (rank-same actions)) (apply str) - (format "digraph{\nid=\"trace\"; size=\"1000,1000\"; page=\"1000,1000\";\n\nnode[shape=\"box\", splines=ortho]\n\"start\"[id=\"start\" shape=circle];\n\"start\" -> \"service-available?\" [id=start_serviceavailable]\n%s\n}")))) + (format "digraph{\nid=\"trace\"; size=\"1000,1000\"; page=\"1000,1000\";\n\nnode[shape=\"box\", splines=ortho]\n\"start\"[id=\"start\" shape=circle];\n\"start\" -> \"async?\" [id=start_async]\n%s\n}")))) (defn generate-graph-dot-file [f] (spit f (generate-graph-dot))) diff --git a/src/liberator/trace.css b/src/liberator/trace.css index 1e464d6..f5e91e5 100644 --- a/src/liberator/trace.css +++ b/src/liberator/trace.css @@ -1,7 +1,7 @@ #start.node ellipse { stroke: #0000dd; fill: rgba(0,221,0,0.1); stroke-width: 3;} #start.node ellipse { stroke: #0000dd; fill: rgba(0,221,0,0.1); stroke-width: 3;} -#start_serviceavailable path { stroke: #0000dd; stroke-width: 3;} -#start_serviceavailable polygon { fill: #0000dd; stroke: #0000dd; stroke-width: 3;} +#start_async path { stroke: #0000dd; stroke-width: 3;} +#start_async polygon { fill: #0000dd; stroke: #0000dd; stroke-width: 3;} .node.hl-true polygon { stroke: #0000dd; stroke-width: 3;} .node.hl-true text { fill: #0000dd; } diff --git a/src/liberator/trace.svg b/src/liberator/trace.svg index 1730bbf..903f03b 100644 --- a/src/liberator/trace.svg +++ b/src/liberator/trace.svg @@ -15,15 +15,25 @@ start -service-available? +async? -service-available? +async? - -start->service-available? + +start->async? + +service-available? + +service-available? + + +async->service-available? + + + handle-service-not-available @@ -32,7 +42,7 @@ service-available?->handle-service-not-available - + false @@ -43,9 +53,9 @@ service-available?->known-method? - + -true +true post!