[java-shib-shared] 08/09: JSSH-71 Remove the impact of the DestructableComponent Interface

Codeberg noreply at shibboleth.net
Mon Jul 6 15:29:35 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch dev/JSSH-71-old
in repository java-shib-shared.

View the commit online:
https://codeberg.org/Shibboleth/java-shib-shared/commit/edf3e48c8ba51250245703c2e6048591c2663e02

commit edf3e48c8ba51250245703c2e6048591c2663e02
Author: Rod Widdowson <rdw at steadingsoftware.com>
AuthorDate: Wed Jun 3 13:34:28 2026 +0100

    JSSH-71 Remove the impact of the DestructableComponent Interface
    
    https://shibboleth.atlassian.net/browse/JSSH-71
    
    Add a variant on TimerTask that is sensitive to its target being Garbage collected.
---
 .../shared/component/GCSensitiveTask.java          | 139 +++++++++++++++++
 .../shared/component/GCSensitiveTaskTest.java      | 168 +++++++++++++++++++++
 2 files changed, 307 insertions(+)

diff --git a/shib-support/src/main/java/net/shibboleth/shared/component/GCSensitiveTask.java b/shib-support/src/main/java/net/shibboleth/shared/component/GCSensitiveTask.java
new file mode 100644
index 00000000..52a9677e
--- /dev/null
+++ b/shib-support/src/main/java/net/shibboleth/shared/component/GCSensitiveTask.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.shared.component;
+
+import java.lang.ref.SoftReference;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.function.Consumer;
+
+import javax.annotation.Nonnull;
+
+import jakarta.annotation.Nullable;
+
+
+/**
+ * Class to run a {@link TimerTask} over a object which may be GC'd.
+ * 
+ * We keep a {@link SoftReference} to the object an when it goes away we cancel ourself.
+ * This means we do not need any explicit cancel of child tasks.  This task will carry on
+ * working until such time as the object goes away, but critically we do not hold a hard
+ * reference on the object so the object itself doesn't have to worry about cancelling
+ * the task.
+ * 
+ * Because some jobs are too big to allow to happen (think reloading a huge aggregate
+ * for a mnetadataresolver which is orphaned but not GC'd) we also allow the object to
+ * tear itself down which is signals back to use by return false to {@link Refreshable#refresh()}.
+ *
+ * @param <T> a type which can be torns down via a provided {@link Consumer}.
+ */
+public class GCSensitiveTask<T> extends TimerTask {
+
+    /** A soft reference on the thing that needs refreshed. */
+    @Nonnull private final SoftReference<Refreshable> theTarget;
+    /** A timer to be town down when we are done. */
+    @Nullable private final Timer theTimer;
+    /** Some part form the inside of the {@link Refreshable} which needs explicit teardown
+     * as part of the GC thing. */
+    @Nullable private final T theTeardownParameter; 
+    /** The thing that does the teardown. */
+    @Nullable private final Consumer<T> theTeardown;
+    
+    /**
+     * Constructor.
+     *
+     * @param target The thing we call upon to refresh itself.
+     * @param timerToTeardown A timer we need to cancel when we go away
+     * @param teardown something to call when we spot that the parent is gone
+     * @param teardownParam what to give it
+     */
+    public GCSensitiveTask(@Nonnull final Refreshable target,
+                           @Nullable final Timer timerToTeardown,
+                           @Nullable Consumer<T> teardown,
+                           @Nullable T teardownParam) { 
+        
+        super();
+        theTarget = new SoftReference<GCSensitiveTask.Refreshable>(target);
+        theTimer = timerToTeardown;
+        theTeardown = teardown;
+        theTeardownParameter = teardownParam; 
+    }
+    
+    /**
+     * Constructor.
+     *
+     * @param target The thing we call upon to refresh itself.
+     * @param timerToTeardown A timer we need to cancel when we go away
+     */
+    public GCSensitiveTask(@Nonnull final Refreshable target, @Nullable final Timer timerToTeardown) {
+        
+        this(target, timerToTeardown, null, null);
+
+    }
+    
+    /** {@inheritDoc}
+     * Just say no - the whole idea here is to make this a non-cancellable task so we dont have to hang on to it
+     * thus pinning ourelves.
+     *  */
+    @Override
+    public boolean cancel() {
+        throw new UnsupportedOperationException();
+    }
+    
+    /** {@inheritDoc} 
+     * 
+     * Is the refreshable still there? If so tell it to do its thing or tell us to stop.
+     * If not just stop ourselves.
+     */
+    @Override
+    public void run() {
+
+        final Refreshable target = theTarget.get();
+        
+        if (target == null) {
+            // target has been gc'd.  Stop ourselves and the timer
+            super.cancel();
+            if (theTimer != null) {
+                theTimer.cancel();
+            }
+            // and if we were given a "clean up" callback call it
+            if (theTeardown != null && theTeardownParameter != null) {
+                theTeardown.accept(theTeardownParameter);
+            }
+        } else {
+             // its still there ask it to refresh
+            if (!target.refresh()) {
+                // It has decided to tear itself down
+                // stop ourselves
+                super.cancel();
+                if (theTimer != null) {
+                    theTimer.cancel();
+                }
+            }                
+        }
+    }
+    
+    /**
+     * Everything class which wants to use this Task implements this interface 
+     */
+    public interface Refreshable {
+        
+        /** Do whatever needs to be done and return true or false if it is time to stop
+         * @return true if processing is to continue.
+         */
+        public boolean refresh();
+        
+    }
+}
diff --git a/shib-support/src/test/java/net/shibboleth/shared/component/GCSensitiveTaskTest.java b/shib-support/src/test/java/net/shibboleth/shared/component/GCSensitiveTaskTest.java
new file mode 100644
index 00000000..efb661fb
--- /dev/null
+++ b/shib-support/src/test/java/net/shibboleth/shared/component/GCSensitiveTaskTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.shared.component;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Timer;
+import java.util.function.Consumer;
+
+import org.testng.annotations.Test;
+
+import net.shibboleth.shared.component.GCSensitiveTask.Refreshable;
+
+/**
+ *
+ */
+public class GCSensitiveTaskTest {
+    
+    private synchronized void sleepFifty () {
+        try {
+            wait(50);
+        } catch (InterruptedException e) {
+            // carry on
+        }            
+    }
+
+    /** Test for the GC driven part of task tear down.
+     *
+     * Do not run this test.  Just don't.
+     *
+     * It forces a GC by trying to allocate an improbable amount of memory (because
+     * {@link System#gc()} doesn't guarantee to do that.  So it causes huge CPU and memory
+     * issues to the JVM, sucks resources (and $$$).
+     * 
+     * It exists purely to be run by individual developers to prove that the code works.
+     * Because of the system wide resource impact it mostly doesn't even work under eclipse.  
+     *   
+     */
+    @Test(enabled = false) public void GCTeardown() {
+        
+        timerGone = false;
+        CancelCheckingTimer timer = new CancelCheckingTimer();
+        assertFalse(timerGone);
+        final SettableBoolean boolObject = new SettableBoolean(false);
+        assertFalse(boolObject.getValue());
+
+        GCSensitiveTask<SettableBoolean> task = new GCSensitiveTask<SettableBoolean>(
+                        new TestRefreshable(true),
+                        timer,
+                        new Consumer<SettableBoolean>() {
+                            /** {@inheritDoc} */
+                            @Override
+                            public void accept(SettableBoolean t) {
+                                t.setValue(true);
+                            }
+                        },
+                        boolObject);
+        timer.schedule(task, 0, 10);
+        timer = null;
+        assertFalse(timerGone);
+        assertFalse(boolObject.getValue());
+        sleepFifty();
+        assertFalse(timerGone);
+        assertFalse(boolObject.getValue());
+        task = null; // we can now reap task
+        boolean outOfMem = false;
+        try {
+            // Pig ugly code to force a GC.
+            @SuppressWarnings("unused")
+            Object object = new int[100][100][100][10][10][10][10][10];
+        } catch(OutOfMemoryError err) {
+            outOfMem = true;
+        }
+        assertTrue(outOfMem);
+        sleepFifty();
+        assertTrue(timerGone);
+        assertTrue(boolObject.getValue());
+        
+    }
+    
+    @Test public void NonGCTeardown() throws InterruptedException {
+
+        timerGone = false;
+        final CancelCheckingTimer timer = new CancelCheckingTimer();
+        
+        //
+        // Start a task repeating for ever, only arrange for the refreshable to say "I'm, done"
+        //
+        timer.schedule(new GCSensitiveTask<TestRefreshable>(new TestRefreshable(false), timer), 0, 10);
+        sleepFifty();
+        assertTrue(timerGone);
+    }
+    
+    private boolean timerGone = false;
+    
+    /** A timer that sets a value when it is cancelled. */
+    private class CancelCheckingTimer extends Timer {
+        
+        /** {@inheritDoc} */
+        @Override
+        public void cancel() {
+            timerGone = true;
+            super.cancel();
+        }
+        
+    }
+    
+    /**
+     * simple class to implement {@link Refreshable}.
+     */
+    private static class TestRefreshable implements Refreshable {
+        
+        /** what to say to {@link #refresh()}. */
+        private final boolean refreshValue;
+        
+        /**
+         * Constructor.
+         *
+         * @param returnValue what to return when called at {@link #refresh()}.
+         */
+        public TestRefreshable(final boolean returnValue) {
+            
+            refreshValue = returnValue;
+        }
+        
+        @Override
+        public boolean refresh() {
+            return refreshValue;
+        }
+
+    }
+    
+    private static class SettableBoolean {
+        
+        private boolean value;
+        
+        public SettableBoolean(final boolean initialValue) {   
+            value = initialValue;
+        }
+        /** Setter.
+         * @param what The val to set.
+         */
+        public void setValue(final boolean what) {
+            value = what;
+        }
+        
+        /** Getter.
+         * @return the stored and potentially changed value
+         * 
+         */
+        private boolean getValue() {
+            return value;
+        }
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list