[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