boot-clj / boot

Build tooling for Clojure.
https://boot-clj.github.io/
Eclipse Public License 1.0
1.75k stars 180 forks source link

PODs vs JNI #484

Open schmir opened 8 years ago

schmir commented 8 years ago

I'm trying to use boot-test. I'm using clojure 1.8.0, boot 2.6.0 on a jdk 8 on Linux. My project uses a JNI based library and I'm not able to use boot-test together with the watch task. If I run "boot test" everything is fine:

(local) ~/steinmetz (git)-[master] % boot test
...
Testing steinmetz.zvk

Testing steinmetz.zvk-test

Ran 20 tests containing 50 assertions.
0 failures, 0 errors.

However, I'm not able to use boot watch test:

30 (local) ~/steinmetz (git)-[master] % boot watch test

Starting file watcher (CTRL-C to quit)...
...
Testing steinmetz.zvk

Testing steinmetz.zvk-test

Ran 20 tests containing 50 assertions.
0 failures, 0 errors.
Elapsed time: 12.677 sec

(which looks good until I change a file)

clojure.lang.Compiler$CompilerException: java.lang.UnsatisfiedLinkError: Native Library /home/ralf/.libsepa/linux-64/libsepa271/libsepa-jni.so already loaded in another classloader, compiling:(clj_libsepa/core.clj:9:3)
     java.lang.UnsatisfiedLinkError: Native Library /home/ralf/.libsepa/linux-64/libsepa271/libsepa-jni.so already loaded in another classloader
                           ...
                 com.libsepa.SEPA.<clinit>
                           ...
                  clojure.core/load/fn          core.clj: 5893
            clojure.core/load/invokeStatic          core.clj: 5892
                     clojure.core/load          core.clj: 5876
                           ...
            clojure.core/load-one/invokeStatic          core.clj: 5697
                 clojure.core/load-one          core.clj: 5692
                  clojure.core/load-lib/fn          core.clj: 5737
            clojure.core/load-lib/invokeStatic          core.clj: 5736
                 clojure.core/load-lib          core.clj: 5717
                           ...
               clojure.core/apply/invokeStatic          core.clj:  648
           clojure.core/load-libs/invokeStatic          core.clj: 5774
                clojure.core/load-libs          core.clj: 5758
                           ...
               clojure.core/apply/invokeStatic          core.clj:  648
             clojure.core/require/invokeStatic          core.clj: 5796
                  clojure.core/require          core.clj: 5796
                           ...
       steinmetz.libsepa/eval27644/loading--auto--       libsepa.clj:    1
          steinmetz.libsepa/eval27644/invokeStatic       libsepa.clj:    1
               steinmetz.libsepa/eval27644       libsepa.clj:    1
                           ...
                  clojure.core/load/fn          core.clj: 5893
            clojure.core/load/invokeStatic          core.clj: 5892
                     clojure.core/load          core.clj: 5876
                           ...
            clojure.core/load-one/invokeStatic          core.clj: 5697
                 clojure.core/load-one          core.clj: 5692
                  clojure.core/load-lib/fn          core.clj: 5737
            clojure.core/load-lib/invokeStatic          core.clj: 5736
                 clojure.core/load-lib          core.clj: 5717
                           ...
               clojure.core/apply/invokeStatic          core.clj:  648
           clojure.core/load-libs/invokeStatic          core.clj: 5774
                clojure.core/load-libs          core.clj: 5758
                           ...
               clojure.core/apply/invokeStatic          core.clj:  648
             clojure.core/require/invokeStatic          core.clj: 5796
                  clojure.core/require          core.clj: 5796
                           ...
