package org.dsa.iot.commons; import org.dsa.iot.dslink.util.Objects; import org.dsa.iot.dslink.util.handler.Handler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * The {@link GuaranteedReceiver} is designed to guarantee retrieving an * instance of {@link T}. The instance will be instantiated as necessary. * * A trivial use case is to support automatically reconnecting to servers * in an elegant and standard fashion. * * @author Samuel Grenier */ public abstract class GuaranteedReceiver<T> { private static final Logger LOGGER; private final TimeUnit timeUnit; private final long delay; private final List<Handler<T>> list = new ArrayList<>(); private ScheduledFuture<?> instantiationFut; private ScheduledFuture<?> loopFut; private T instance; private boolean running = true; /** * Constructs a {@link GuaranteedReceiver} with a specified {@code delay}. * * @param delay Delay, in seconds, to instantiate the instance if an error * occurs. * @see #GuaranteedReceiver(long, boolean) */ public GuaranteedReceiver(long delay) { this(delay, false); } /** * * @param delay Delay, in seconds, to instantiate the instance if an error * occurs. * @param loop Whether to enable looping or not. * @see #GuaranteedReceiver(long, TimeUnit, boolean) */ public GuaranteedReceiver(long delay, boolean loop) { this(delay, TimeUnit.SECONDS, loop); } /** * The instance of {@link T} will not be initially initialized until * {@link #get} is called or looping is enabled. If looping is enabled then * {@link #onLoop} will be called repeatedly when the {@code delay} time * elapses based on the specified {@code unit} of time. * * @param delay Delay to instantiate the instance, in the specified * {@link TimeUnit}, to instantiate the instance if an * error occurs. * @param unit Unit of time the delay is in. * @param loop Whether to enable looping or not. */ public GuaranteedReceiver(long delay, TimeUnit unit, boolean loop) { if (delay <= 0) { String err = "Delay must be greater than zero"; throw new IllegalArgumentException(err); } this.delay = delay; this.timeUnit = unit; if (loop) { initializeLoop(); } } /** * Creates the instance of {@link T}. * * @return The created instance of {@link T} * @throws Exception An error occurred creating the instance. */ protected abstract T instantiate() throws Exception; /** * If the instance of {@link T} is invalidated, then it will be set to * {@code null} and be re-instantiated, otherwise the current instance * of {@link T} will not be set to {@code null}. If the instance is not * invalidated then it will be treated as an unhandled exception and will * be logged. * * @param e Exception that occurred when instantiating or calling a * handler. * @return Whether to invalidate the instance or not. It is recommended to * return {@code false} by default. */ protected abstract boolean invalidateInstance(Exception e); /** * Called every time a loop occurs. The looping feature must be enabled. A * loop is called based on the desired delay. * * The loop functionality is designed to test if a resource is still being * held to determine a status as needed. A {@link RuntimeException} must * be thrown in order to invalidate the instance. The implementation of * {@link #invalidateInstance(Exception)} will determine if the instance * should be invalidated. * * @param event Instantiated {@link T}. * @see #GuaranteedReceiver(long, TimeUnit, boolean) * @see #invalidateInstance(Exception) */ @SuppressWarnings("UnusedParameters") protected void onLoop(T event) { } /** * Guarantees the instance of {@link T} is available for consumption. The * {@code handler} will persist by default. * * @param handler Called when the instance has been retrieved and is ready * for consumption. * @param checked If the receiver is shutdown and {@code checked} is * {@code true} then {@link IllegalStateException} is * thrown, otherwise the {@code handler} gets ignored. */ public final void get(Handler<T> handler, boolean checked) { get(handler, checked, true); } /** * Guarantees the instance of {@link T} is available for consumption. * * @param handler Called when the instance has been retrieved and is ready * for consumption. * @param checked If the receiver is shutdown and {@code checked} is * {@code true} then {@link IllegalStateException} is * thrown, otherwise the {@code handler} gets ignored. * @param persist Whether to persist the handler in a cache if the instance * is not ready yet or an exception has occurred. */ public final void get(final Handler<T> handler, final boolean checked, final boolean persist) { boolean reattempt = false; synchronized (this) { if (!running) { if (checked) { throw new IllegalStateException("Receiver shutdown"); } else { return; } } if (instance == null) { if (handler != null && persist) { list.add(handler); } if (instantiationFut != null) { return; } ScheduledThreadPoolExecutor stpe = getSTPE(); InstantiationRunner runner = new InstantiationRunner(); instantiationFut = stpe.scheduleWithFixedDelay(runner, 0, delay, timeUnit); } } T tmp; synchronized (this) { tmp = instance; } if (tmp != null && handler != null) { try { handler.handle(tmp); } catch (Exception e) { if (invalidateInstance(e)) { synchronized (this) { instance = null; } reattempt = true; } else { LOGGER.error("Unhandled exception", e); } } } else if (tmp == null) { reattempt = true; } if (reattempt && persist) { get(handler, checked); } } /** * Shuts down the receiver from preventing any new instances to be created. * If the looping feature is enabled then it will be shutdown as well. * * @return The underlying instance of {@link T}, which can be * {@code null}, to allow freeing any resources the instance holds. */ public T shutdown() { synchronized (this) { running = false; } if (loopFut != null) { try { loopFut.cancel(true); } catch (Exception ignored) { } loopFut = null; } T tmp; synchronized (this) { tmp = instance; list.clear(); instance = null; } stopRunner(); return tmp; } /** * Cancels the instantiation scheduled future from running any further. */ private void stopRunner() { if (instantiationFut != null) { try { instantiationFut.cancel(true); } catch (Exception ignored) { } instantiationFut = null; } } private void initializeLoop() { ScheduledThreadPoolExecutor stpe = getSTPE(); loopFut = stpe.scheduleWithFixedDelay(new Runnable() { @Override public void run() { get(new Handler<T>() { @Override public void handle(T event) { onLoop(event); } }, false, false); } }, 0, delay, timeUnit); } private class InstantiationRunner implements Runnable { @Override public void run() { synchronized (GuaranteedReceiver.this) { try { instance = instantiate(); stopRunner(); } catch (Exception e) { LOGGER.debug("Failed to instantiate", e); return; } } List<Handler<T>> list; synchronized (GuaranteedReceiver.this) { list = new ArrayList<>(GuaranteedReceiver.this.list); } ScheduledThreadPoolExecutor stpe = getSTPE(); final Container<Boolean> doBreak = new Container<>(); for (final Handler<T> handler : list) { // latch is used to ensure the instance check is complete final CountDownLatch latch = new CountDownLatch(1); stpe.execute(new Runnable() { @Override public void run() { boolean doRemove = true; try { T inst; synchronized (GuaranteedReceiver.this) { inst = instance; } if (inst == null) { doBreak.setValue(true); latch.countDown(); return; } latch.countDown(); handler.handle(inst); } catch (Exception e) { if (invalidateInstance(e)) { synchronized (GuaranteedReceiver.this) { instance = null; } doRemove = false; get(null, false); } else { LOGGER.error("Unhandled exception", e); } } finally { if (doRemove) { synchronized (GuaranteedReceiver.this) { GuaranteedReceiver.this.list.remove(handler); } } } } }); try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } if (doBreak.getValue()) { break; } } } } private static ScheduledThreadPoolExecutor getSTPE() { return Objects.getDaemonThreadPool(); } static { LOGGER = LoggerFactory.getLogger(GuaranteedReceiver.class); } }