This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-03-10
Channels
- # announcements (48)
- # asami (8)
- # babashka (183)
- # beginners (56)
- # calva (42)
- # clerk (84)
- # clj-kondo (75)
- # cljdoc (21)
- # clojure (121)
- # clojure-art (1)
- # clojure-australia (1)
- # clojure-china (1)
- # clojure-conj (2)
- # clojure-europe (10)
- # clojure-filipino (1)
- # clojure-hk (1)
- # clojure-indonesia (1)
- # clojure-japan (1)
- # clojure-korea (1)
- # clojure-my (1)
- # clojure-nl (2)
- # clojure-norway (9)
- # clojure-sg (1)
- # clojure-taiwan (1)
- # clojure-uk (2)
- # clojurescript (11)
- # cursive (30)
- # datalevin (20)
- # datomic (4)
- # fulcro (5)
- # gratitude (1)
- # hyperfiddle (87)
- # introduce-yourself (1)
- # java (5)
- # jobs-discuss (8)
- # lsp (89)
- # malli (57)
- # membrane (16)
- # off-topic (12)
- # pathom (36)
- # releases (5)
- # shadow-cljs (17)
- # tools-deps (18)
- # xtdb (62)
So far the go macro was "mocked" in bb for compatibility and fell back to real threads. In this commit I improve on this "mocked" situation by leveraging virtual threads for go blocks: https://github.com/babashka/babashka/compare/master...go-macro-uses-virtual-threads That makes the following example work in bb whereas previously it ran out of OS threads:
(require '[clojure.core.async :as async])
(def n 100000)
(let [begin (System/currentTimeMillis)
state (atom 0)]
(dotimes [_ n]
(async/go (Thread/sleep 1000) ;; bad, you shouldn't block in a JVM go block, but what's a better way to force bb to go out of OS threads?
(swap! state inc)))
(while (< @state n))
(println "Spawned" n "go blocks and finished in" (- (System/currentTimeMillis) begin) "ms"))
Nice! But a question about the example, you would normally not call Thread/sleep
in a go
block?
haha i see. Good job!
I haven’t played with core.async for a long time so would be a challenge. Just remembered that you can’t block in a go
block 😅
I was mostly wondering if this would work in Clojure hehe. Nice it works in bb 👏
in the book I had this example: https://book.babashka.org/#core_async but it still works for me at high numbers or it takes ages in both clj and bb
I’ll think about it too
Is core.async/timeout
implemented? Something like (<! (timeout 1000))
maybe?
by replacing (Thread/sleep 1000)
with (async/<! (async/timeout 1000))
it works better in clj, still crashes with the old bb but in the new bb I'm getting:
java.lang.AssertionError: Assert failed: No more than 1024 pending takes are allowed on a single channel.
Yeah I recognize that error from the JVM. I think 1024 is the maximum of the channel’s queue so you are going over the limit of the go blocks internal channel?
Maybe use (dotimes [i 1000] (<!! )
. The blocking one
I hope 1024 is still enough to prove your point
Sorry not (<!! ..)
but (>!! ..)
to insert in a channel
But I guess this completely changes the example hmmm
Not sure exactly about the bb
implementation, but in jvm
. The go
never blocks. So you are doing N=100000 puts on an internal go block channel (I believe) and the maximum is 1024
I cannot explain it by reading the core.async code (I tried), but I can easily reproduce it in bb
(def c (async/chan))
(dotimes [i 1025] (async/go (async/>! c i)))
Exception in thread "async-thread-macro-1025" java.lang.AssertionError: Assert failed: No more than 1024 pending puts are allowed on a single channel. Consider using a windowed buffer.
(< (.size puts) impl/MAX-QUEUE-SIZE)
at clojure.core.async.impl.channels.ManyToManyChannel.put_BANG_(channels.clj:157)
at clojure.core.async$fn__17563.invokeStatic(async.clj:174)
at clojure.core.async$fn__17563.invoke(async.clj:166)
at sci.lang.Var.invoke(lang.cljc:202)
at sci.impl.analyzer$return_call$reify__4482.eval(analyzer.cljc:1402)
I think one problem is that 1024 is still ok for normal threads. So you run into the limits of core.async itself. Starting a million threads is an issue in my bb though:
(require '[clojure.core.async :as async])
(def n (* 1000 1024))
(println n "Threads")
(def *cnt (atom 0))
(def start (System/currentTimeMillis))
(def threads (doall (for [i (range n)]
(let [t (Thread. (fn [] (swap! *cnt inc)))]
(.start t)
t))))
(println "Elapsed" (- (System/currentTimeMillis) start))
(println "cnt" @*cnt)
(System/exit 0)
>
time bb async.clj
1024000 Threads
Elapsed 48743
cnt 1024000
bb async.clj 34.36s user 47.61s system 167% cpu 48.860 total
So this is a small repro:
(require '[clojure.core.async :as async])
(def n 2000)
(let [begin (System/currentTimeMillis)
state (atom 0)]
(dotimes [_ n]
(async/go
(async/<!! (async/timeout 1))
(swap! state inc))
#_(async/<! (async/timeout 1000)) #_(async/go (async/<! (async/timeout 1000))
#_(swap! state inc)))
(while (< @state n))
(println "Spawned" @state "go blocks and finished in" (- (System/currentTimeMillis) begin) "ms"))
when changing async/go
to async/thread
the error goes away. also the error only occurs in the native image, not in a JVM that runs bb
(go (<!! …)
should be (go (<! …))
maybe. Otherwise it is similar to Thread/sleep
yeah, in this case (bb) it doesn't matter since they both map to the blocking option
I’m not sure what a good example is 😅 But you clearly did something special 🙂
I was also thinking maybe that 1024 queue size is related to the thread limit in core.async itself. With virtual threads maybe https://github.com/clojure/core.async/search?q=1024 can be increased to something higher (https://github.com/clojure/core.async/search?q=1024)
There is something odd with the timeout channel:
(def hashes (atom #{}))
(let [begin (System/currentTimeMillis)
state (atom 0)]
(dotimes [_ n]
(async/go
(let [chan (async/timeout 1)]
(swap! hashes conj chan)
(async/<! chan))
(swap! state inc)))
(while (< @state n))
(println "Spawned" @state "go blocks and finished in" (- (System/currentTimeMillis) begin) "ms")
(prn (count @hashes)))
It is as if they re-used:
$ clojure -J--enable-preview /tmp/go.bb
WARNING: Implicit use of clojure.main with options is deprecated, use -M
Spawned 2000 go blocks and finished in 41 ms
36
I would assume the timeout channel being unique each time, but that doesn't seem to be the case
hmm yeah indeed
I almost feared there was a concurrency issue with SCI and virtual threads, but I read the source here:
(or (when (and me (< (.getKey me) (+ timeout TIMEOUT_RESOLUTION_MS)))
(.channel ^TimeoutQueueEntry (.getValue me)))
(let [timeout-channel (channels/chan nil)
timeout-entry (TimeoutQueueEntry. timeout-channel timeout)]
(.put timeouts-map timeout timeout-entry)
(.put timeouts-queue timeout-entry)
timeout-channel))
so perhaps this behavior can also be reproduced with normal JVM + clojure... let's see
Yep, JVM repro:
(require '[clojure.core.async :as async])
(def ^java.util.concurrent.Executor virtual-executor
(java.util.concurrent.Executors/newVirtualThreadPerTaskExecutor))
(def n 200000)
(let [chans (atom #{})
begin (System/currentTimeMillis)
state (atom 0)]
(dotimes [_ n]
(.execute virtual-executor
(fn []
(let [chan (async/timeout 1)]
(swap! chans conj chan)
(async/<!! chan))
(swap! state inc))))
(while (< @state n))
(println "Spawned" @state "go blocks and finished in" (- (System/currentTimeMillis) begin) "ms")
(prn (count @chans)))
Linked discussion in #clojure channel: https://clojurians.slack.com/archives/C03S1KBA2/p1678466471709759
Now that I identified the issue I'm more confident that I can continue with virtual threads. I'll just make a virtual thread version of timeout
hmm weird not sure if I understand how the number of puts is related to the caching of the timeout channel :thinking_face:
oh wait now it’s number of takes. Never mind
Ah ok I was misunderstanding the issue the whole time 🙈
sorry, was thinking it was about puts
(defn timeout [ms]
(if virtual-executor
(let [chan (async/chan nil)]
(.submit virtual-executor (fn []
(Thread/sleep ms)
(async/close! chan)))
chan)
(async/timeout ms)))
Yeah I think this pending take is an issue for when you have many threads.. I was trying something myself, but ran into it again
(require '[clojure.core.async :as a])
(def n 100000)
(def *cnt (atom 0))
(def ch (a/chan 100000000))
(dotimes [i n]
(a/thread
(loop []
(let [v (a/<!! ch)]
(swap! *cnt inc)
(Thread/sleep 10)))))
(dotimes [i n]
(a/>!! ch n))
(Thread/sleep 1000)
(println "COUNTED" @*cnt " and n " n)
but I didn't expect calls like (async/timeout 1000)
returning identical channels (re-used channels) sometimes
Yeah true. It feels like it’s impossible to proof your improvement this way hehe
Nice! 🚀
Did you already find an upper limit of the number of virtual threads?
$ bb /tmp/go.bb
Spawned 20000000 go blocks and finished in 160668 ms
20M... I guess I could go on ;)Hi!
The docstring for babashka.cli/auto-coerce
says:
> starts with :, it is coerced as a keyword (through parse-keyword)
I'm seeing this:
(babashka.cli/auto-coerce ":remote-reference")
;; => ":remote-reference"
(type (babashka.cli/auto-coerce ":remote-reference"))
;; => java.lang.String
Is this expected behavior?
(System/getProperty "babashka.version")
;; => "1.2.174"
;;
;; org.babashka/cli {:mvn/version "0.6.48"}
this however does work:
user=> (babashka.cli/parse-args [":remote"])
{:opts {:remote true}}
Can you find out why? I'm going out for a walk, brb(don't worry about getting back to the computer, this isn't time sensitive for me at all)
Better example:
user=> (babashka.cli/parse-args [":remote" ":always"])
{:opts {:remote :always}}
it looks like the regex in auto-coerce
only matches if everything after the colon is letters/digits: <https://github.com/babashka/cli/blob/52e7eaa3755043a0db7638133d96e4e7e55c2eba/src/babashka/cli.cljc#L97>
user=> (cli/auto-coerce ":abc")
:abc ; keyword
user=> (cli/auto-coerce ":abc-def")
":abc-def" ; string
so perhaps you're avoiding things with spaces in them?
./mycli --title ":1 This is a title."
Some options:
;; single slash:
;;
;; [a-zA-Z][a-zA-Z0-9]+(/[a-zA-Z0-9-]+)?
;;
;; multiple slashes:
;;
;; [a-zA-Z][a-zA-Z0-9/]+
;;
;; no whitespace:
;;
;; (not (str/includes? s " "))
I feel like being a bit defensive on the auto-coercion and only supporting things that really are wanted to be keywords is a good thing.
let's do this: it has to start with an alphabetic character, and then followed by alphanumerics, _, - or /
Looking into this now, I've got a simple wrapper for cognitect.test-runner working: https://gist.github.com/pesterhazy/ec1dedd5c83ae47d981488f743042ca8
It looks at throwables and checks if they have (= :sci/error (:type e))
. This works fine, but not always. Sometimes I get exception that aren't wrapped in a sci-provided ex-info
https://clojurians.slack.com/archives/CLX41ASCS/p1678016771527859?thread_ts=1677844683.592549&cid=CLX41ASCS
Let's say I have test.clj and test_test.clj (which is the corresponding test)
Then if I (throw (Exception. "boom"))
in the deftest in test_test.clj, I get a wrapped error
However, if I throw one level down in test.cj, the error is not wrapped
That works, but what about doing this at a deeper level, in bb's clojure.test implementation?
Yeah that's the next step, I just wanted to make it work reliably
Which it isn't yet
As I sometimes get unwrapped errors like
#error {
:cause "boom"
:via
[{:type java.lang.Exception
:message "boom"
:at [java.lang.reflect.Constructor newInstanceWithCaller "Constructor.java" 500]}]
:trace
[[java.lang.reflect.Constructor newInstanceWithCaller "Constructor.java" 500]
[java.lang.reflect.Constructor newInstance "Constructor.java" 484]
[sci.impl.Reflector invokeConstructor "Reflector.java" 310]
[sci.impl.interop$invoke_constructor invokeStatic "interop.cljc" 78]
[sci.impl.analyzer$analyze_new$reify__4304 eval "analyzer.cljc" 1084]
[sci.impl.analyzer$analyze_throw$reify__4264 eval "analyzer.cljc" 968]
[sci.impl.analyzer$return_do$reify__3927 eval "analyzer.cljc" 124]
[sci.impl.fns$fun$arity_1__1166 invoke "fns.cljc" 107]
[sci.lang.Var invoke "lang.cljc" 200]
Basically I'm using it in my work project to iron out errors before (hopefully) moving it to clojure.stacktrace
However, if I throw one level down in test.cj, the error is not wrapped
The solution here is to catch it with:
(try ... (catch ^:sci/error Exception e))
Ah-ha
That's interesting. Bad news for me because I'm not controlling the try/catch here. I think it's here https://github.com/babashka/babashka/blob/master/src/babashka/impl/clojure/test.clj#L540
Could also be this https://github.com/babashka/babashka/blob/master/src/babashka/impl/clojure/test.clj#L720
But I think it's try-expr
Which defmethod do you mean? I'm not following
on a macro?! wow
20: (alter-var-root clojure.test/try-expr (constantly try-expr))
^--- Can't take value of a macro: #'clojure.test/try-expr
(alter-var-root #'clojure.test/try-expr (constantly @#'try-expr))
Ok this runs
(defmacro try-expr
"Used by the 'is' macro to catch unexpected exceptions.
You don't call this."
{:added "1.1"}
[msg form]
`(try ~(clojure.test/assert-expr msg form)
(catch ^:sci/error Exception t#
(clojure.test/do-report {:type :error, :message ~msg,
:expected '~form, :actual t#}))))
(alter-var-root #'clojure.test/try-expr (constantly @#'try-expr))
But I'm still seeing an unwrapped exception
Doesn't see to make a difference
The exception is caught in my monkey-patched try-expr but it's not wrapped
{:type java.lang.Exception
:message "boom"
:at [java.lang.reflect.Constructor newInstanceWithCaller "Constructor.java" 500]}
this is because in macro you should probably write:
~(with-meta 'Exception {:sci/error true})
or so
That worked! I'm not a macro pro
Added it to the gist https://gist.github.com/pesterhazy/ec1dedd5c83ae47d981488f743042ca8#file-core-clj-L11
I think there are similar problems with thrown? and thrown-with-msg? https://github.com/babashka/babashka/blob/master/src/babashka/impl/clojure/test.clj#L507
Would it make sense as a next step to upstream those changes into babashka.impl.clojure.test?
OK, I have to pick up my daughter now. Maybe I'll have some time next week In the meantime the gist is there if people want to try it
About to make use of babashka/json
given code that needs to be run from bb
and clj
, hence:
Caused by: clojure.lang.ExceptionInfo: defrecord/deftype currently only support protocol implementations, found: java.util.Iterator {:type :sci/error, :line 34, :column 1, :file "charred/coerce.clj"}
So happy that it was only recently born 🧡👶While I'm at it -- should babashka.cli/auto-coerce
support "nil"
too?
current behavior: (babashka.cli/auto-coerce "nil") => "nil"
proposed behavior: (babashka.cli/auto-coerce "nil") => nil
Any fzf-fans here who have been able to fully integrate fzf with babashka (e.g. via https://github.com/babashka/process)? By fully, I refer to the non-blocking nature of fzf, where it starts seemingly instantaneously, then searches and populates the selection on the fly, as opposed to everything upfront. I’ve used the example at https://github.com/babashka/babashka/blob/master/examples/fzf.clj , which covers most use-cases where the selection is small (e.g. listing git branches). However, if I want to, say, glob recursively in a huge project, it will block until the search is complete, before presenting the fzf-interface. This may just be a limitation in my understanding of https://github.com/babashka/process. I looked at the streaming options as well, but wasn’t sure where to go from here. Any pointers?
FZF fan here -- but I haven't tried streaming FZF input yet!
As far as I'm aware, process
can take stdin as an eager string, or as a lazy stream. So I believe streaming from a process command to find
or ls
should work.
Something like this perhaps: https://github.com/babashka/process#piping-output
This is working for me:
(require '[babashka.process :refer [shell process]])
(defn demo-3 []
(-> (process "ls")
(select-keys [:out])
(shell "fzf")))
Edit: it does something, but it doesn't look quite right.Got this one working with your tip (fd is a find-replacement)
#!/usr/bin/env bb
(ns file.fzf
(:require [babashka.process :as p]
[clojure.string :as str]))
(let [find (:out (p/process "fd . '/'"))
fzf @(p/process ["fzf" "-m"] {:in find
:out :string
:err :inherit})]
(->> fzf :out str/split-lines (mapv println)))
Didn’t even cross my mind that you could pass a not-yet-deref’d process as the input of another process, then deref the second without deref’ing the first :melting_face:
I think it should be possible to create a lazy sequence in clojure too and pass that to process
or shell
, but I haven't tried that either.
You might also be interested in babashka.fs/glob
if you're doing stuff like this:
user=> (prn (type (babashka.fs/glob "." "**/*.clj")))
clojure.lang.PersistentVector
nil
user=> (prn (babashka.fs/glob "." "**/*.clj"))
[#object[sun.nio.fs.UnixPath 0x1a7235f7 "src/teodorlu/shed/libclonecd.clj"] #object[sun.nio.fs.UnixPath 0xffd8e2c "src/teodorlu/shed/month.clj"] #object[sun.nio.fs.UnixPath 0x65282c8b "src/teodorlu/shed/update_repos.clj"] #object[sun.nio.fs.UnixPath 0x1d7cd503 "src/teodorlu/shed/path_lines.clj"] #object[sun.nio.fs.UnixPath 0x74f11d65 "src/teodorlu/shed/install.clj"] #object[sun.nio.fs.UnixPath 0x60e2d902 "src/teodorlu/shed/quick_clone.clj"] #object[sun.nio.fs.UnixPath 0x28e4e6cc "src/teodorlu/shed/libquickcd.clj"] #object[sun.nio.fs.UnixPath 0x651fbf8a "src/teodorlu/shed/fzf_stream_demo.clj"] #object[sun.nio.fs.UnixPath 0xf9394f5 "src/teodorlu/shed/explore.clj"] #object[sun.nio.fs.UnixPath 0x7429b4c9 "src/teodorlu/shed/browsetxt.clj"] #object[sun.nio.fs.UnixPath 0x1a6156d4 "src/teodorlu/shed/ukenummer.clj"]]
As far as I can tell, it's eager (persistent vector, not lazy seq).Glob was my initial input ( files (->> (fs/glob "." "**")
) but it blocks, so wasn’t suited for this use-case (searching tons of files incrementally)
Perhaps fs/glob and fs/match could have a callback based API as well. e.g. fs/glob-async
or so
is it possible to change the current directory with babashka? In Python:
>>> os.getcwd()
'/home/teodorlu'
>>> os.chdir("dev/teodorlu")
>>> os.getcwd()
'/home/teodorlu/dev/teodorlu'
The only option is to either source
some generated bash stuff from bb or start a new shell
in a different directory
Gotcha 👍
I'm trying to make an interactive babashka script that can change shell directories, which I asked about a bit earlier. I tried using a shell function that executes some of the babashka script's stdout, but ran into problems with my own user interface. Because I was messing with stdout, I got problems when I tried to shell out to less
. So I've decided to try to (process/exec (System/getenv SHELL))
instead.
For my case, I can probably just as well keep track of the current directory, and pass :dir *current-directory
into the places where that's required.
I ended up doing something like this:
(babashka.process/exec "zsh" "-c" (str "cd " (fs/absolutize loc) "; exec zsh"))
(https://github.com/teodorlu/shed/tree/841aac4344ccbe41783538800be0929231494697/src/teodorlu/shed/libquickcd.clj)
And it seems to work! 😁 Though there's plenty of pitfalls. Manually creating that cd
command will break on any path with spaces in it, and I have to invoke the shell twice to set the path. But it doesn't look like https://www.graalvm.org/sdk/javadoc/org/graalvm/nativeimage/ProcessProperties.html#exec-java.nio.file.Path-java.lang.String...- supports passing in a directory.
Now I'm going to see if this hacked-together Babashka cd replacement actually feels any good in practice.Is there a convenient way to distribute babashka projects? Clojure's uberjars are pretty nice, but I keep hitting walls with things that Clojure doesn't have that babashka does. Best would be "compile babashka AND my project using Graal", that sounds feasible, although I imagine it takes ages to build.
bbin is the recommended and easiest way for bb projects, but you can also do it using brew and other package managers, this just more work. Examples of projects that have been distributed like that are #C0400TZENE7 itself and #C03KCV7TM6F
Thanks.