Skip to content

Commit 5a0c697

Browse files
PawelStroinskiDerGuteMoritz
authored andcommitted
Adopting an existing, pre-bound server-socket channel
The use-case is to pass a server-socket channel, e.g. obtained by calling JVM's `System/inheritedChannel`. In this case, Aleph/Netty is not responsible for opening the channel nor closing it. It should simply listen on it. In other words, maintaining the connection sockets is still Aleph/Netty's responsibility, but not maintaining the listening socket if it has been passed to it. * In the server startup part, this has been implemented as per Netty's Norman Maurer's advice [found here] (https://stackoverflow.com/a/50196956). * In the server shutdown part, Aleph is not shutting down nor closing the passed socket, because in practice it's owned by a process that passed it to Aleph process, and it's that parent process' responsibility to reuse or close it after Aleph process exits. In terms of validation and fallback, for comparison, [this used to be Jetty's approach to adopting `inheritedChannel` at version 8] (https://github.com/jetty/jetty.project/blob/jetty-8.0.0.v20110901/jetty-server/src/main/java/org/eclipse/jetty/server/nio/InheritedChannelConnector.java). * Jetty automatically falls back to opening its own socket (with a warning) if the `inheritedChannel` is not supported or not present. In Aleph implementation, it refuses to start the server if the channel type is not supported, or the socket is not bound, but it silently falls back to an own socket if the passed channel is `nil`. As a result, it's Aleph user's responsibility to trigger an error if it expects an 'inherited channel' which is missing. Still, the user can expect that Aleph will fail early if a channel was actually passed, but it cannot be used. * Unlike Jetty, Aleph doesn't itself call `configureBlocking false` on the channel because [this is done by Netty during server startup] (https://github.com/netty/netty/blob/46d6e164a2a46e2c6d802f84e0c24abd475bcd89/transport/src/main/java/io/netty/channel/nio/AbstractNioChannel.java#L84).
1 parent 6663416 commit 5a0c697

File tree

7 files changed

+100
-14
lines changed

7 files changed

+100
-14
lines changed

src/aleph/http.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
| --- | --- |
5252
| `port` | The port the server will bind to. If `0`, the server will bind to a random port. |
5353
| `socket-address` | A `java.net.SocketAddress` specifying both the port and interface to bind to. |
54+
| `existing-channel` | A pre-bound `java.nio.channels.ServerSocketChannel` for the server to use rather than opening and binding its own server-socket. It won't be closed by the server. Possibly obtained from `System/inheritedChannel`. |
5455
| `bootstrap-transform` | A function that takes an `io.netty.bootstrap.ServerBootstrap` object, which represents the server, and modifies it. |
5556
| `http-versions` | An optional vector of allowable HTTP versions to negotiate via ALPN, in preference order. Defaults to `[:http1]`. |
5657
| `ssl-context` | An `io.netty.handler.ssl.SslContext` object or a map of SSL context options (see `aleph.netty/ssl-server-context` for more details) if an SSL connection is desired. When passing an `io.netty.handler.ssl.SslContext` object, it must have an ALPN config matching the `http-versions` option (see `aleph.netty/ssl-server-context` and `aleph.netty/application-protocol-config`). If only HTTP/1.1 is desired, ALPN config is optional.

src/aleph/http/server.clj

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,7 @@
737737
[handler
738738
{:keys [port
739739
socket-address
740+
existing-channel
740741
executor
741742
http-versions
742743
ssl-context
@@ -786,9 +787,10 @@
786787
:http1-pipeline-transform http1-pipeline-transform
787788
:continue-executor continue-executor))
788789
:bootstrap-transform bootstrap-transform
789-
:socket-address (if socket-address
790-
socket-address
791-
(InetSocketAddress. port))
790+
:socket-address (cond
791+
socket-address socket-address
792+
(nil? existing-channel) (InetSocketAddress. port))
793+
:existing-channel existing-channel
792794
:on-close (when (and shutdown-executor?
793795
(or (instance? ExecutorService executor)
794796
(instance? ExecutorService continue-executor)))

src/aleph/netty.clj

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
Unpooled)
2222
(io.netty.channel
2323
Channel
24+
ChannelFactory
2425
ChannelFuture
2526
ChannelHandler
2627
ChannelHandlerContext
@@ -1667,6 +1668,22 @@
16671668
(.addFirst pipeline "channel-tracker" ^ChannelHandler (channel-tracking-handler chan-group))
16681669
(pipeline-builder pipeline)))
16691670

1671+
(defn- validate-existing-channel
1672+
[existing-channel]
1673+
(when (some? existing-channel)
1674+
(when-not (instance? java.nio.channels.ServerSocketChannel existing-channel)
1675+
(throw (IllegalArgumentException.
1676+
(str "The existing-channel type is not supported: " (pr-str existing-channel)))))
1677+
(when (nil? (.getLocalAddress ^java.nio.channels.ServerSocketChannel existing-channel))
1678+
(throw (IllegalArgumentException.
1679+
(str "The existing-channel is not bound: " (pr-str existing-channel)))))))
1680+
1681+
(defn- wrapping-channel-factory
1682+
^ChannelFactory [^java.nio.channels.ServerSocketChannel channel]
1683+
(proxy [ChannelFactory] []
1684+
(newChannel []
1685+
(NioServerSocketChannel. channel))))
1686+
16701687
(defn ^:no-doc start-server
16711688
([pipeline-builder
16721689
ssl-context
@@ -1685,11 +1702,13 @@
16851702
bootstrap-transform
16861703
on-close
16871704
^SocketAddress socket-address
1705+
existing-channel
16881706
transport
16891707
shutdown-timeout]
16901708
:or {shutdown-timeout default-shutdown-timeout}
16911709
:as opts}]
16921710
(ensure-transport-available! transport)
1711+
(validate-existing-channel existing-channel)
16931712
(let [num-cores (.availableProcessors (Runtime/getRuntime))
16941713
num-threads (* 2 num-cores)
16951714
thread-factory (enumerating-thread-factory "aleph-server-pool" false)
@@ -1715,23 +1734,30 @@
17151734
(.option ChannelOption/SO_REUSEADDR true)
17161735
(.option ChannelOption/MAX_MESSAGES_PER_READ Integer/MAX_VALUE)
17171736
(.group group)
1718-
(.channel channel-class)
1737+
(cond-> (nil? existing-channel) (.channel channel-class))
1738+
(cond-> (some? existing-channel) (.channelFactory (wrapping-channel-factory existing-channel)))
17191739
;;TODO: add a server (.handler) call to the bootstrap, for logging or something
17201740
(.childHandler (pipeline-initializer pipeline-builder))
17211741
(.childOption ChannelOption/SO_REUSEADDR true)
17221742
(.childOption ChannelOption/MAX_MESSAGES_PER_READ Integer/MAX_VALUE)
17231743
bootstrap-transform)
17241744

17251745
^ServerSocketChannel
1726-
ch (-> b (.bind socket-address) .sync .channel)]
1746+
ch (-> (if (nil? existing-channel)
1747+
(.bind b socket-address)
1748+
(.register b))
1749+
.sync
1750+
.channel)]
17271751

