Skip to content

Commit e828d84

Browse files
committed
Initial async channel support [IMMUTANT-521]
This adds a new namespace (immutant.web.async), that provides a common channel abstraction for http streaming and websockets. WIP
1 parent 1fdf2d9 commit e828d84

File tree

11 files changed

+528
-172
lines changed

11 files changed

+528
-172
lines changed

project.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
h2 "1.3.176"
8888

8989
;; org.projectodd.wunderboss "0.3.0"
90-
org.projectodd.wunderboss "1.x.incremental.174"
90+
org.projectodd.wunderboss "1.x.incremental.176"
9191
;; org.projectodd.wunderboss "0.4.0-SNAPSHOT"
9292

9393
org.immutant :version}}

web/dev-resources/testing/app.clj

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
(ns testing.app
1616
(:require [immutant.web :as web]
1717
[immutant.web.websocket :as ws]
18+
[immutant.web.async :as async]
1819
[immutant.web.middleware :refer (wrap-session)]
1920
[immutant.codecs :refer (encode)]
2021
[compojure.core :refer (GET defroutes)]
@@ -23,16 +24,16 @@
2324
(def handshakes (atom {}))
2425

2526
(defn on-open-set-handshake [channel handshake]
26-
(let [data {:headers (ws/headers handshake)
27-
:parameters (ws/parameters handshake)
28-
:uri (ws/uri handshake)
29-
:query (ws/query-string handshake)
30-
:session (ws/session handshake)
31-
:user-principal (ws/user-principal handshake)}]
27+
(let [data {:headers (async/headers handshake)
28+
:parameters (async/parameters handshake)
29+
:uri (async/uri handshake)
30+
:query (async/query-string handshake)
31+
:session (async/session handshake)
32+
:user-principal (async/user-principal handshake)}]
3233
(swap! handshakes assoc channel data)))
3334

3435
(defn on-message-send-handshake [channel message]
35-
(ws/send! channel (encode (get @handshakes channel))))
36+
(async/send! channel (encode (get @handshakes channel))))
3637

3738
(defn counter [{session :session}]
3839
(let [count (:count session 0)
@@ -51,16 +52,55 @@
5152
:body body}
5253
cs)))
5354

55+
(defn chunked-stream [request]
56+
(async/as-channel request
57+
{:on-open
58+
(fn [stream]
59+
(.start
60+
(Thread.
61+
(fn []
62+
(async/send! stream "[" false)
63+
(dotimes [n 10]
64+
;; we have to send a few bytes with each
65+
;; response - there is a min-bytes threshold to
66+
;; trigger data to the client
67+
(async/send! stream (format "%s ;; %s\n" n (repeat 128 "1")) false))
68+
;; 2-arity send! closes the stream
69+
(async/send! stream "]")))))}))
70+
71+
(defn non-chunked-stream [request]
72+
(async/as-channel request
73+
{:on-open
74+
(fn [stream]
75+
(async/send! stream (str (repeat 128 "1"))))}))
76+
77+
(defn ws-as-channel
78+
[request]
79+
(async/as-channel request
80+
{:on-open (fn [ch hs]
81+
#_(println "TC: open" ch hs))
82+
:on-message (fn [ch message]
83+
#_(println "TC: message" message)
84+
(async/send! ch (.toUpperCase message)))
85+
:on-error (fn [ch err]
86+
(println "Error on websocket")
87+
(.printStackTrace err))
88+
:on-close (fn [ch reason]
89+
#_(println "TC: closed" reason))}))
90+
5491
(defroutes routes
5592
(GET "/" [] counter)
5693
(GET "/session" {s :session} (encode s))
5794
(GET "/unsession" [] {:session nil})
5895
(GET "/request" [] dump)
59-
(GET "/charset" [] with-charset))
96+
(GET "/charset" [] with-charset)
97+
(GET "/chunked-stream" [] chunked-stream)
98+
(GET "/non-chunked-stream" [] non-chunked-stream))
6099

61100
(defn run []
62101
(web/run (-> #'routes
63102
wrap-session
64103
(ws/wrap-websocket
65104
:on-open #'on-open-set-handshake
66-
:on-message #'on-message-send-handshake))))
105+
:on-message #'on-message-send-handshake)))
106+
(web/run ws-as-channel :path "/ws"))

web/src/immutant/web/async.clj

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
;; Copyright 2014 Red Hat, Inc, and individual contributors.
2+
;;
3+
;; Licensed under the Apache License, Version 2.0 (the "License");
4+
;; you may not use this file except in compliance with the License.
5+
;; You may obtain a copy of the License at
6+
;;
7+
;; http://www.apache.org/licenses/LICENSE-2.0
8+
;;
9+
;; Unless required by applicable law or agreed to in writing, software
10+
;; distributed under the License is distributed on an "AS IS" BASIS,
11+
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
;; See the License for the specific language governing permissions and
13+
;; limitations under the License.
14+
15+
(ns immutant.web.async
16+
(:import [org.projectodd.wunderboss.web.async HttpChannel]))
17+
18+
(defn ^:internal streaming-body? [body]
19+
(instance? HttpChannel body))
20+
21+
(defn ^:internal open-stream [^HttpChannel channel headers]
22+
(.notifyOpen channel nil))
23+
24+
(defmulti ^:internal initialize-stream :handler-type)
25+
26+
(defmulti ^:internal initialize-websocket :handler-type)
27+
28+
(defprotocol WebsocketHandshake
29+
"Reflects the state of the initial websocket upgrade request"
30+
(headers [hs] "Return request headers")
31+
(parameters [hs] "Return map of params from request")
32+
(uri [hs] "Return full request URI")
33+
(query-string [hs] "Return query portion of URI")
34+
(session [hs] "Return the user's session data, if any")
35+
(user-principal [hs] "Return authorized `java.security.Principal`")
36+
(user-in-role? [hs role] "Is user in role identified by String?"))
37+
38+
(defprotocol Channel
39+
"Streaming channel interface"
40+
(open? [ch] "Is the channel open?")
41+
(close [ch]
42+
"Gracefully close the channel.
43+
44+
This will trigger the on-close callback for the channel if one is
45+
registered.")
46+
(send! [ch message] [ch message close?]
47+
"Send a message to the channel.
48+
49+
If close? is truthy, close the channel after writing. close?
50+
defaults to false for WebSockets, true otherwise.
51+
52+
Sending is asynchronous for WebSockets, but blocking for
53+
HTTP channels.
54+
55+
Returns nil if the channel is closed, true otherwise."))
56+
57+
(extend-type org.projectodd.wunderboss.web.async.Channel
58+
Channel
59+
(open? [ch] (.isOpen ch))
60+
(close [ch] (.close ch))
61+
(send!
62+
([ch message]
63+
;; TODO: support codecs? support the same functionality as ring bodies?
64+
(.send ch message))
65+
([ch message close?]
66+
(.send ch message close?))))
67+
68+
(defn as-channel
69+
"Converts the current ring `request` in to an asynchronous channel.
70+
71+
The type of channel created depends on the request - if the request
72+
is a Websocket upgrade request, a Websocket channel will be created.
73+
Otherwise, an HTTP channel is created. You interact with both
74+
channel types through the [[Channel]] protocol, and through the
75+
given `callbacks`.
76+
77+
The callbacks common to both channel types are:
78+
79+
* `:on-open` - `(fn [ch ctx] ...)`
80+
* `:on-close` - `(fn [ch reason] ...)` - invoked after close. TODO: make reason consistent
81+
82+
If the channel is a Websocket, the following callbacks are also used:
83+
84+
* `:on-message` - `(fn [ch message] ...)` - String or byte[]
85+
* `:on-error` - `(fn [ch throwable] ...)`
86+
87+
The channel won't be available for writing until the `:on-open`
88+
callback is invoked.
89+
90+
discuss: sessions, headers
91+
provide usage example
92+
93+
Returns a ring response map, at least the :body of which *must* be
94+
returned in the response map from the calling ring handler."
95+
[request {:keys [on-open on-close on-message on-error] :as callbacks}]
96+
(let [ch (if (:websocket? request)
97+
(initialize-websocket request callbacks)
98+
(initialize-stream request callbacks))]
99+
{:status 200
100+
:body ch}))
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
;; Copyright 2014 Red Hat, Inc, and individual contributors.
2+
;;
3+
;; Licensed under the Apache License, Version 2.0 (the "License");
4+
;; you may not use this file except in compliance with the License.
5+
;; You may obtain a copy of the License at
6+
;;
7+
;; http://www.apache.org/licenses/LICENSE-2.0
8+
;;
9+
;; Unless required by applicable law or agreed to in writing, software
10+
;; distributed under the License is distributed on an "AS IS" BASIS,
11+
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
;; See the License for the specific language governing permissions and
13+
;; limitations under the License.
14+
15+
(ns ^{:no-doc true}
16+
immutant.web.internal.headers
17+
(:require [clojure.string :as str]
18+
ring.util.request))
19+
20+
(def charset-pattern (deref #'ring.util.request/charset-pattern))
21+
22+
(def default-encoding "ISO-8859-1")
23+
24+
(defprotocol Headers
25+
(get-names [x])
26+
(get-values [x key])
27+
(get-value [x key])
28+
(set-header [x key value])
29+
(add-header [x key value]))
30+
31+
(defn ^String get-character-encoding [headers]
32+
(or
33+
(when-let [type (get-value headers "content-type")]
34+
(second (re-find charset-pattern type)))
35+
default-encoding))
36+
37+
(defn headers->map [headers]
38+
(persistent!
39+
(reduce
40+
(fn [accum ^String name]
41+
(assoc! accum
42+
(-> name .toLowerCase)
43+
(->> name
44+
(get-values headers)
45+
(str/join ","))))
46+
(transient {})
47+
(get-names headers))))
48+
49+
(defn write-headers
50+
[output, headers]
51+
(doseq [[^String k, v] headers]
52+
(if (coll? v)
53+
(doseq [value v]
54+
(add-header output k (str value)))
55+
(set-header output k (str v)))))

web/src/immutant/web/internal/ring.clj

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@
1313
;; limitations under the License.
1414

1515
(ns ^{:no-doc true} immutant.web.internal.ring
16-
(:require [potemkin :refer [def-map-type]]
17-
[clojure.string :as str]
18-
[clojure.java.io :as io]
19-
ring.util.request)
16+
(:require [potemkin :refer [def-map-type]]
17+
[clojure.java.io :as io]
18+
[immutant.web.async :as async]
19+
[immutant.web.internal.headers :as hdr])
2020
(:import [java.io File InputStream OutputStream]
2121
[clojure.lang ISeq PersistentHashMap]))
2222

23-
(def charset-pattern (deref #'ring.util.request/charset-pattern))
24-
2523
(defprotocol Session
2624
(attribute [session key])
2725
(set-attribute! [session key value])
@@ -105,37 +103,6 @@
105103
(.put m k v))
106104
m))))
107105

108-
(defprotocol Headers
109-
(get-names [x])
110-
(get-values [x key])
111-
(get-value [x key])
112-
(set-header [x key value])
113-
(add-header [x key value]))
114-
115-
(defn headers->map [headers]
116-
(persistent!
117-
(reduce
118-
(fn [accum ^String name]
119-
(assoc! accum
120-
(-> name .toLowerCase)
121-
(->> name
122-
(get-values headers)
123-
(str/join ","))))
124-
(transient {})
125-
(get-names headers))))
126-
127-
(defn write-headers
128-
[output, headers]
129-
(doseq [[^String k, v] headers]
130-
(if (coll? v)
131-
(doseq [value v]
132-
(add-header output k (str value)))
133-
(set-header output k (str v)))))
134-
135-
(defn ^String get-character-encoding [headers]
136-
(when-let [type (get-value headers "content-type")]
137-
(second (re-find charset-pattern type))))
138-
139106
(defprotocol BodyWriter
140107
"Writing different body types to output streams"
141108
(write-body [body stream headers]))
@@ -150,7 +117,7 @@
150117

151118
String
152119
(write-body [body ^OutputStream os headers]
153-
(.write os (.getBytes body (or (get-character-encoding headers) "ISO-8859-1"))))
120+
(.write os (.getBytes body (hdr/get-character-encoding headers))))
154121

155122
ISeq
156123
(write-body [body ^OutputStream os headers]
@@ -177,6 +144,7 @@
177144
(when status
178145
(set-status response status))
179146
(let [hmap (header-map response)]
180-
(write-headers hmap headers)
181-
(write-body body (output-stream response) hmap)))
182-
147+
(hdr/write-headers hmap headers)
148+
(if (async/streaming-body? body)
149+
(async/open-stream body hmap)
150+
(write-body body (output-stream response) hmap))))

0 commit comments

Comments
 (0)