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.
I think it would be great if at the very least, the doc string made this clear.
there is a ticket for this
have not gotten around to doing it
What's the bug?
partially because I'm torn between whether it's something that should be better doc'ed or made more defensive in the code
if you close a timeout channel you close all timeout channels
that's the blowing up part :)
I've accidentally done things like used timeouts as keys in maps
Ah, I see. Phew. I was worried they were reused in a different context.
@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!
And because they don't print distinctly, it's not that easy to figure out what is going on.
Until I read the source code ... which is not the ideal way to understand what is happening.
to me the fact that they don't print distinctly would be a clue they are reused - or did you mean the opposite?
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.
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.
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.
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"]
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.
Yeah, I'm in CLJS, which I suspect is the difference.
It just looks like:
#object[cljs.core.async.impl.channels.ManyToManyChannel]
OK, that clears it up, thanks
It occurs to me that a dumb workaround would be:
(defn dumb-timeout
[ms]
(go
(<! (timeout ms))))
or (pipe (timeout ms) (chan) false)
that's likely less complex under the hood than a go block(?)
nope, just expands to a go-loop
@pmooser so is the issue that the (timeout) channels close “at the same time” every 10 ms?
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.
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.
@alexmiller what do you mean by “if you close a timeout channel you close all timeout channels” ?
my understanding is it's more nuanced - timeout channels are merged with a granularity and shared / reused
(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@8ea59edFor a moment I got scared 🙂
I thought that it “globally” closes all timeout channels…