You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tomee.apache.org by db...@apache.org on 2022/09/23 01:53:12 UTC

[tomee] 02/03: Prototype CachingSupplier required for TOMEE-4050: Retry and Refresh for MP JWT keys supplied via HTTP

This is an automated email from the ASF dual-hosted git repository.

dblevins pushed a commit to branch TOMEE-4050
in repository https://gitbox.apache.org/repos/asf/tomee.git

commit 6e201ba5085a696aa3c0b0d8cd7af1790de73caa
Author: David Blevins <db...@tomitribe.com>
AuthorDate: Wed Sep 21 18:51:16 2022 -0700

    Prototype CachingSupplier required for TOMEE-4050: Retry and Refresh for MP JWT keys supplied via HTTP
---
 .../org/apache/openejb/util/CachedSupplier.java    | 237 +++++++++++++++++++++
 .../java/org/apache/openejb/util/Duration.java     |  14 ++
 .../apache/openejb/util/CachedSupplierTest.java    | 112 ++++++++++
 .../java/org/apache/openejb/util/DurationTest.java |  17 ++
 4 files changed, 380 insertions(+)

diff --git a/container/openejb-core/src/main/java/org/apache/openejb/util/CachedSupplier.java b/container/openejb-core/src/main/java/org/apache/openejb/util/CachedSupplier.java
new file mode 100644
index 0000000000..8eec3d6444
--- /dev/null
+++ b/container/openejb-core/src/main/java/org/apache/openejb/util/CachedSupplier.java
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.util;
+
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+public class CachedSupplier<T> implements Supplier<T> {
+
+    private final Duration initialRetryDelay;
+    private final Duration maxRetryDelay;
+    private final Duration accessTimeout;
+    private final Duration refreshInterval;
+    private final Supplier<T> supplier;
+
+    private final AtomicReference<T> value = new AtomicReference<>();
+    private final AtomicReference<Accessor<T>> accessor = new AtomicReference<>();
+    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(new DaemonThreadFactory());
+
+    private CachedSupplier(final Supplier<T> supplier, final Duration initialRetryDelay,
+                           final Duration maxRetryDelay, final Duration accessTimeout,
+                           final Duration refreshInterval) {
+
+        Objects.requireNonNull(supplier, "supplier");
+        Objects.requireNonNull(initialRetryDelay, "initialRetryDelay");
+        Objects.requireNonNull(maxRetryDelay, "maxRetryDelay");
+        Objects.requireNonNull(accessTimeout, "accessTimeout");
+        Objects.requireNonNull(refreshInterval, "refreshInterval");
+
+        this.supplier = supplier;
+        this.initialRetryDelay = initialRetryDelay;
+        this.maxRetryDelay = maxRetryDelay;
+        this.accessTimeout = accessTimeout;
+        this.refreshInterval = refreshInterval;
+
+        /*
+         * This must be last as it starts running code
+         * that uses the above settings
+         */
+        this.accessor.set(new BlockTillInitialized());
+    }
+
+    @Override
+    public T get() {
+        final Accessor<T> accessor = this.accessor.get();
+        return accessor.get();
+    }
+
+    public interface Accessor<T> {
+        T get();
+    }
+
+    class BlockTillInitialized implements Accessor<T> {
+        final CountDownLatch initialized = new CountDownLatch(1);
+
+        public BlockTillInitialized() {
+            executor.execute(new Initialize(1, initialRetryDelay));
+        }
+
+        @Override
+        public T get() {
+            try {
+                initialized.await(accessTimeout.getTime(), accessTimeout.getUnit());
+                return value.get();
+            } catch (InterruptedException e) {
+                throw new TimeoutException();
+            }
+        }
+
+        class Initialize implements Runnable {
+            final int attempts;
+            final Duration delay;
+
+            public Initialize(final int attempts, final Duration delay) {
+                this.attempts = attempts;
+                this.delay = delay;
+            }
+
+            public Initialize retry() {
+                if (delay.greaterOrEqualTo(maxRetryDelay)) {
+                    return new Initialize(attempts + 1, maxRetryDelay);
+                } else {
+                    return new Initialize(attempts + 1, Duration.min(maxRetryDelay, delay.multiply(2)));
+                }
+            }
+
+            @Override
+            public void run() {
+                try {
+                    final T t = supplier.get();
+                    if (t != null) {
+                        value.set(t);
+                        accessor.set(new Initialized());
+                        initialized.countDown();
+                        return;
+                    }
+                } catch (Throwable e) {
+                    // TODO
+                    e.printStackTrace();
+                }
+
+                /*
+                 * Initialization didn't work.  Let's try again
+                 */
+                final Initialize retry = retry();
+                executor.schedule(retry, retry.delay.getTime(), retry.delay.getUnit());
+            }
+        }
+    }
+
+    class Initialized implements Accessor<T> {
+        public Initialized() {
+            executor.scheduleAtFixedRate(new Refresh(), refreshInterval.getTime(), refreshInterval.getTime(), refreshInterval.getUnit());
+        }
+
+        @Override
+        public T get() {
+            return value.get();
+        }
+
+        class Refresh implements Runnable {
+            @Override
+            public void run() {
+                try {
+                    final T t = supplier.get();
+                    if (t != null) {
+                        value.set(t);
+                    }
+                } catch (Throwable e) {
+                    // TODO
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    public static <T> CachedSupplier<T> of(final Supplier<T> supplier) {
+        return new Builder<T>().supplier(supplier).build();
+    }
+
+    public static <T> Builder<T> builder(final Supplier<T> supplier) {
+        return new Builder<T>().supplier(supplier);
+    }
+
+    public static class TimeoutException extends RuntimeException {
+        // TODO
+    }
+
+    private static class DaemonThreadFactory implements ThreadFactory {
+        @Override
+        public Thread newThread(final Runnable r) {
+            final Thread thread = new Thread(r);
+            thread.setName(CachedSupplier.class.getSimpleName() + " Supplier");
+            thread.setDaemon(true);
+            return thread;
+        }
+    }
+
+    public static class Builder<T> {
+        private Duration initialRetryDelay = new Duration(2, TimeUnit.SECONDS);
+        private Duration maxRetryDelay = new Duration(1, TimeUnit.HOURS);
+        private Duration accessTimeout = new Duration(30, TimeUnit.SECONDS);
+        private Duration refreshInterval = new Duration(1, TimeUnit.DAYS);
+        private Supplier<T> supplier;
+
+
+        public Builder<T> initialRetryDelay(final Duration initialRetryDelay) {
+            this.initialRetryDelay = initialRetryDelay;
+            return this;
+        }
+
+        public Builder<T> initialRetryDelay(final int time, final TimeUnit unit) {
+            return initialRetryDelay(new Duration(time, unit));
+        }
+
+        public Builder<T> maxRetryDelay(final Duration maxRetryDelay) {
+            this.maxRetryDelay = maxRetryDelay;
+            return this;
+        }
+
+        public Builder<T> maxRetryDelay(final int time, final TimeUnit unit) {
+            return maxRetryDelay(new Duration(time, unit));
+        }
+
+        public Builder<T> accessTimeout(final Duration accessTimeout) {
+            this.accessTimeout = accessTimeout;
+            return this;
+        }
+
+        public Builder<T> accessTimeout(final int time, final TimeUnit unit) {
+            return accessTimeout(new Duration(time, unit));
+        }
+
+
+        public Builder<T> refreshInterval(final Duration refreshInterval) {
+            this.refreshInterval = refreshInterval;
+            return this;
+        }
+
+        public Builder<T> refreshInterval(final int time, final TimeUnit unit) {
+            return refreshInterval(new Duration(time, unit));
+        }
+
+        public Builder<T> supplier(final Supplier<T> supplier) {
+            this.supplier = supplier;
+            return this;
+        }
+
+        public CachedSupplier<T> build() {
+            return new CachedSupplier<>(supplier,
+                    initialRetryDelay,
+                    maxRetryDelay,
+                    accessTimeout,
+                    refreshInterval);
+        }
+    }
+}
diff --git a/container/openejb-core/src/main/java/org/apache/openejb/util/Duration.java b/container/openejb-core/src/main/java/org/apache/openejb/util/Duration.java
index dd7b1c6af4..9de1d84094 100644
--- a/container/openejb-core/src/main/java/org/apache/openejb/util/Duration.java
+++ b/container/openejb-core/src/main/java/org/apache/openejb/util/Duration.java
@@ -169,6 +169,10 @@ public class Duration {
         return new Duration(n.a - n.b, n.base);
     }
 
+    public Duration multiply(final long n) {
+        return new Duration(getTime() * n, getUnit());
+    }
+
     public boolean greaterThan(final Duration that) {
         final Normalize n = new Normalize(this, that);
         return n.a > n.b;
@@ -189,6 +193,16 @@ public class Duration {
         return n.a <= n.b;
     }
 
+    public static Duration max(final Duration a, final Duration b) {
+        final Normalize n = new Normalize(a, b);
+        return (n.a >= n.b) ? a : b;
+    }
+
+    public static Duration min(final Duration a, final Duration b) {
+        final Normalize n = new Normalize(a, b);
+        return (n.a <= n.b) ? a : b;
+    }
+
     public static Duration parse(final String text) {
         return new Duration(text);
     }
diff --git a/container/openejb-core/src/test/java/org/apache/openejb/util/CachedSupplierTest.java b/container/openejb-core/src/test/java/org/apache/openejb/util/CachedSupplierTest.java
new file mode 100644
index 0000000000..e179bc3acc
--- /dev/null
+++ b/container/openejb-core/src/test/java/org/apache/openejb/util/CachedSupplierTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.openejb.util;
+
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.*;
+
+public class CachedSupplierTest {
+
+    /**
+     * Supplier returns a valid result immediately and there
+     * are no delays on the first get.
+     *
+     * We also assert that calling get multiple times on the
+     * CachedSupplier return the cached result and do not get
+     * updated results before the refresh occurs.
+     */
+    @Test
+    public void happyPath() {
+    }
+
+    /**
+     * Supplier does not immediately return an initial instance, so we
+     * block till one is available. We assert that we blocked until get
+     * a valid result and no timeout or null is returned.
+     */
+    @Test
+    public void delayedInitialization() {
+    }
+
+    /**
+     * Supplier does not immediately return an initial instance
+     * and the timeout is reached.  We assert a TimeoutException
+     * is thrown.  We also assert that when the Supplier eventually
+     * does return a valid result we no longer get a TimeoutException
+     * or any blocking.
+     */
+    @Test
+    public void delayedInitializationTimeout() {
+    }
+
+    /**
+     * Supplier returns null on the first three calls to get.  On the
+     * fourth retry a valid result is returned from get.  We assert
+     * the number of times the supplier get is called as well as the
+     * time between each call to ensure exponential backoff is working
+     */
+    @Test
+    public void initializationRetry() {
+    }
+
+    /**
+     * Supplier returns null repeatedly on all initialization attempts.
+     * We assert that when the max retry time is reached all subsequent
+     * retries are at that same time interval and do not continue increasing
+     * exponentially.
+     */
+    @Test
+    public void initializationRetryTillMax() {
+    }
+
+    /**
+     * Suppler returns a valid result on initialization.  We expect that even
+     * when we are not actively calling get() the value will be refreshed
+     * according to the refreshInterval.  We wait for at least 3 refreshes
+     * to occur and assert the value we get is the most recent value returned
+     * from the supplier.  We intentionally check for this expected value
+     * while the refresh is currently executing for the fourth time.  We do
+     * that to ensure that there is no time values are null, even when we're
+     * concurrently trying to refresh it in the background.
+     */
+    @Test
+    public void refreshReliablyCalled() {
+    }
+
+
+    /**
+     * On the first refresh the Supplier returns null indicating there is
+     * no valid replacement.  We assert that the previous valid value is
+     * still in use.
+     */
+    @Test
+    public void refreshFailedWithNull() {
+    }
+
+    /**
+     * On the first refresh the Supplier throws an exception, therefore there is
+     * no valid replacement.  We assert that the previous valid value is
+     * still in use.
+     */
+    @Test
+    public void refreshFailedWithException() {
+    }
+
+}
diff --git a/container/openejb-core/src/test/java/org/apache/openejb/util/DurationTest.java b/container/openejb-core/src/test/java/org/apache/openejb/util/DurationTest.java
index 4f370dbac0..f0258da294 100644
--- a/container/openejb-core/src/test/java/org/apache/openejb/util/DurationTest.java
+++ b/container/openejb-core/src/test/java/org/apache/openejb/util/DurationTest.java
@@ -140,4 +140,21 @@ public class DurationTest extends TestCase {
         assertTrue(a.lessOrEqualTo(a));
         assertFalse(b.lessOrEqualTo(a));
     }
+
+    public void testMultiply() {
+        {
+            final Duration a = new Duration(1, SECONDS);
+            final Duration b = a.multiply(3);
+
+            assertEquals(b.getUnit(), SECONDS);
+            assertEquals(b.getTime(), 3);
+        }
+        {
+            final Duration a = new Duration(3, MINUTES);
+            final Duration b = a.multiply(3);
+
+            assertEquals(b.getUnit(), MINUTES);
+            assertEquals(b.getTime(), 9);
+        }
+    }
 }