steinmetz.qlauf.ueberweisung/eval26343/loading--auto--  ueberweisung.clj:    1
   steinmetz.qlauf.ueberweisung/eval26343/invokeStatic  ueberweisung.clj:    1
        steinmetz.qlauf.ueberweisung/eval26343  ueberweisung.clj:    1
                           ...
                  clojure.core/load/fn          core.clj: 5893
            clojure.core/load/invokeStatic          core.clj: 5892
                     clojure.core/load          core.clj: 5876
                           ...
            clojure.core/load-one/invokeStatic          core.clj: 5697
                 clojure.core/load-one          core.clj: 5692
                  clojure.core/load-lib/fn          core.clj: 5737
            clojure.core/load-lib/invokeStatic          core.clj: 5736
                 clojure.core/load-lib          core.clj: 5717
                           ...
               clojure.core/apply/invokeStatic          core.clj:  648
           clojure.core/load-libs/invokeStatic          core.clj: 5774
                clojure.core/load-libs          core.clj: 5758
                           ...
               clojure.core/apply/invokeStatic          core.clj:  648
             clojure.core/require/invokeStatic          core.clj: 5796
                  clojure.core/require          core.clj: 5796
                           ...
      steinmetz.render.qlauf/eval26335/loading--auto--         qlauf.clj:    1
     steinmetz.render.qlauf/eval26335/invokeStatic         qlauf.clj:    1
              steinmetz.render.qlauf/eval26335         qlauf.clj:    1
                           ...
                  clojure.core/load/fn          core.clj: 5893
            clojure.core/load/invokeStatic          core.clj: 5892
                     clojure.core/load          core.clj: 5876
                           ...
            clojure.core/load-one/invokeStatic          core.clj: 5697
                 clojure.core/load-one          core.clj: 5692
                  clojure.core/load-lib/fn          core.clj: 5737
            clojure.core/load-lib/invokeStatic          core.clj: 5736
                 clojure.core/load-lib          core.clj: 5717
                           ...
               clojure.core/apply/invokeStatic          core.clj:  648
           clojure.core/load-libs/invokeStatic          core.clj: 5774
                clojure.core/load-libs          core.clj: 5758
                           ...
               clojure.core/apply/invokeStatic          core.clj:  648
             clojure.core/require/invokeStatic          core.clj: 5796
                  clojure.core/require          core.clj: 5796
                           ...
     steinmetz.http.core/eval19626/loading--auto--          core.clj:    1
        steinmetz.http.core/eval19626/invokeStatic          core.clj:    1
             steinmetz.http.core/eval19626          core.clj:    1
                           ...
                  clojure.core/load/fn          core.clj: 5893
            clojure.core/load/invokeStatic          core.clj: 5892
                     clojure.core/load          core.clj: 5876
                           ...
            clojure.core/load-one/invokeStatic          core.clj: 5697
                 clojure.core/load-one          core.clj: 5692
                  clojure.core/load-lib/fn          core.clj: 5737
            clojure.core/load-lib/invokeStatic          core.clj: 5736
                 clojure.core/load-lib          core.clj: 5717
                           ...
               clojure.core/apply/invokeStatic          core.clj:  648
           clojure.core/load-libs/invokeStatic          core.clj: 5774
                clojure.core/load-libs          core.clj: 5758
                           ...
               clojure.core/apply/invokeStatic          core.clj:  648
             clojure.core/require/invokeStatic          core.clj: 5796
                  clojure.core/require          core.clj: 5796
                           ...
          steinmetz.http/eval19475/loading--auto--          http.clj:    1
         steinmetz.http/eval19475/invokeStatic          http.clj:    1
                  steinmetz.http/eval19475          http.clj:    1
                           ...
                  clojure.core/load/fn          core.clj: 5893
            clojure.core/load/invokeStatic          core.clj: 5892
                     clojure.core/load          core.clj: 5876
                           ...
            clojure.core/load-one/invokeStatic          core.clj: 5697
                 clojure.core/load-one          core.clj: 5692
                  clojure.core/load-lib/fn          core.clj: 5737
            clojure.core/load-lib/invokeStatic          core.clj: 5736
                 clojure.core/load-lib          core.clj: 5717
                           ...
               clojure.core/apply/invokeStatic          core.clj:  648
           clojure.core/load-libs/invokeStatic          core.clj: 5774
                clojure.core/load-libs          core.clj: 5758
                           ...
               clojure.core/apply/invokeStatic          core.clj:  648
             clojure.core/require/invokeStatic          core.clj: 5796
                  clojure.core/require          core.clj: 5796
                           ...
                  pod$eval504.invokeStatic    NO_SOURCE_FILE
                    pod$eval504.invoke    NO_SOURCE_FILE
                           ...
            clojure.core/eval/invokeStatic          core.clj: 3105
                     clojure.core/eval          core.clj: 3101
                     boot.pod/eval-in*           pod.clj:  437
                           ...
                     boot.pod/eval-in*           pod.clj:  440
             adzerk.boot-test/eval161/fn/fn/fn     boot_test.clj:  102
          boot.task.built-in/fn/fn/fn/fn/fn/fn      built_in.clj:  348
             boot.task.built-in/fn/fn/fn/fn/fn      built_in.clj:  348
            boot.task.built-in/fn/fn/fn/fn      built_in.clj:  345
                   boot.core/run-tasks          core.clj:  938
                     boot.core/boot/fn          core.clj:  948
           clojure.core/binding-conveyor-fn/fn          core.clj: 1938
                           ...
Elapsed time: 9.029 sec

