Share your thoughts in the 2024 State of Clojure Survey!

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

0 votes
in core.async by

I'm new to core.async (and CSP more generally) and struggling to find good debugging strategies. In particular, I frequently fail to diagnosis which part of the code is infinitely blocking the main thread. I'll give a particular example:

(deftest passing-test
  (let [c (async/to-chan! [2 1 0 5 4])]
    (is (= [2 1 0 5 4]
           (async/<!! (async/into [] c))))))


(deftest hanging-test
  (let [[e o] (->> (async/to-chan! [2 1 0 5 4])
                   (async/split even?))]
    (is (= [2 0 4]
           (async/<!! (async/into [] e))))
    (is (= [1 5]
           (async/<!! (async/into [] o))))))

The first test passes and does not block the main thread. The second test hangs forever. The only difference is the addition of async/split to route even and odd integers to their respective channels.

My specific questions are as follows:

  1. What explains the difference between the first and second tests with respect to blocking the main thread?

  2. What are some strategies to debugging core.async code that is unexpectedly blocking the main thread?

  3. What is the best way to write unit tests for code that creates and manipulates channels? Is (async/<!! (async/into [] o)) a reasonable way to collect the values flowing through a channel into a collection?

1 Answer

+1 vote
by
selected by
 
Best answer

(1) The producer half of the 2nd test puts 2, the consumer (reading evens) reads 2; the producer puts 1, but the consumer isn't consuming odds yet, so the producer waits, and from the consumer's point of view the channel of evens "never closes", so the consumer sticks at the first <!!.

(2) The hang is the best problem to have because you can insert prints and figure out what's hanging. Sub-optimal balance is harder to get clarity on.

(3) The simpler, the better.

by
Thanks so much for your help! Your explanation of (1) is exactly what what I needed. It seems like it is important to understand (at least a little bit) about the underlying sequence of events. My original mental model was that `core.async` provides a declarative API.

Regarding your point (2) about using prints to identify the problem, I agree that it sounds ideal but I am not sure how to accomplish this. If I understand the problem correctly, we should be able to add a print statement somewhere in the code that will print just a `2` before the hang. Is that correct? Do you have an example of what that might look like?

Regarding (3): Amen to that.

Much appreciated!
by
After re-reading some resources with new perspective, I found one way to add a debug print statement. I would be interested in hearing about anything that is more consice.

  (let [[e o] (->> (async/to-chan! [2 1 0 5 4])
                     (async/split even?))]

      (async/go-loop []
        (when-let [x (async/<! e)]
          (println x)
          (recur))))
by
At a coarse level, keeping your original algorithm, you could put a print before/after each `<!!` in order to discern which of them was blocking.
...