Skip to content

Commit

Permalink
replace Cleaner API with a local Thread
Browse files Browse the repository at this point in the history
Cleaner is Java 9+, so it cannot be used on JRuby 9 because that is meant
to run on Java 8 and up.

The disadvantage wrt the Cleaner API is that this will leak a Thread if
`terminate()` is never called. At least it's a Daemon Thread so it doesn't
keep the JVM from exiting.
  • Loading branch information
matthias-fratz-bsz committed Jan 8, 2025
1 parent 3b0c8d1 commit 71260b8
Showing 1 changed file with 48 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
*/
package org.jruby.embed.internal;

import java.lang.ref.Cleaner;
import java.lang.ref.Cleaner.Cleanable;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
Expand All @@ -43,8 +44,8 @@
*/
class ThreadLocalContext {
private final ConcurrentHashMap<LocalContextCleaningAction, Object> contextRefs = new ConcurrentHashMap<>();
private final Cleaner cleaner = Cleaner.create();
private final Supplier<LocalContext> localContextFactory;
private final Cleaner cleaner = new Cleaner();

public ThreadLocalContext(final Supplier<LocalContext> localContextFactory) {
this.localContextFactory = localContextFactory;
Expand All @@ -61,13 +62,13 @@ protected AtomicReference<LocalContextCleaningAction> initialValue() {
// GC'd (i.e. when this class gets GC'd because terminate() was never called).
// see ThreadLocal JavaDoc: ref will stay reachable "as long as the thread is
// alive and the ThreadLocal instance is accessible"
final Cleanable cleanable = cleaner.register(ref, ctx);
ctx.register(ref, cleaner);
if (contextHolder == null)
// boundary case if we're already terminating: clean up immediately, because
// there is no more cleanup thread to do that later
// the returned context will be null, but that's to be expected when operating
// on an object that has been terminated
cleanable.clean();
ctx.run();
return ref;
}
};
Expand All @@ -77,6 +78,7 @@ public LocalContext get() {
}

public void terminate() {
cleaner.interrupt();
contextHolder = null;
for (final LocalContextCleaningAction ref : contextRefs.keySet())
ref.run();
Expand All @@ -88,11 +90,14 @@ public void terminate() {
* everything that is GC-reachable from them will stay reachable until the
* cleaning action has been run.
*/
private static class LocalContextCleaningAction extends AtomicReference<LocalContext> implements Runnable {
static class LocalContextCleaningAction extends AtomicReference<LocalContext> implements Runnable {
private static final long serialVersionUID = 1L;

private final ConcurrentHashMap<LocalContextCleaningAction, Object> contextRefs;

@SuppressWarnings("unused") // only used to make sure the PhantomReference doesn't get GC'd
private CleanerReference phantomReference;

private LocalContextCleaningAction(final ConcurrentHashMap<LocalContextCleaningAction, Object> contextRefs,
final LocalContext context) {
super(context);
Expand All @@ -111,5 +116,42 @@ public void run() {
lc.remove();
contextRefs.remove(this);
}

private void register(final AtomicReference<LocalContextCleaningAction> ref, final Cleaner cleaner) {
phantomReference = new CleanerReference(ref, cleaner.q, this);
}
}

private static class CleanerReference extends PhantomReference<AtomicReference<LocalContextCleaningAction>> {
private Runnable cleanup;

public CleanerReference(final AtomicReference<LocalContextCleaningAction> referent,
final ReferenceQueue<AtomicReference<LocalContextCleaningAction>> q, final Runnable cleanup) {
super(referent, q);
this.cleanup = cleanup;
}
}

private static class Cleaner extends Thread {
private final ReferenceQueue<AtomicReference<LocalContextCleaningAction>> q = new ReferenceQueue<>();

public Cleaner() {
setName("JRuby-ThreadLocalContext-Cleaner-" + getId());
setDaemon(true);
start();
}

@Override
public void run() {
while (!interrupted()) {
final Reference<?> cleanable;
try {
cleanable = q.remove();
} catch (InterruptedException e) {
break;
}
((CleanerReference) cleanable).cleanup.run();
}
}
}
}

0 comments on commit 71260b8

Please sign in to comment.