I've ran into the same issue while trying to use clojurescript with my project. So, running "boot test" manually is not a workaround for me here.

micha commented 8 years ago

I don't know of any mechanism by which you can isolate JNI libraries in the JVM. This means that the code that calls loadLibrary() needs to check to make sure the library isn't already loaded before it loads it. I don't know of any robust way to manage native libraries, so Boot just stays out of your way there.

Do you have a recommendation for how to deal with these issues?

schmir commented 8 years ago

Sorry, but I don't have any recommendation, nor do I have any clue about classloaders. I've tried to search google and it looks like running JNI libs in Tomcat also may trigger that error.

I've tried to load the library directly from build.boot, but that didn't help.

If time permits I'll take another look at that issue. At least I can learn something about classloaders.

schmir commented 8 years ago

I've tried to ignore errors by wrapping the loadLibrary code in a try/catch block. That didn't work since I've later got an UnsatisfiedLinkError, when trying to call functions from the package.

Then I've took a look at the classloader implementation in openjdk. It looks like the above error is thrown here:

http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/lang/ClassLoader.java#1901

It's possible to modify the loadedLibraryNames, with the following code:

(defn filter-loaded-library-names
  [pred]
  (let [loaded-library-names (.getDeclaredField java.lang.ClassLoader "loadedLibraryNames")
        loaded-library-names-vector (do (.setAccessible loaded-library-names true)
                                        (.get loaded-library-names java.lang.ClassLoader))
        filename-idx (mapv vector loaded-library-names-vector (range))]
    (doseq [[filename idx] (->> filename-idx
                                (remove (comp pred first))
                                reverse)]
      #_(println "REMOVE" filename idx)
      (.removeElementAt loaded-library-names-vector idx))))

;: adapt this:
(filter-loaded-library-names (fn [filename] (not (str/index-of filename "sepa-jni"))))

I'm now able to use 'boot watch test' with this workaround.

I'm not sure if it would make sense to have that workaround in boot. At least it may be useful to have a link in the documentation to this issue.

I'm also not sure why I seem to be the first person running into that problem.

mobileink commented 8 years ago

On Aug 1, 2016 2:27 PM, "Ralf Schmitt" notifications@github.com wrote:

I've tried to ignore errors by wrapping the loadLibrary code in a try/catch block. That didn't work since I've later got an UnsatisfiedLinkError, when trying to call functions from the package.

Then I've took a look at the classloader implementation in openjdk. It looks like the above error is thrown here:

http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/lang/ClassLoader.java#1901

It's possible to modify the loadedLibraryNames, with the following code:

(defn filter-loaded-library-names [pred](let [loaded-library-names %28.getDeclaredField java.lang.ClassLoader) loaded-library-names-vector (do (.setAccessible loaded-library-names true) (.get loaded-library-names java.lang.ClassLoader)) filename-idx (mapv vector loaded-library-names-vector (range))] (doseq [[filename idx](->> filename-idx %28remove %28comp pred first%29%29 reverse)] #_(println "REMOVE" filename idx) (.removeElementAt loaded-library-names-vector idx)))) ;: adapt this: (filter-loaded-library-names (fn [filename](not %28str/index-of filename))))

I'm now able to use 'boot watch test' with this workaround.

I'm not sure if it would make sense to have that workaround in boot. At least it may be useful to have a link in the documentation to this issue.

I'm also not sure why I seem to be the first person running into that problem.

FWIW I was planning to run into that problem. Glad you got there first, cause I never would have been able to figure it out Thanks!

mtnygard commented 8 years ago

I ran into a similar problem on a different project. JNI requires that a given library be loaded at most once within a JVM. However, even if the library was previously loaded in the same process, if it was loaded by a different classloader, then the same UnsatisfiedLinkError will result.

The only solution I've ever found is to force the library to load at a very high level classloader, such that all descendants will resolve the JNI classes (and the linked library) via the application classloader.

I have no idea whether this would be possible with pods. Do pods ultimately delegate classloading up to the boot classloader?

micha commented 8 years ago

@mtnygard Pods don't do anything unusual with class loading, they use normal URLClassLoaders that do parent-first delegation. The boot.App class is loaded from a higher-level class loader, and all pods are siblings whose parent class loader is the one that spawned boot.App.

Perhaps it would be possible to monkey-patch System.loadLibrary(String libname) in pods? If such a thing is possible then we could perhaps have that method delegate to a similar method on the boot.App class that can manage the state?

samestep commented 8 years ago

I ran into this same issue while playing around with boot-test in my clj-vulkan-guide project. You can reproduce the issue by checking out the test branch and running boot test test.