core-async

gtrak 2025-05-20T15:12:02.747889Z

My team is trying to wrap our heads around some recent changes. It seems the bounded threadpool was removed recently: https://github.com/clojure/core.async/commits/master/src/main/clojure/clojure/core/async/impl/dispatch.clj Why did we do that? Is there something to replace it? I see some mention of a :compute workload tag, but it doesn't seem it does anything except separate a few cached-threadpool-executors from each other.

2025-05-24T06:18:28.923319Z

For my own curiosity, what was the reason to change the Go pool? So I stop my conjecture.

2025-05-20T15:27:52.261389Z

Preparing for virtual threads support.

2025-05-20T15:36:40.314439Z

I think because go will use virtual threads in the future, which are unbounded. The idea is that if you move back to a JVM without virtual thread now it uses an unbounded cached pool so the behavior will remain the same.

2025-05-20T15:38:35.586399Z

They probably also didn't want to carry that extra thread pool (the old go one) given when it uses vthreads it would be doing nothing.

gtrak 2025-05-20T15:41:29.600399Z

I'd like some way to limit the number of threads used in a production setting. Our whole app doesn't use core.async, just one thing (cognitect.aws). Vthreads wouldn't use up the resources, I guess, but it wouldn't solve that either. (Maybe there's another way around that specific issue) This seems like a semantic change from fast(er) failure when you block the fixed thread-pool to exhausting your JVM threads.

2025-05-20T15:55:52.502909Z

I think there's better support for providing your own pools now. You can set clojure.core.async.executor-factory system property and provide your own factory for the executor pools.

gtrak 2025-05-20T15:59:12.302089Z

ah I see

gtrak 2025-05-20T16:00:25.470939Z

yeah, I think that's what I was looking for

2025-05-20T16:01:55.188819Z

What I gathered is: Workload: • :io - this is for io-thread • :mixed - this is for thread • :compute - this is for flow • :core-async-dispatch - this is for go

2025-05-20T16:03:25.793809Z

Your factory takes a workload and returns the pool to use for each of the above, can be the same for all or different, up to you.

gtrak 2025-05-20T16:09:25.961169Z

aws-api calls async/thread: https://github.com/cognitect-labs/aws-api/blob/f9490bad5c8e58214bdb40926c27898802a538cd/src/cognitect/aws/util.clj#L291 which would eventually be considered a mixed workload, and have no limiter. In an older version of core.async, I'm pretty sure that's unbounded too. For go-blocks in the new impl, go-impl calls dispatch/run, calls (exec r :core-async-dispatch), that keyword is the workload we'd have to hook

gtrak 2025-05-20T16:39:47.004019Z

I wonder what happens to resource consumption with all the apps already out there changing to this default executor.

gtrak 2025-05-20T16:40:50.502809Z

It's unlikely that someone would write new code against a newer version that appears to work, but have it eventually run on an older version of core.async and block the fixed threadpool.

gtrak 2025-05-20T16:44:13.019409Z

I was thinking we lost a feedback mechanism, but just found a new one I didn't know about: https://github.com/clojure/core.async/blob/e0ba619dc6bae7d2d0e33e58b9301a2233daca8d/src/main/clojure/clojure/core/async.clj#L12-L17

ghadi 2025-05-20T16:53:19.558229Z

Does the ns docstring clarify things for you?

ghadi 2025-05-20T16:53:44.779129Z

Try not to examine impl when a doc is available

🤣 1
gtrak 2025-05-20T16:54:04.599019Z

what got me here won't get me there?

gtrak 2025-05-20T16:58:01.754969Z

I don't think it's clear at all. Go-blocks were supposed to be non-blocking i/o, and now it appears it's ok to use them in what was considered a wrong way in the past.

ghadi 2025-05-20T16:58:39.776879Z

no, that is not correct

gtrak 2025-05-20T16:58:39.942309Z

We have checks for blocking ops, but you can call any java api

ghadi 2025-05-20T16:58:53.388919Z

go blocks are subject to the same restrictions as before

gtrak 2025-05-20T16:59:46.015989Z

sure, an individual go-block acts the same way, but it might have never run before b/c the executor was hosed.

ghadi 2025-05-20T17:00:34.733429Z

I do not know what your referring to wrt hosed

gtrak 2025-05-20T17:01:06.142909Z

> This seems like a semantic change from fast(er) failure when you block the fixed thread-pool to exhausting your JVM threads.

ghadi 2025-05-20T17:03:26.054279Z

don’t know what that means, sorry

ghadi 2025-05-20T17:03:49.770799Z

If you want to have unrestrained blocking, IO or Chan or otherwise, use io-thread

gtrak 2025-05-20T17:04:03.601609Z

I want the opposite, I want constrained behavior in production

gtrak 2025-05-20T17:04:15.465039Z

now I see I can set an executor for that, but I think it's easy to miss

gtrak 2025-05-20T17:05:07.243389Z

what happens if someone does blocking i/o in go-blocks, is it fine with virtual threads?

ghadi 2025-05-20T17:06:17.670239Z

what happens today?

ghadi 2025-05-20T17:06:43.039059Z

go blocks must continue to work in the same way whether vthreads are available or not

ghadi 2025-05-20T17:06:58.327249Z

thus you can't do blocking IO in them.

gtrak 2025-05-20T17:07:46.812469Z

If you spin up go-blocks in a quick loop, the executor will make new threads and use more memory (unrealistic but possible). When it was a fixed-thread-pool, some would have had to wait.

gtrak 2025-05-20T17:09:21.498019Z

https://github.com/clojure/core.async/blob/e0ba619dc6bae7d2d0e33e58b9301a2233daca8d/src/main/clojure/clojure/core/async.clj#L175-L177 this docstring is wrong now, I think. Blocking stuff will work with an unbounded thread pool today and would have broken in the past.

gtrak 2025-05-20T17:10:08.343879Z

Is that fine? maybe it's fine. I'm not clear on it, though.

gtrak 2025-05-20T17:18:13.801109Z

I think this doesn't change semantics for existing code that already worked on older versions of core.async, except for possibly increased resource usage and throughput. New code on new jvms should probably just not use go.

gtrak 2025-05-20T17:25:30.838119Z

This disconnect seems to be that runtime behavior !== semantics, and I'm not sure that standard is consistently applied

gtrak 2025-05-20T17:27:28.735609Z

Maybe empirically this is a fine change, but it did surprise someone on my team. We worry about being woken up at night due to things like unpredictable resource usage and want to lock that down as much as possible.

gtrak 2025-05-20T17:30:32.551059Z

I also expected go-blocks to be run on a fixed thread-pool just like they did 10 years ago, so I dug into it.

gtrak 2025-05-20T17:37:34.279109Z

Hmm, if there were no change to semantics, then it would have also been fine to keep it a fixed-thread-pool for older jvms by default.

Alex Miller (Clojure team) 2025-05-20T18:11:53.269499Z

we did have this discussion at length in the team, specifically around changes in resource behavior. it's a subtle issue, but generally we do not consider changes in resource behavior to be breaking in this sense. applications always need to manage and tune for resource usage, which changes as we get new jvms, new clojure, new libs, new containers, new hardware, etc. we released 3 alphas asking people to test and provide feedback on this kind of thing, still happy to hear more. keep in mind that go blocks should not be doing either blocking I/O or long compute. spinning up large numbers of go blocks in a quick loop is not a typical use case

gtrak 2025-05-20T18:26:01.316109Z

Makes sense, thanks. I think the old fixed threadpool was not an appropriate first line of defense for something like limiting the concurrency in the aws api client, and I suggested we just front it with something else. I'm not sure why I didn't see the release notes, or actually where to find them except looking at the repo.

2025-05-20T19:17:56.003049Z

The flip side here too, is an unbounded thread pool behind go solves some of the deadlock issues that you could experience where things would get mutually blocked on one another.

2025-05-20T19:19:29.146659Z

It also means it takes up less memory when usage of core.async is small, as threads are only created as they are needed.

Alex Miller (Clojure team) 2025-05-20T19:32:27.755019Z

You should never be deadlocked if you follow the constraints of go, which have not changed, so that is not the reason here

➕ 1