Skip to content

Commit 6143d5e

Browse files
authored
Ensure DynamicClassLoader is the Context Class Loader (#604)
* Ensure DynamicClassLoader is the Context Class Loader
1 parent 131811e commit 6143d5e

File tree

3 files changed

+90
-27
lines changed

3 files changed

+90
-27
lines changed

project.clj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
[criterium "0.4.6"]
3333
[cheshire "5.10.0"]
3434
[org.slf4j/slf4j-simple "1.7.30"]
35-
[com.cognitect/transit-clj "1.0.324"]]}
35+
[com.cognitect/transit-clj "1.0.324"]
36+
[spootnik/signal "0.2.4"]
37+
[me.mourjo/dynamic-redef "0.1.0"]]}
3638
;; This is for self-generating certs for testing ONLY:
3739
:test {:dependencies [[org.bouncycastle/bcprov-jdk15on "1.69"]
3840
[org.bouncycastle/bcpkix-jdk15on "1.69"]]

src/aleph/netty.clj

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
[clj-commons.primitive-math :as p]
1111
[potemkin :as potemkin :refer [doit doary]])
1212
(:import
13+
[clojure.lang DynamicClassLoader]
1314
[java.io IOException]
1415
[io.netty.bootstrap Bootstrap ServerBootstrap]
1516
[io.netty.buffer ByteBuf Unpooled]
@@ -55,6 +56,7 @@
5556
FastThreadLocalThread GenericFutureListener Future]
5657
[java.io InputStream File]
5758
[java.nio ByteBuffer]
59+
[java.nio.channels ClosedChannelException]
5860
[io.netty.util.internal SystemPropertyUtil]
5961
[java.util.concurrent
6062
ConcurrentHashMap
@@ -199,41 +201,51 @@
199201
(->> (bs/convert x (bs/stream-of ByteBuf) {:chunk-size chunk-size})
200202
(s/onto nil)))
201203

204+
(defn ensure-dynamic-classloader
205+
"Ensure the context class loader has a valid loader chain to
206+
prevent `ClassNotFoundException`.
207+
https://github.com/clj-commons/aleph/issues/603."
208+
[]
209+
(let [thread (Thread/currentThread)
210+
context-class-loader (.getContextClassLoader thread)
211+
compiler-class-loader (.getClassLoader clojure.lang.Compiler)]
212+
(when-not (instance? DynamicClassLoader context-class-loader)
213+
(.setContextClassLoader
214+
thread
215+
(DynamicClassLoader. (or context-class-loader
216+
compiler-class-loader))))))
217+
218+
(defn- operation-complete [^Future f d]
219+
(cond
220+
(.isSuccess f)
221+
(d/success! d (.getNow f))
222+
223+
(.isCancelled f)
224+
(d/error! d (CancellationException. "future is cancelled."))
225+
226+
(some? (.cause f))
227+
(if (instance? ClosedChannelException (.cause f))
228+
(d/success! d false)
229+
(d/error! d (.cause f)))
230+
231+
:else
232+
(d/error! d (IllegalStateException. "future in unknown state"))))
233+
202234
(defn wrap-future
203235
[^Future f]
204236
(when f
205237
(if (.isSuccess f)
206238
(d/success-deferred (.getNow f) nil)
207239
(let [d (d/deferred nil)
208-
;; workaround for the issue with executing RT.readString
209-
;; on a Netty thread that doesn't have appropriate class loader
210-
;; more information here:
211-
;; https://github.com/clj-commons/aleph/issues/365
212-
class-loader (or (clojure.lang.RT/baseLoader)
213-
(clojure.lang.RT/makeClassLoader))]
240+
;; Ensure the same bindings are installed on the Netty thread (vars,
241+
;; classloader) than the thread registering the
242+
;; `operationComplete` callback.
243+
bound-operation-complete (bound-fn* operation-complete)]
214244
(.addListener f
215245
(reify GenericFutureListener
216246
(operationComplete [_ _]
217-
(try
218-
(. clojure.lang.Var
219-
(pushThreadBindings {clojure.lang.Compiler/LOADER
220-
class-loader}))
221-
(cond
222-
(.isSuccess f)
223-
(d/success! d (.getNow f))
224-
225-
(.isCancelled f)
226-
(d/error! d (CancellationException. "future is cancelled."))
227-
228-
(some? (.cause f))
229-
(if (instance? java.nio.channels.ClosedChannelException (.cause f))
230-
(d/success! d false)
231-
(d/error! d (.cause f)))
232-
233-
:else
234-
(d/error! d (IllegalStateException. "future in unknown state")))
235-
(finally
236-
(. clojure.lang.Var (popThreadBindings)))))))
247+
(ensure-dynamic-classloader)
248+
(bound-operation-complete f d))))
237249
d))))
238250

239251
(defn allocate [x]

test/aleph/classloader_test.clj

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
(ns aleph.classloader-test
2+
(:require [clojure.test :refer [deftest testing is]]
3+
[aleph.http :as http]
4+
[aleph.netty :as netty]
5+
[manifold.deferred :as d]
6+
[manifold.utils :refer [when-class]]
7+
[signal.handler :refer [on-signal]]
8+
[dynamic-redef.core :refer [with-dynamic-redefs]])
9+
(:import [io.netty.util.concurrent Future]
10+
[java.lang.management ManagementFactory]
11+
[java.util.concurrent CompletableFuture]))
12+
13+
(defn- operation-complete
14+
"Stubs for `GenericFutureListener/operationComplete` which
15+
returns a completed `CompletableFuture` containing either
16+
`true` or a Throwable."
17+
[^CompletableFuture result ^Future f d]
18+
(try
19+
(d/success! d (.getNow f))
20+
(.complete result true)
21+
d
22+
(catch Throwable e
23+
(prn e)
24+
(.completeExceptionally result e)
25+
d)))
26+
27+
(defn pid
28+
"Gets this process' PID."
29+
[]
30+
(if-let [pid (when-class java.lang.ProcessHandle
31+
(.pid (java.lang.ProcessHandle/current)))]
32+
pid
33+
(let [pid (.getName (ManagementFactory/getRuntimeMXBean))]
34+
(->> pid
35+
(re-seq #"[0-9]+")
36+
(first)
37+
(Integer/parseInt)))))
38+
39+
(deftest test-classloader
40+
(testing "classloader: ensure the class loader is always a DynamicClassLoader"
41+
(let [result (CompletableFuture.)]
42+
(with-dynamic-redefs [netty/operation-complete (partial operation-complete result)]
43+
(let [server (http/start-server
44+
(constantly {:body "ok"})
45+
{:port 9999})]
46+
(on-signal :int
47+
(bound-fn [_] (.close ^java.io.Closeable server)))
48+
(.exec (Runtime/getRuntime) (format "kill -SIGINT %s" (pid)))
49+
(is (= (deref result 10000 ::timeout) true)))))))

0 commit comments

Comments
 (0)