Fork me on GitHub
#clojure
<
2023-01-19
>
didibus04:01:38

Is there a way I can try/catch a compile error? I have a macro that throws when it expands, and I want a test to assert that it does throw, but I can't try/catch around the macro call, because I'm guessing it expands before try/catch

didibus04:01:48

For example, how would I catch this:

(try
  (var i 1)
  (catch Exception e))
This does not work, the exception is still thrown.

didibus04:01:12

Hum, looks like I can use eval:

(try
  (eval '(var i 1))
  (catch Exception e))
Any better way?

Alex Miller (Clojure team)04:01:47

that's how we do it in clojure tests

👍 2
hiredman04:01:15

You cannot catch a compiler error in the same compilation unit as the error

Ravinder Ram06:01:55

how to remove this warning from Pesdestal web framework

thheller10:01:49

upgrade your tools.analyzer dependency. could maybe just be a pedestal dependency bump or specifically [org.clojure/tools.analyzer "1.1.1"]

Asher Serling08:01:37

I'm getting this error when I run migrations with migratus

Message: class com.mysql.cj.jdbc.ConnectionImpl cannot be cast to class java.sql.PreparedStatement (com.mysql.cj.jdbc.ConnectionImpl is in unnamed module of loader 'app'; java.sql.PreparedStatement is in module java.sql of loader 'platform')
specifically in prod where the app is deployed as a .jar file. What is likely to be the issue?

roklenarcic08:01:07

Seems like you’re putting arguments where they don’t belong.

roklenarcic08:01:42

You are calling a function with connection where the argument should have been a prepared statement

Asher Serling08:01:09

it does seem that way

Asher Serling08:01:25

any idea why that should happen only in one environment but not in another?

roklenarcic08:01:48

hard to say without seeing the whole setup and code

Asher Serling08:01:56

The migratus config

(ns ai-clojure.database.migrations
  (:require [ai-clojure.config :refer [env]] 
            [clojure.pprint :as pp]
            [migratus.core :as migratus]))


(defn config []
  {:store                :database
   :migration-dir        "resources/migrations"
   :init-script          "init.sql" ;script should be located in the :migration-dir path 
   :init-in-transaction? false
   :migration-table-name "migrations"
   :db {:dbtype "mysql"
        :dbname (:db-database env)
        :host (:db-host env)
        :port (:db-port env)
        :user (:db-user env)
        :password (:db-password env)}})

(defn migrate []
  (migratus/migrate (config)))

Asher Serling08:01:20

the calling code

(defn start-app [args]
  (doseq [component (-> args
                        (parse-opts cli-options)
                        mount/start-with-args
                        :started)]
    (log/info component "started"))

  (when (= (:app-env env) "PRODUCTION")
    (try (do (log/info "Production environment; running migrations")
             (migrate))
         (catch Exception e (logging-utils/log-exception e)))) 
  
  (.addShutdownHook (Runtime/getRuntime) (Thread. stop-app)))

Asher Serling08:01:07

in dev the env variables come from dev-config.edn, in live they are environment properties for a aws elastic beanstalk instance

roklenarcic08:01:14

do you have just SQL files for migrations or do you also have clojure code migrations?

roklenarcic08:01:20

and how does dev env run migrate? via repl?

Asher Serling08:01:30

i just switched out the env = prod for true

roklenarcic08:01:16

Well one thing you can do is print out return of (config) in migrate then compare what you get there in both environments

roklenarcic08:01:44

The other thing you could do is open up the jar you use in production and look if there are all files from resources in there that you expect

roklenarcic08:01:59

jar is just a zip file

Asher Serling08:01:13

ok i'll try those things thank you

Asher Serling08:01:35

i did print out full contents of env in prod and found it to be as expected

Asher Serling08:01:12

i dont see the resources folder inside the .jar at all

Asher Serling08:01:38

but something is working, because the html template does get served

roklenarcic08:01:02

there shouldn’t be a literal resource folder inside jar

roklenarcic08:01:13

but the contents of the resource folder should be in the jar

Asher Serling08:01:33

where in the jar should the contents be?

Asher Serling08:01:57

i was looking under the folder named like my app

Asher Serling08:01:39

config looks good

roklenarcic09:01:34

change migration dir to “migrations/”

Asher Serling09:01:16

i don't need to prefix it with resources?

roklenarcic09:01:30

no, why would you need to do that?

roklenarcic09:01:54

contents of resources dir get included in jar at base folder

roklenarcic09:01:06

so you get a dir migrations in jar

Asher Serling09:01:34

i see great thank you

roklenarcic09:01:36

so when you try to read a file resources/migrations/a.sql it works when you run it locally, but in production there is no such file. This is why most things also try to load via classloader (e.g. io/resource ) in which case the actual path is “migrations/a.sql”

roklenarcic09:01:22

classloader loads from classpath, in your local dev environment your resources folder is on your classpath, in production the jar is on your classpath

Asher Serling09:01:51

wow that is so clarifying

Asher Serling09:01:20

i still have the same error as before, but we certainly just prevented another error i'd have after getting past this one

roklenarcic09:01:27

you can of course have any number of “resources” folders on classpath, any number of jars. JVM makes no special distinction for resources, it’s just a folder on classpath, same as src folder, the fact that you put noncode there and code in src is just your project structure and makes no difference to JVM, you could put your migration in `/src/migrations/a.sql” and set migration folder to “migrations/” and it would work in dev

Asher Serling09:01:13

so if i have two 'resources' folders on the classpath, and in each one i have a folder called 'x', what happens in the .jar file? merged?

roklenarcic09:01:54

no, it will load one, which one is undefined behaviour, but likely the one from the folder that appears on classpath the first, there is no guarantee from JVM spec. It’s the same situation if you have 2 jars on the classpath that supply the same class (e.g. two versions of Jackson).

roklenarcic09:01:10

Normally in clojure people use uberjar

roklenarcic09:01:26

which already does this, because it includes just one copy of everything

roklenarcic09:01:50

you can tell uberjar to combine files and how to combine them

roklenarcic09:01:25

very important for duct-hierarchy.edn

roklenarcic09:01:46

in lein project:

:uberjar-merge-with {"duct_hierarchy.edn" [(fn [in] (clojure.edn/read-string (slurp in)))
                                             clojure.core/merge
                                             (fn [out datum] (spit out (prn-str datum)))]}

roklenarcic09:01:07

since you need to have 1 duct-hierarchy.edn file that is a combination of all of them

Asher Serling09:01:15

any other ideas about what may be causing the error I'm having?

Asher Serling09:01:33

btw i'm about to leave the computer for around an hour+ if i don't response

Asher Serling09:01:47

thanks for all your help, this has been enlightening

roklenarcic08:01:56

I was looking at equals implementations in persistent vector and persistent queue and what I found very surprising is lack of use of hashcode. I had always assumed that, given the immutability of datastructures, hashcode was always calculated, cached and when doing equality comparisons, it would first compare hashcodes, before comparing elements. But that doesn’t seem to be the case, very interesting.

Hendrik08:01:55

I stumbled across this, too. My thought was: Why doesn’t it make a simple identity (pointer comparison) as a first check?

p-himik09:01:18

Some reasons I can think of right now: • Computing a vector's hash necessarily traverses the whole vector (and the hash is not always calculated - only when it's needed, and cached afterwards), whereas comparing two vectors might short-circuit on the first element or even on the length comparison • Using hashes would rely on all the items being good citizens and also using the hash semantics correctly, which might not always be the case (although it would probably become apparent by itself)

p-himik09:01:16

Although, which makes it all even more interesting, comparing a vector to a list does use hashes of the collections.

p-himik09:01:40

Worth a post on http://ask.clojure.org, I'd say. ;)

roklenarcic09:01:46

Hm… but then instead of using hashCode which requires that you always calculate hashcode on call, we could have a method called, hashCodeIfCached, which would use it if it was calculated already, otherwise return 0.

roklenarcic09:01:23

or _hasheq if cached

roklenarcic09:01:43

which is more likely to have been calculated, if the object was used as a map key at any time

p-himik09:01:37

An existing relevant thread, might make sense to add a comment there instead of creating a new post: https://ask.clojure.org/index.php/11124/persistent-collections-implement-equiv-more-efficiently?show=11124

borkdude12:01:02

equiv already does the identity check. I think clojure kind of assumes that you go through =

Alex Miller (Clojure team)13:01:34

You can’t use hashes to check for positive equivalence because of collisions

👍 2
Alex Miller (Clojure team)13:01:46

You can use them for negative equivalence (fail fast) in some cases but keep in mind that the set of collection implementations is open

chrisn15:01:29

ham-fisted allows you to create hashtables with various hash and equals providers if you want to mess with these things. Also, I am open to changing the chunked vector implementation to whatever works best - I don't have the compatibility or stability requirements of core Clojure. For instance I think there is an argument for not using murmur at all for integers - the java hashmap impl has a clever work around and overally their impl is much faster in all tested cases. So if you want to explore this further - that is why I created ham-fisted. We can safely explore these questions and you can use the results in your program without changing the entire Clojure ecosystem.

chrisn15:01:23

Another thought is that I think there isn't a need for separate hasheq and hashCode impls - I haven't seen concretely a program that breaks if we just use hasheq for hashcode in our datastructures. Similar for equiv and equals although I imagine there are serialization instances. So another thing I question is whether it is necessary to have a parallel and slightly different arch for equals and equiv, hashcode and hasheq.

chrisn15:01:06

Again, ham-fisted is where you can test this out and try it in your systems in a limited sense without needing nearly the rigour we need if we want to force these changes on the entire ecosystem.

Alex Miller (Clojure team)15:01:07

it is necessary - algorithms for Java colls are defined in the collections lib so we need to match those for hashcodes

Alex Miller (Clojure team)15:01:46

and similarly, clojure's notion of equivalence is different than Java's

chrisn00:01:12

I think the first is in question for me. It isn't clear to me why a persistent hash map must have the same hash, equals as a java hashmap or persistent vectors and arraylists but I can definitely respect the decision. I think clojure's equiv pathway overall makes a lot more logical sense than java's rather fragile equals pathway. I think mumur3 for integers in general isn't necessary - java in general just returns the int itself or the long xoring the high bits into the low bits and as I said the java hashmap also does a mask of high bits into low bits to make sure the high bits participate in hashmaps that aren't gargantuan. In any case, there are easy wins in just the Util.equiv without changing any definitions and this tends to be the slow path in lots of stuff. One thing I noticed is that special casing long-long and double-double comparisons helps quote a lot and doesn't risk breaking other things - https://github.com/cnuernber/ham-fisted/blob/master/java/ham_fisted/CljHash.java#L36.

Alex Miller (Clojure team)02:01:43

java's collection api defines things that must be true about implementations of the collection interfaces, such as: https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#hashCode--

Alex Miller (Clojure team)02:01:24

we wish Clojure maps to be good citizens in the Java world in this respect so that Clojure maps can (invisibly) acts as java.util.Maps if passed to such interfaces

Alex Miller (Clojure team)02:01:17

we found the integer thing was problematic because it was trivial to get hashcode collisions for combinations of positive and negative integers (but changing to murmur on hashed colls may have made that a non-issue). we did look at re-hashing, can't remember what the decision was based on (that was almost a decade ago now so it's a little fuzzy, and I think Rich did some of that work by himself)

roklenarcic20:01:40

Yes, if two objects are equal, they must have same hash, that’s the contract. Of course this really impossible to guarantee in the java world. You immutable collection might have an element that is a java object that can be mutated and returns a different hashcode every time. In general, map implementations ignore this possibility, which leads to “fun” things e.g.: Let’s say you have class X that has a single int member that can be mutated via a setter and hashCode for X just returns the member (actual implementation doesn’t matter, any hashcode implementation relying on the mutable member exhibits same problem.) You can actually insert the same object (instance of X) into HashSet 3 times, if you just change the int member to 1, 2 and 3 before each .add call. Hashcode changes and it lands in different, empty bucket, so it gets added. If it happens to land in the same bucket where it’s already there, it will not be added, because .equals will figure out that it’s the same object as the one in the bucket already. Fun stuff.

Adam Helins15:01:06

Does someone have any insights about dynamic vars, binding , and virtual threads? Are there any gotchas?

👀 2
borkdude15:01:17

I've been wondering about this too. It seems to work correctly but there is some performance penalty for ThreadLocals with "big objects" but what exactly they (JVM docs) mean by this, isn't clear and I couldn't repro any case so far where it matters

feng16:01:51

thread binding means each thread have such a var

feng16:01:53

it's not virtual thread, it's native thread

borkdude17:01:15

@U01125FLA02 yes, but there is an interaction between real threads and virtual threads

chrisn13:01:33

That is a great article. The advice at the bottom of the article - use semaphores instead of thread pools and use reentrant locks instead of syncrhonized (`locking` in clojure) - those are somewhat architectural changes. I wonder if we can leverage the Clojure compiler to make those changes a bit more automatic.

chrisn13:01:47

It appears the advice against thread locals is simply that if you can have millions of threads the cost of each thread local is much greater so be careful and attempt to limit them. Specifically relating to binding I think that uses 1 thread local (https://github.com/clojure/clojure/blob/e6fce5a42ba78fadcde00186c0b0c3cd00f45435/src/jvm/clojure/lang/Var.java#L71) so I think you are fine aside from the normal perf hits you take from dynamic variables.

chrisn13:01:23

Related - scoped local variables - https://openjdk.org/jeps/429

Noah Bogart21:01:50

What's the best way to type hint interop that can take multiple types? The one I'm looking at right now is (.decode (java.util.Base64/getDecoder) value) . .decode can take a byte array or a string, and I'm not sure that we're only passing in one or the other in our codebase. I could say (cond (string? value) (.decode ... ^String value) (bytes? value) (.decode ... ^bytes value)), but that seems like i'm losing speed merely to appease the reflection warnings. Maybe it doesn't matter? How do y'all handle this?

2
Alex Miller (Clojure team)21:01:15

well make up your mind - do you want dynamic typing or not? :)

😂 4
Alex Miller (Clojure team)21:01:39

if you want to avoid reflection, then you need to pick a lane

Alex Miller (Clojure team)21:01:54

if you want to support multiple types, then you need to explicitly check and type hint each path

Alex Miller (Clojure team)21:01:36

the runtime check is generally much faster than the reflective call

👍 2
Alex Miller (Clojure team)21:01:03

(maybe not true in very recent jvms that are replacing reflection impl with indy)

👍 2
Noah Bogart21:01:20

I only care because I've had some errors within this code path and the reflection calls obfuscate the source of the error in the stack

Noah Bogart21:01:54

and because it's a fun rabbit hole to fall down lol

Alex Miller (Clojure team)21:01:06

the jvm can generally optimize this kind of conditional well, esp if it's usually one type (monomorphic)

phronmophobic22:01:22

out of curiosity, how would that compare to using a protocol? something like:

(defprotocol IDecode
  (decode [this]))

(extend-protocol IDecode
  String
  (decode [s]
    (.decode (java.util.Base64/getDecoder) s)))
(extend (Class/forName "[B")
  IDecode
  {:decode (fn [^bytes b]
             (.decode (java.util.Base64/getDecoder) b))})

Alex Miller (Clojure team)22:01:48

that doesn't have an explicit if test but you can get an idea

Noah Bogart22:01:53

Wow, those timings are wild. Thanks for the link!

borkdude22:01:34

> (maybe not true in very recent jvms that are replacing reflection impl with indy) what's this?

borkdude22:01:34

ah, invoke dynamic

Alex Miller (Clojure team)22:01:14

I think that's in Java 18?

Darin Douglass22:01:58

@U7RJTCH6J that’s what i would reach for

jumar04:01:34

Does https://openjdk.org/jeps/416 mean that Clojure code can get potentially significant speedups? (in the JEP they seem to be talking about reducing the maintenance and development cost )

roklenarcic08:01:20

If you have a handful of known types then dispatching by using instanceof is still way faster than reflection.