Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Clojure by

I'm not sure if this is a bug or intended behaviour. I've been investigating a strange case where Clojure's binding conveyance wasn't behaving as we had expected. The repro case below shows the issue that occurred, using just clojure.core functions. In reality, what had happened was that we had used clojure.lang.Agent/soloExecutor as the ExecutorService for a Langohr RabbitMQ subscription. Threads handling RabbitMQ messages were able to read dynamic variables which were set in unrelated code which ran via an agent.

Symptoms

We were setting a dynamic var like this:

(defn http-handler [url]
  (binding [*url* url]
    (log/info "doing a http request")))

(defn rabbit-handler [message]
  (log/info "handling a RabbitMQ message"))

Our logging system picks up the current value of *url* and adds it to the log lines. We were seeing log lines from other, unrelated threads logging a value for *url* when the var should have had no value. In the toy example above, we were seeing a value for *url* set in the "handling a RabbitMQ message" log lines.

The cause of the observed behaviour is in send-via and in the way binding-conveyor-fn sets a thread binding. binding-conveyor-fn sets the threadBindingFrame in thread-local storage, and never un-sets it.

If that same thread is subsequently re-used for another purpose, the dynamic bindings remain in place from the previous use of the thread.

Repro Case

(ns binding
  (:import java.util.concurrent.Executors))

(def thread-pool
  (Executors/newFixedThreadPool 1))

(set-agent-send-off-executor! thread-pool)

(def counter (agent 0))

(add-tap println)

(def ^:dynamic *some-context* nil)

(tap> "start")

(defn inspect []
  (tap>
   (format "tap: thread=%s context=%s"
           (.getId (Thread/currentThread))
           *some-context*)))

(binding [*some-context* "in the agent"]
  (send-off counter (fn [& _]
                      (inspect))))


(.submit thread-pool inspect)

;; Output
;; start
;; tap: thread=43 context=in the agent
;; tap: thread=43 context=in the agent

Gist

1 Answer

0 votes
by
edited by

I can add that neither wrapping inspect with bound-fn, like:

(def inspect (bound-fn []
  (tap>
   (format "tap: thread=%s context=%s"
           (.getId (Thread/currentThread))
           *some-context*))))

nor wrapping the inspect function at the invocation at the end of the sample:

(.submit thread-pool (bound-fn* inspect))

fixes the issue.

Replacing the last line with

(.submit thread-pool (#'clojure.core/binding-conveyor-fn inspect))

does fix it however.

Old issue, related: https://clojure.atlassian.net/browse/CLJ-2476

by
The same problem with futures if the user is bold enough to reuse Clojure's soloExecutor. Is it possible to make it private?

https://gist.github.com/karolinepauls/9c976d95950141bc3b7d8b179b65b1c4
...