17281752
(reify
17291753
Closeable
17301754
(close [_]
17311755
(when (compare-and-set! closed? false true)
17321756
;; This is the three step closing sequence:
17331757
;; 1. Stop listening to incoming requests
1734-
(-> ch .close .sync)
1758+
(if (nil? existing-channel)
1759+
(-> ch .close .sync)
1760+
(-> ch .deregister .sync))
17351761
(-> (if (pos? shutdown-timeout)
17361762
;; 2. Wait for in-flight requests to stop processing within the supplied timeout
17371763
;; interval.
@@ -1759,7 +1785,8 @@
17591785
(port [_]
17601786
(-> ch .localAddress .getPort))
17611787
(wait-for-close [_]
1762-
(-> ch .closeFuture .await)
1788+
(when (nil? existing-channel)
1789+
(-> ch .closeFuture .await))
17631790
(-> group .terminationFuture .await)
17641791
nil)))
17651792

src/aleph/tcp.clj

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,15 @@
7474
| --- | ---
7575
| `port` | the port the server will bind to. If `0`, the server will bind to a random port.
7676
| `socket-address` | a `java.net.SocketAddress` specifying both the port and interface to bind to.
77+
| `existing-channel` | a pre-bound `java.nio.channels.ServerSocketChannel` for the server to use rather than opening and binding its own server-socket. It won't be closed by the server. Possibly obtained from `System/inheritedChannel`. |
7778
| `ssl-context` | an `io.netty.handler.ssl.SslContext` object or a map of SSL context options (see `aleph.netty/ssl-server-context` for more details). If given, the server will only accept SSL connections and call the handler once the SSL session has been successfully established. If a self-signed certificate is all that's required, `(aleph.netty/self-signed-ssl-context)` will suffice.
7879
| `bootstrap-transform` | a function that takes an `io.netty.bootstrap.ServerBootstrap` object, which represents the server, and modifies it.
7980
| `pipeline-transform` | a function that takes an `io.netty.channel.ChannelPipeline` object, which represents a connection, and modifies it.
8081
| `raw-stream?` | if true, messages from the stream will be `io.netty.buffer.ByteBuf` objects rather than byte-arrays. This will minimize copying, but means that care must be taken with Netty's buffer reference counting. Only recommended for advanced users.
8182
| `shutdown-timeout` | interval in seconds within which in-flight requests must be processed, defaults to 15 seconds. A value of 0 bypasses waiting entirely.
8283
| `transport` | the transport to use, one of `:nio`, `:epoll`, `:kqueue` or `:io-uring` (defaults to `:nio`)."
8384
[handler
84-
{:keys [port socket-address ssl-context bootstrap-transform pipeline-transform epoll?
85+
{:keys [port socket-address existing-channel ssl-context bootstrap-transform pipeline-transform epoll?
8586
shutdown-timeout transport]
8687
:or {bootstrap-transform identity
8788
pipeline-transform identity
@@ -101,9 +102,10 @@
101102
(server-channel-handler handler options))
102103
(pipeline-transform pipeline))
103104
:bootstrap-transform bootstrap-transform
104-
:socket-address (if socket-address
105-
socket-address
106-
(InetSocketAddress. port))
105+
:socket-address (cond
106+
socket-address socket-address
107+
(nil? existing-channel) (InetSocketAddress. port))
108+
:existing-channel existing-channel
107109
:transport (netty/determine-transport transport epoll?)
108110
:shutdown-timeout shutdown-timeout})))
109111

test/aleph/http_test.clj

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
[aleph.resource-leak-detector]
99
[aleph.ssl :as test-ssl]
1010
[aleph.tcp :as tcp]
11-
[aleph.testutils :refer [str=]]
11+
[aleph.testutils :refer [str= bound-channel]]
1212
[clj-commons.byte-streams :as bs]
1313
[clojure.java.io :as io]
1414
[clojure.string :as str]
@@ -48,7 +48,10 @@
4848
(java.io
4949
Closeable
5050
File)
51-
(java.net UnknownHostException)
51+
(java.net
52+
BindException
53+
UnknownHostException)
54+
(java.nio.channels ServerSocketChannel)
5255
(java.util.concurrent
5356
SynchronousQueue
5457
ThreadPoolExecutor
@@ -1771,6 +1774,40 @@
17711774
(catch Exception e
17721775
(is (instance? ProxyConnectException e)))))))))
17731776

