Fork me on GitHub
#babashka
<
2022-08-25
>
Crispin07:08:59

Would it be possible to implement a finalize method on a java object in bb? clojure:

user=> (reify java.lang.Object (finalize [this] (println "collected")))
#object[user$eval138$reify__139 0x29182679 "user$eval138$reify__139@29182679"]
bb:
user=> (reify java.lang.Object (finalize [this] (println "collected")))
java.lang.ClassCastException: clojure.lang.Symbol cannot be cast to java.lang.Throwable [at <repl>:1:1]

borkdude16:08:39

Theoretically maybe. Could you give more info on why?

Crispin00:08:00

I have a pod. And on the pod side I have objects on the heap. And on the bb side I can new those objects. And the bb gets a keyword to refer to the instance, and uses that keyword to refer to it in future calls. This is all cool, except the instance may get garbage collected over on the pod. So, I can keep a reference to it so it doesn't. But then I need a delete call on the bb side to clean that up. And the user needs to manage those instances or they will build up and use more and more ram. It would be great if the bb side could automatically send those delete calls when the referering keyword (or other object) on the bb side gets garbage collected. Essentially moving the life-cycle management from the pod to the bb garbage collector.

Crispin00:08:06

There are two ways I can think of achieving this. One is to have the referring object on the bb side call something in its finalize. This could be a finalize on a clojure.lang.Keyword, but that may be difficult/impossible even in clojure (https://clojure.atlassian.net/browse/CLJ-911). Or the object with finalize may not be a keyword, but a keyword-like re-implementation. A reified instance implementing all the protocols that a Keyword does (IFn, Comparable, Named, Serializable, IHashEq).

Crispin00:08:02

The other way might be to have a java.util.WeakHashMap, java.lang.ref.WeakReference and a java.lang.ref.ReferenceQueue to register the keywords into and trigger the appropriate delete calls on GC.

Crispin00:08:05

(bb has java.util.WeakHashMap exposed, but not java.lang.ref.WeakReference or java.lang.ref.ReferenceQueue)

Crispin00:08:36

I would appreciate your learned perspective on which approach to use in bb 🙂

Crispin04:08:27

Ok this morning I have been doing some experiments with weakreferences. Added java.lang.ref.WeakReference to bb. Some interesting discoveries. Triggering GC in clojure doesn't destruct keywords. But it does destruct java Objects. And in bb also keywords don't destruct. But sometimes objects do, and sometimes they do not. I wonder if something holds a reference to keywords inside clojure. And similarly in bb with Objects (but only sometimes?)

Crispin04:08:42

Here is my test-code

Crispin04:08:09

(ns test
  (:import [java.lang.ref WeakReference]))

(def weakref (atom nil))

(defn gc []
  (println "gc")
  (System/gc)
  (System/runFinalization)
  (println "gc done")
  )

(defn force-gc []
  (println "force-gc")
  (let [t (atom (Object.))
        wr (WeakReference. @t)]
    (reset! t nil)
    (while (.get wr)
      (System/gc)
      (System/runFinalization)))
  (println "force-gc done"))

(defn main []
  (let [a (Object.)]
    (println "inside")
    (reset! weakref (WeakReference. a))
    (println "a:" a)
    (println "wr:" (.get @weakref))
    (force-gc)
    (println "a:" a)
    (println "wr:" (.get @weakref)))
  (println "outside")
  (println "wr:" (.get @weakref))
  (force-gc)
  (println "wr:" (.get @weakref))
  )

(main)

Crispin04:08:01

I have a standard gc call (that may or may not actually trigger a gc) and a force-gc which blocks until it does

Crispin04:08:21

a is being set to an Object instance here

Crispin04:08:27

using clojure:

Crispin04:08:43

$ clj test/bb-gc.clj
inside
a: #object[java.lang.Object 0x5167268 java.lang.Object@5167268]
wr: #object[java.lang.Object 0x5167268 java.lang.Object@5167268]
force-gc
force-gc done
a: #object[java.lang.Object 0x5167268 java.lang.Object@5167268]
wr: #object[java.lang.Object 0x5167268 java.lang.Object@5167268]
outside
wr: #object[java.lang.Object 0x5167268 java.lang.Object@5167268]
force-gc
force-gc done
wr: nil

Crispin04:08:33

so the final force GC does destruct the Object (`wr: nil`)

Crispin04:08:53

$ bb test/bb-gc.clj
inside
a: #object[java.lang.Object 0x2d3102ab java.lang.Object@2d3102ab]
wr: #object[java.lang.Object 0x2d3102ab java.lang.Object@2d3102ab]
force-gc
force-gc done
a: #object[java.lang.Object 0x2d3102ab java.lang.Object@2d3102ab]
wr: #object[java.lang.Object 0x2d3102ab java.lang.Object@2d3102ab]
outside
wr: #object[java.lang.Object 0x2d3102ab java.lang.Object@2d3102ab]
force-gc
force-gc done
wr: #object[java.lang.Object 0x2d3102ab java.lang.Object@2d3102ab]

Crispin04:08:04

it does not

Crispin04:08:47

but it's interesting... because the WeakReference used to check if GC had actually run, used inside force-gc does work in bb

Crispin04:08:21

so one Object gets destroyed by GC, but the other does not, as if something hidden is holding a reference to it

Crispin05:08:15

OK... bit more info... making the end of this test code:

Crispin05:08:22

(main)
(force-gc)
(println "wr:" (.get @weakref))

Crispin05:08:41

and the GC does clean up the object in bb. So it seems that internally it doesnt hold a ref in the use of the atom (inside force-gc), it doesn't hold a ref on exit from a function (`main`) but it does hold a ref on exit from a let body

Crispin05:08:26

I think a java.lang.ref.WeakReference might be enough to move pod object lifecycle control into bb mainline...

Crispin05:08:50

I will do some test code and see if I can get it working

Crispin06:08:26

ok.. some more info for the interested. Keywords do GC, but not if they are every used as a literal (obvious in retrospect). So (keyword "foo") behaves just like (Object.). But :foo never GCs

borkdude07:08:22

You can add finalize to reify/src/babashka/impl/reify2.clj to experiment with this

borkdude07:08:35

and if that works for you, then make a PR

Crispin07:08:57

awesome, yes. I am in full experiment mode atm so is a good time to do it

Crispin09:08:19

so by the looks of it, bb reify can only reify a single interface?

Crispin09:08:05

interfaces is a set, too. So if you do a multi interface reify, which interface actually has its methods implemented is not obvious

borkdude09:08:03

yes, only a single interface is supported. supporting multiple would lead to a combinatorial explosion

Crispin09:08:07

user=> (def a (reify java.lang.Object (toString [_] "do") clojure.lang.Seqable (seq [_] '(1 2 3))))
#'user/a
user=> (seq a)
(1 2 3)
user=> (str a)
"babashka.impl.clojure.lang.Seqable@615b1baf"

Crispin09:08:27

yeah I can see that

borkdude09:08:50

Object / toString is a bit of an exception I would say, we could hardcode that

Crispin09:08:29

I reimplemented toString to get an optional toString method with the finalize

Crispin09:08:50

then came across the single interface issue

borkdude09:08:05

we could special case object probably

Crispin09:08:20

atm, you cant leave out toString

Crispin09:08:55

user=> (reify java.lang.Object)         
java.lang.ClassCastException: class clojure.lang.Symbol cannot be cast to class java.lang.Throwable (clojure.lang.Symbol is in unnamed module of loader 'app'; java.lang.Throwable is in module java.base of loader 'bootstrap') [at <repl>:7:1]

borkdude09:08:04

I special cased this in defrecord, you could take a peek there

Crispin09:08:32

I got finalize and toString working together optionally

Crispin09:08:39

I will PR so you can take a peek

Crispin09:08:56

not even sure about this finalize approach now... because take a look at this:

borkdude10:08:01

looks good to me

Crispin10:08:40

It might not be... Just putting some examples on the PR

Crispin10:08:14

the multiple finalize happens in clojure too.

Crispin10:08:24

I think there may be multiple instances created

Crispin10:08:40

but in clojure it doesnt happen while the var binding is still in place

borkdude10:08:43

and after finalization, can you still use the object?

borkdude10:08:58

also worth testing in bb on JVM vs bb compiled to native

Crispin10:08:14

aaah Im actually testing on JVM...

Crispin10:08:19

that could be something...

Crispin10:08:32

wonder if native image is different. it has a different GC

borkdude10:08:45

what are you testing on the JVM, both?

Crispin10:08:26

yeah I can still use the object

Crispin10:08:40

but then defing nil, and gcing, I get the final finalize

Crispin10:08:01

like there are temporary instances being created, and they get finalized in the first gc

Crispin10:08:10

let me compare to native-image

borkdude10:08:32

I think an artifact of how SCI implements reify perhaps?

borkdude10:08:35

could it be there are more objects created elsewhere?

Crispin10:08:41

yes looks like it

borkdude10:08:06

perhaps a bug in SCI even

Crispin10:08:41

totally different behavior on native-image

Crispin10:08:43

it may also be repl related... I will try a script

Crispin10:08:48

I think graalvm has deprecated finalize

Crispin10:08:03

WeakReference is the way...

Crispin10:08:12

dinnertime bbl

Crispin14:08:02

closed that finalize PR. But made this one with what I discovered along the way https://github.com/babashka/babashka/pull/1348

emccue01:08:12

@U1QQJJK89 please don't use finalize, the thing you are looking for is a Cleaner

emccue01:08:54

(ns test
  (:import [java.lang.ref Cleaner]))

(def global-cleaner (Cleaner/create))

(defn cleanup-code [internals]
  (println "cleaning up object" internals))

(deftype ThingCleanup
  [thing]
  Runnable
  (run [_]
    (cleanup-code thing)))

(defn do-thing 
  []
  (let [object {:name "bob"
                :whatever 123}]
    (.register global-cleaner
               object 
               ;; Can't hold a ref to the object itself
               (->ThingCleanup (into {} object)))
    123))

(defn main
  []
  (do-thing)
  (loop []
    (Thread/sleep 100)))

(future (main))

emccue01:08:14

just noticing we're in babashka...

emccue01:08:39

welp that won't work 👀

emccue01:08:06

but that is the proper mechanism usually - cleaner runs a thread that consumes from a queue of weakrefs

Crispin02:08:45

Thanks @U3JH98J4R I will experiment. It could be made to work...