core-async

2020-06-12T18:50:00.462600Z

It's a little shocking to find out that timeout will reuse the same channels. I understand why, but it's not like it's even easy to figure out that is what's being done, since printing doesn't print different timeouts distinctly or anything. It's the kind of thing that when you discover it, it's probably because it exploded in your face.

2020-06-12T18:50:46.462900Z

I think it would be great if at the very least, the doc string made this clear.

alexmiller 2020-06-12T18:56:42.463100Z

there is a ticket for this

alexmiller 2020-06-12T18:56:53.463400Z

have not gotten around to doing it

dominicm 2020-06-12T18:57:40.464700Z

What's the bug?

alexmiller 2020-06-12T18:57:43.464800Z

partially because I'm torn between whether it's something that should be better doc'ed or made more defensive in the code

alexmiller 2020-06-12T18:57:53.465100Z

if you close a timeout channel you close all timeout channels

alexmiller 2020-06-12T18:58:01.465300Z

that's the blowing up part :)

2020-06-12T19:05:22.466300Z

I've accidentally done things like used timeouts as keys in maps

dominicm 2020-06-12T19:13:55.468600Z

Ah, I see. Phew. I was worried they were reused in a different context.

2020-06-12T19:15:30.469400Z

@alexmiller For me, the problem wasn't that it was blowing up, but I erroneously assumed they would be unique, so I built some maps based on that assumption and was terribly surprised. But if it were documented, that would have at least saved me from myself!

2020-06-12T19:15:53.469800Z

And because they don't print distinctly, it's not that easy to figure out what is going on.

2020-06-12T19:16:15.470200Z

Until I read the source code ... which is not the ideal way to understand what is happening.

2020-06-12T19:32:59.470600Z

to me the fact that they don't print distinctly would be a clue they are reused - or did you mean the opposite?

2020-06-12T20:48:15.470800Z

No channels print distinctly. They just say "many to many channel", so you can't use that as any sort of identity hint. If that isn't different than "less special" channels, you can't use it as a hint either.

2020-06-12T20:48:54.471Z

I ended up implementing custom printing while debugging which would embed the hash code of the channel in the printed output, so at least I could visually see what was happening when looking at my data structures.

2020-06-12T20:49:16.471200Z

Honestly core.async debugging is never good (line numbers for stack traces always being at the first line of a go block is a major culprit). This kind of thing is another unfortunate stumbling block that makes it worse.

2020-06-12T20:52:12.471600Z

for me they print with unique ids

e2e.accounts.fc.oea-test=> (a/chan)
#object[clojure.core.async.impl.channels.ManyToManyChannel 0x525379b7 "clojure.core.async.impl.channels.ManyToManyChannel@525379b7"]
e2e.accounts.fc.oea-test=> (a/chan)
#object[clojure.core.async.impl.channels.ManyToManyChannel 0x51cfe9ef "clojure.core.async.impl.channels.ManyToManyChannel@51cfe9ef"]

2020-06-12T20:54:53.471900Z

Without fundamentally changing this, I'm not sure what could be done to sidestep all of the fallout that may occur. That's why I think a doc string is perhaps the best we can do. My honest opinion is that it is unfortunate that we have to know/care about what kinds of channels different channels represent or where they came from - to me, that's not ideal when the fundamental underlying abstraction is that of a conveyance. And when we don't have predicates to test if things are timeouts (for example), it's a challenge to build reliable code the first time without knowing a lot of implementation details. If the code isn't very dynamic, it's not very hard. I ran into this situation due to the fact that I was using alts! on a seq of chans where some were timeouts, and others weren't, and I wanted to know what was what, so I started putting things in collections in various ways ... and then was surprised.

2020-06-12T20:55:55.472200Z

Yeah, I'm in CLJS, which I suspect is the difference.

2020-06-12T20:58:17.472500Z

It just looks like:

#object[cljs.core.async.impl.channels.ManyToManyChannel]

2020-06-12T21:08:19.472800Z

OK, that clears it up, thanks

2020-06-12T22:15:06.473300Z

It occurs to me that a dumb workaround would be:

(defn dumb-timeout
  [ms]
  (go
    (<! (timeout ms))))

2020-06-12T22:31:05.473700Z

or (pipe (timeout ms) (chan) false)

2020-06-12T22:31:26.474100Z

that's likely less complex under the hood than a go block(?)

2020-06-12T22:33:50.474300Z

nope, just expands to a go-loop

raspasov 2020-06-12T23:45:24.475200Z

@pmooser so is the issue that the (timeout) channels close “at the same time” every 10 ms?

2020-06-14T10:29:26.484100Z

No - the problem is that that timeout channels are special and need to be treated in a special way compared to other channels, but there's no (easy) way to discover that timeout channels may be shared across calls (ie different calls to timeout returning the same timeout), and there's no easy way to identify that something IS a timeout (no predicate provided) ... so I wrote some code where I ended up putting them in maps to keep track of them, but the fact that they weren't distinct from one another caused a problem in my code. I am not really arguing about how it is implemented, but that one shouldn't have to read the source code to a channel to discover how to safely use it. It seems like that's against the concept of having an abstraction like a channel to begin with.

2020-06-14T10:32:30.484300Z

Like to expand on that idea, part of the idea of an abstraction is for the consumer not to have to write special code to consume every different implementation of that abstraction independently.

raspasov 2020-06-12T23:53:22.475800Z

@alexmiller what do you mean by “if you close a timeout channel you close all timeout channels” ?

2020-06-13T17:38:04.483400Z

my understanding is it's more nuanced - timeout channels are merged with a granularity and shared / reused

raspasov 2020-06-12T23:54:05.476300Z

(do
  (clojure.core.async/go
    (let [t1 (clojure.core.async/timeout 10000)]
      (clojure.core.async/<! t1)
      (println (str "t1::: " t1))))

  (clojure.core.async/go
    (let [t2 (clojure.core.async/timeout 10000)]
      (clojure.core.async/close! t2)
      (println (str "t2::: " t2)))))
this outputs: (immediately) t2::: clojure.core.async.impl.channels.ManyToManyChannel@522c88e6 (wait ~10 seconds) t1::: clojure.core.async.impl.channels.ManyToManyChannel@8ea59ed

raspasov 2020-06-12T23:54:17.476600Z

For a moment I got scared 🙂

raspasov 2020-06-12T23:54:31.477Z

I thought that it “globally” closes all timeout channels…