1777+
(deftest test-existing-channel
1778+
(testing "validation"
1779+
(is (thrown-with-msg? Exception #"existing-channel"
1780+
(with-http-servers basic-handler {:existing-channel "a string"})))
1781+
(with-open [unbound-channel (ServerSocketChannel/open)]
1782+
(is (thrown-with-msg? Exception #"existing-channel"
1783+
(with-http-servers basic-handler {:existing-channel unbound-channel})))))
1784+
1785+
(testing "with a bound server-socket channel"
1786+
(testing "- unknown to the server"
1787+
(with-open [_ (bound-channel port)]
1788+
;; The port is already bound by a server-socket channel, but
1789+
;; we are not telling Aleph about it, so we should get a
1790+
;; BindException when Aleph tries to bind to the same port.
1791+
(is (thrown? BindException
1792+
(with-http-servers basic-handler {})))))
1793+
1794+
(testing "- known to the server"
1795+
(with-open [channel (bound-channel port)]
1796+
;; This time, we shouldn't get a BindException, because we are
1797+
;; telling Aleph to use an existing server-socket channel,
1798+
;; which should be already bound, so Aleph doesn't try to bind.
1799+
(with-http-servers basic-handler {:existing-channel channel}
1800+
(is (= 200 (:status @(http-get "/string")))))
1801+
;; The existing channel should not be closed on a server shutdown,
1802+
;; because that channel is not owned by the server.
1803+
(is (.isOpen channel)))))
1804+
1805+
(testing "the :port option is not required when :existing-channel is passed"
1806+
(with-redefs [http-server-options (dissoc http-server-options :port)]
1807+
(with-open [channel (bound-channel port)]
1808+
(with-http1-server basic-handler {:existing-channel channel}
1809+
(is (= 200 (:status @(http-get "/string")))))))))
1810+
17741811
(deftest ^:leak test-leak-in-raw-stream-handler
17751812
;; NOTE: Expecting 2 leaks because `with-raw-handler` will run its body for both http1 and
17761813
;; http2. It would be nicer to put this assertion into the body but the http1 server seems to

test/aleph/tcp_test.clj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
[aleph.netty :as netty]
44
[aleph.resource-leak-detector]
55
[aleph.tcp :as tcp]
6+
[aleph.testutils :refer [bound-channel]]
67
[clj-commons.byte-streams :as bs]
78
[clojure.test :refer [deftest testing is]]
89
[manifold.stream :as s]))
@@ -55,4 +56,13 @@
5556
(catch Exception _
5657
(is (not (netty/io-uring-available?)))))))
5758

59+
(deftest test-existing-channel
60+
(testing "the :port option is not required when :existing-channel is passed"
61+
(let [port 8083]
62+
(with-open [channel (bound-channel port)]
63+
(with-server (tcp/start-server echo-handler {:existing-channel channel :shutdown-timeout 0})
64+
(let [c @(tcp/client {:host "localhost" :port port})]
65+
(s/put! c "foo")
66+
(is (= "foo" (bs/to-string @(s/take! c))))))))))
67+
5868
(aleph.resource-leak-detector/instrument-tests!)

test/aleph/testutils.clj

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
(ns aleph.testutils
2-
(:import (io.netty.util AsciiString)))
2+
(:import (io.netty.util AsciiString)
3+
(java.net InetSocketAddress)
4+
(java.nio.channels ServerSocketChannel)))
35

46
(defn str=
57
"AsciiString-aware equals"
68
[^CharSequence x ^CharSequence y]
79
(AsciiString/contentEquals x y))
810

11+
(defn bound-channel
12+
"Returns a new server-socket channel bound to a `port`."
13+
^ServerSocketChannel [port]
14+
(doto (ServerSocketChannel/open)
15+
(.bind (InetSocketAddress. port))))

0 commit comments

Comments
 (0)