From 78d6ebe289f1b9597596088ac04168654d35f1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Va=C5=BEan?= Date: Tue, 11 Apr 2023 07:59:19 +0000 Subject: [PATCH] Removed core library code --- .../hookless/CurrentReactiveScope.java | 107 -- .../hookless/ReactiveBlockingException.java | 168 --- .../machinezoo/hookless/ReactiveExecutor.java | 187 --- .../machinezoo/hookless/ReactiveFreezes.java | 93 -- .../machinezoo/hookless/ReactiveFuture.java | 294 ----- .../com/machinezoo/hookless/ReactiveLazy.java | 65 - .../com/machinezoo/hookless/ReactivePins.java | 156 --- .../machinezoo/hookless/ReactiveScope.java | 317 ----- .../hookless/ReactiveStateMachine.java | 210 ---- .../machinezoo/hookless/ReactiveThread.java | 314 ----- .../machinezoo/hookless/ReactiveTrigger.java | 233 ---- .../machinezoo/hookless/ReactiveValue.java | 374 ------ .../machinezoo/hookless/ReactiveVariable.java | 567 --------- .../machinezoo/hookless/ReactiveWorker.java | 289 ----- .../hookless/experimental/ReactiveCache.java | 13 - .../experimental/ReactiveComputation.java | 5 - .../experimental/ReactiveComputationNode.java | 51 - .../hookless/experimental/ReactiveData.java | 5 - .../experimental/ReactiveDataNode.java | 41 - .../experimental/ReactiveIntermediary.java | 5 - .../ReactiveIntermediaryNode.java | 6 - .../hookless/experimental/ReactiveKey.java | 22 - .../hookless/experimental/ReactiveObject.java | 6 - .../experimental/ReactiveObjectConfig.java | 11 - .../experimental/ReactiveObjectNode.java | 9 - .../experimental/ReactiveSideEffect.java | 43 - .../experimental/ReactiveSideEffectKey.java | 8 - .../hookless/experimental/ReactiveStack.java | 46 - .../experimental/ReactiveVersion.java | 21 - .../experimental/ReactiveVersionHash.java | 132 -- .../hookless/experimental/package-info.java | 2 - .../std/StandardReactiveDataNode.java | 78 -- .../experimental/std/bells/ReactiveBell.java | 20 - .../std/bells/ReactiveBellConfig.java | 26 - .../std/bells/ReactiveBellNode.java | 29 - .../experimental/std/bells/package-info.java | 2 - .../std/blocking/ReactiveBlocking.java | 28 - .../blocking/ReactiveBlockingException.java | 31 - .../std/blocking/ReactiveBlockingKey.java | 8 - .../std/blocking/package-info.java | 2 - .../std/caches/PermanentReactiveCache.java | 22 - .../std/caches/TransientReactiveCache.java | 17 - .../experimental/std/caches/package-info.java | 2 - .../HashedReactiveConstantObject.java | 12 - .../HashedReactiveConstantObjectConfig.java | 18 - .../std/constants/ReactiveConstant.java | 12 - .../std/constants/ReactiveConstantConfig.java | 19 - .../std/constants/ReactiveConstantNode.java | 35 - .../std/constants/ReactiveConstantNumber.java | 17 - .../ReactiveConstantNumberConfig.java | 22 - .../std/constants/ReactiveConstantObject.java | 17 - .../ReactiveConstantObjectConfig.java | 22 - .../std/constants/ReactiveConstantString.java | 14 - .../ReactiveConstantStringConfig.java | 22 - .../std/constants/package-info.java | 2 - .../std/markers/ReactiveMarker.java | 14 - .../std/markers/ReactiveMarkerConfig.java | 26 - .../std/markers/ReactiveMarkerNode.java | 36 - .../std/markers/package-info.java | 2 - .../experimental/std/package-info.java | 2 - .../std/versions/NullReactiveVersion.java | 20 - .../std/versions/ReactiveVersionNumber.java | 17 - .../std/versions/ReactiveVersionObject.java | 21 - .../std/versions/ReactiveVersionString.java | 20 - .../std/versions/package-info.java | 2 - .../noexception/ReactiveExceptions.java | 76 -- .../hookless/noexception/package-info.java | 5 - .../com/machinezoo/hookless/package-info.java | 19 - .../machinezoo/hookless/time/AlarmIndex.java | 80 -- .../hookless/time/AlarmScheduler.java | 106 -- .../time/GrowingReactiveDuration.java | 142 --- .../hookless/time/ReactiveAlarm.java | 41 - .../hookless/time/ReactiveClock.java | 127 -- .../hookless/time/ReactiveDuration.java | 106 -- .../hookless/time/ReactiveInstant.java | 149 --- .../time/ShrinkingReactiveDuration.java | 142 --- .../hookless/time/package-info.java | 6 - .../hookless/util/ConsistentRandom.java | 34 - .../machinezoo/hookless/util/OwnerTrace.java | 274 ---- .../hookless/util/ReactiveCollections.java | 1100 ----------------- .../hookless/util/package-info.java | 6 - .../hookless/utils/WeakRunnable.java | 48 - .../hookless/CurrentReactiveScopeTest.java | 57 - .../ReactiveBlockingExceptionTest.java | 65 - .../hookless/ReactiveExecutorTest.java | 136 -- .../hookless/ReactiveFreezesTest.java | 115 -- .../hookless/ReactiveFutureTest.java | 256 ---- .../machinezoo/hookless/ReactiveLazyTest.java | 69 -- .../machinezoo/hookless/ReactivePinsTest.java | 119 -- .../hookless/ReactiveScopeTest.java | 165 --- .../hookless/ReactiveStateMachineTest.java | 215 ---- .../hookless/ReactiveThreadTest.java | 218 ---- .../hookless/ReactiveTriggerTest.java | 136 -- .../hookless/ReactiveValueTest.java | 114 -- .../hookless/ReactiveVariableTest.java | 128 -- .../hookless/ReactiveWorkerTest.java | 187 --- .../com/machinezoo/hookless/TestBase.java | 20 - 97 files changed, 8898 deletions(-) delete mode 100644 src/main/java/com/machinezoo/hookless/CurrentReactiveScope.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactiveBlockingException.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactiveExecutor.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactiveFreezes.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactiveFuture.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactiveLazy.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactivePins.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactiveScope.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactiveStateMachine.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactiveThread.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactiveTrigger.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactiveValue.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactiveVariable.java delete mode 100644 src/main/java/com/machinezoo/hookless/ReactiveWorker.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveCache.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveComputation.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveComputationNode.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveData.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveDataNode.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveIntermediary.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveIntermediaryNode.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveKey.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveObject.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveObjectConfig.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveObjectNode.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveSideEffect.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveSideEffectKey.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveStack.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveVersion.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/ReactiveVersionHash.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/package-info.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/StandardReactiveDataNode.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/bells/ReactiveBell.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/bells/ReactiveBellConfig.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/bells/ReactiveBellNode.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/bells/package-info.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/blocking/ReactiveBlocking.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/blocking/ReactiveBlockingException.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/blocking/ReactiveBlockingKey.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/blocking/package-info.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/caches/PermanentReactiveCache.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/caches/TransientReactiveCache.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/caches/package-info.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/constants/HashedReactiveConstantObject.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/constants/HashedReactiveConstantObjectConfig.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstant.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantConfig.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantNode.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantNumber.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantNumberConfig.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantObject.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantObjectConfig.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantString.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantStringConfig.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/constants/package-info.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/markers/ReactiveMarker.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/markers/ReactiveMarkerConfig.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/markers/ReactiveMarkerNode.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/markers/package-info.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/package-info.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/versions/NullReactiveVersion.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/versions/ReactiveVersionNumber.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/versions/ReactiveVersionObject.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/versions/ReactiveVersionString.java delete mode 100644 src/main/java/com/machinezoo/hookless/experimental/std/versions/package-info.java delete mode 100644 src/main/java/com/machinezoo/hookless/noexception/ReactiveExceptions.java delete mode 100644 src/main/java/com/machinezoo/hookless/noexception/package-info.java delete mode 100644 src/main/java/com/machinezoo/hookless/package-info.java delete mode 100644 src/main/java/com/machinezoo/hookless/time/AlarmIndex.java delete mode 100644 src/main/java/com/machinezoo/hookless/time/AlarmScheduler.java delete mode 100644 src/main/java/com/machinezoo/hookless/time/GrowingReactiveDuration.java delete mode 100644 src/main/java/com/machinezoo/hookless/time/ReactiveAlarm.java delete mode 100644 src/main/java/com/machinezoo/hookless/time/ReactiveClock.java delete mode 100644 src/main/java/com/machinezoo/hookless/time/ReactiveDuration.java delete mode 100644 src/main/java/com/machinezoo/hookless/time/ReactiveInstant.java delete mode 100644 src/main/java/com/machinezoo/hookless/time/ShrinkingReactiveDuration.java delete mode 100644 src/main/java/com/machinezoo/hookless/time/package-info.java delete mode 100644 src/main/java/com/machinezoo/hookless/util/ConsistentRandom.java delete mode 100644 src/main/java/com/machinezoo/hookless/util/OwnerTrace.java delete mode 100644 src/main/java/com/machinezoo/hookless/util/ReactiveCollections.java delete mode 100644 src/main/java/com/machinezoo/hookless/util/package-info.java delete mode 100644 src/main/java/com/machinezoo/hookless/utils/WeakRunnable.java delete mode 100644 src/test/java/com/machinezoo/hookless/CurrentReactiveScopeTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactiveBlockingExceptionTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactiveExecutorTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactiveFreezesTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactiveFutureTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactiveLazyTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactivePinsTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactiveScopeTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactiveStateMachineTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactiveThreadTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactiveTriggerTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactiveValueTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactiveVariableTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/ReactiveWorkerTest.java delete mode 100644 src/test/java/com/machinezoo/hookless/TestBase.java diff --git a/src/main/java/com/machinezoo/hookless/CurrentReactiveScope.java b/src/main/java/com/machinezoo/hookless/CurrentReactiveScope.java deleted file mode 100644 index b8cb380..0000000 --- a/src/main/java/com/machinezoo/hookless/CurrentReactiveScope.java +++ /dev/null @@ -1,107 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import java.util.function.*; -import com.machinezoo.stagean.*; - -/* - * Since reactive code can run outside of reactive scope, for example during tests, - * all code would be normally required to constantly check ReactiveScope.current() for null. - * To avoid that, we offer static methods that provide reasonable fallback for null scope. - */ -/** - * Convenience methods to access current {@link ReactiveScope}. - * All calls are forwarded to {@link ReactiveScope#current()} if it is not {@code null}. - * If {@link ReactiveScope#current()} is {@code null}, methods of this class provide safe fallback behavior, - * which is usually functionally equivalent to creating temporary {@link ReactiveScope}. - * - * @see ReactiveScope - * @see ReactiveScope#current() - */ -@DraftDocs("reactive freezes link, reactive pins link, reactive computation link") -public class CurrentReactiveScope { - /** - * Mark the current reactive computation as reactively blocking. - * If there's no current reactive computation ({@link ReactiveScope#current()} is {@code null}), this method has no effect. - * - * @see ReactiveScope#block() - * @see ReactiveScope#current() - * @see Reactive blocking - */ - public static void block() { - ReactiveScope current = ReactiveScope.current(); - if (current != null) - current.block(); - } - /** - * Returns {@code true} if the current reactive computation is reactively blocking. - * If there's no current reactive computation ({@link ReactiveScope#current()} is {@code null}), this method returns {@code false}. - * - * @return {@code true} if the current reactive computation is reactively blocking, {@code false} otherwise - * - * @see ReactiveScope#blocked() - * @see ReactiveScope#current() - * @see Reactive blocking - */ - public static boolean blocked() { - ReactiveScope current = ReactiveScope.current(); - if (current != null) - return current.blocked(); - else - return false; - } - /** - * Freezes result of specified computation. - * On first call with given {@code key}, returns result of evaluating the {@code supplier}. - * On subsequent calls within the same reactive computation, returns the same result without calling the {@code supplier}. - * If there's no current reactive computation ({@link ReactiveScope#current()} is {@code null}), - * this method evaluates {@code supplier} anew on every call. - * The {@code supplier} should be always the same for given {@code key}. - * - * @param - * type of value returned by the {@code supplier} - * @param key - * identifier for particular frozen value, which implements {@link Object#equals(Object)} and {@link Object#hashCode()} - * @param supplier - * value supplier to evaluate - * @return value computed by {@code supplier} or previously frozen value - * - * @see ReactiveScope#freeze(Object, Supplier) - * @see ReactiveScope#current() - */ - public static T freeze(Object key, Supplier supplier) { - ReactiveScope current = ReactiveScope.current(); - if (current != null) - return current.freeze(key, supplier); - else - return supplier.get(); - } - /** - * Pins result of specified computation. - * On first call with given {@code key}, returns result of evaluating the {@code supplier}. - * On subsequent calls within the same sequence of blocking reactive computations (pin lifetime), - * returns the same result without calling the {@code supplier}. - * If there's no current reactive computation ({@link ReactiveScope#current()} is {@code null}), - * this method evaluates {@code supplier} anew on every call. - * The {@code supplier} should be always the same for given {@code key}. - * - * @param - * type of value returned by the {@code supplier} - * @param key - * identifier for particular pinned value, which implements {@link Object#equals(Object)} and {@link Object#hashCode()} - * @param supplier - * value supplier to evaluate - * @return value computed by {@code supplier} or previously pinned value - * - * @see ReactiveScope#pin(Object, Supplier) - * @see ReactiveScope#current() - * @see Reactive blocking - */ - public static T pin(Object key, Supplier supplier) { - ReactiveScope current = ReactiveScope.current(); - if (current != null) - return current.pin(key, supplier); - else - return supplier.get(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactiveBlockingException.java b/src/main/java/com/machinezoo/hookless/ReactiveBlockingException.java deleted file mode 100644 index e1daa04..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactiveBlockingException.java +++ /dev/null @@ -1,168 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -/** - * Default exception to throw when reactive code needs to reactively block. - *

- * Merely creating or throwing this exception is not sufficient to indicate blocking. - * Current reactive computation must be explicitly marked as blocked before throwing - * by calling {@link CurrentReactiveScope#block()}. - * This class defines convenience {@link #block()} method and its overloads that - * call {@link CurrentReactiveScope#block()} before throwing {@code ReactiveBlockingException}. - *

- * Reactive code should preferably return fallback in case it signals blocking, - * because that allows the rest of the code to continue and to trigger further blocking operations. - * This way all blocking operations (e.g. database queries) can execute in parallel. - *

- * It is however sometimes impossible to provide a reasonable fallback result. - * In those cases, throwing an exception is acceptable. - *

- * This exception type is provided as the most descriptive for cases of reactive blocking. - * Its use is not mandatory though. Code that reactively blocks may throw any exception. - * - * @see ReactiveScope#block() - * @see CurrentReactiveScope#block() - * @see Reactive blocking - */ -public class ReactiveBlockingException extends RuntimeException { - private static final long serialVersionUID = 1L; - /* - * Constructors do not cause blocking on their own, - * so that the exception can be created as a fallback before there is any reason to block. - */ - /** - * Constructs new {@code ReactiveBlockingException} with the specified message and cause. - *

- * Merely calling this constructor does not block - * current {@link ReactiveScope}. Use {@link #block(String, Throwable)} for that. - * - * @param message - * informative message (possibly {@code null}) that can be later retrieved via {@link Throwable#getMessage()} - * @param cause - * cause of this exception (possibly {@code null}) that can be later retrieved via {@link Throwable#getCause()} - * - * @see #block(String, Throwable) - */ - public ReactiveBlockingException(String message, Throwable cause) { - super(message, cause); - } - /** - * Constructs new {@code ReactiveBlockingException} with the specified message. - *

- * Merely calling this constructor does not block - * current {@link ReactiveScope}. Use {@link #block(String)} for that. - * - * @param message - * informative message (possibly {@code null}) that can be later retrieved via {@link Throwable#getMessage()} - * - * @see #block(String) - */ - public ReactiveBlockingException(String message) { - this(message, null); - } - /** - * Constructs new {@code ReactiveBlockingException} with the specified cause. - * If {@code cause} is not {@code null}, message string of this exception will be set to {@code cause.toString()}. - *

- * Merely calling this constructor does not block - * current {@link ReactiveScope}. Use {@link #block(Throwable)} for that. - * - * @param cause - * cause of this exception (possibly {@code null}) that can be later retrieved via {@link Throwable#getCause()} - * - * @see #block(Throwable) - */ - public ReactiveBlockingException(Throwable cause) { - super(cause != null ? cause.toString() : null, cause); - } - /** - * Constructs new {@code ReactiveBlockingException}. - * The exception will have no message, i.e. it will return {@code null} from {@link Throwable#getMessage()}. - *

- * Merely calling this constructor does not block - * current {@link ReactiveScope}. Use {@link #block()} for that. - * - * @see #block() - */ - public ReactiveBlockingException() { - this(null, null); - } - /* - * Blocking of current computation can be explicitly requested by calling one of the methods below. - * These methods throw instead of returning the exception to make sure people don't forget to throw it. - * They nevertheless declare exception return, so that callers can add throw clause to avoid issues with unreachable code. - */ - /** - * Marks the current {@link ReactiveScope} as blocking - * and then throws {@code ReactiveBlockingException} with the specified message and cause. - * Current reactive scope is blocked by calling {@link CurrentReactiveScope#block()}. - *

- * This method always throws and thus never returns. Declared return type is just a convenience - * that lets callers avoid unreachable code errors by placing the call in a {@code throw} statement, - * e.g. {@code throw ReactiveBlockingException.block(...)}. - * - * @param message - * message (possibly {@code null}) passed to {@link #ReactiveBlockingException(String, Throwable)} - * @param cause - * cause (possibly {@code null}) passed to {@link #ReactiveBlockingException(String, Throwable)} - * @return never returns - * - * @see #ReactiveBlockingException(String, Throwable) - */ - public static ReactiveBlockingException block(String message, Throwable cause) { - CurrentReactiveScope.block(); - throw new ReactiveBlockingException(message, cause); - } - /** - * Marks the current {@link ReactiveScope} as blocking - * and then throws {@code ReactiveBlockingException} with the specified message. - * Current reactive scope is blocked by calling {@link CurrentReactiveScope#block()}. - *

- * This method always throws and thus never returns. Declared return type is just a convenience - * that lets callers avoid unreachable code errors by placing the call in a {@code throw} statement, - * e.g. {@code throw ReactiveBlockingException.block(...)}. - * - * @param message - * message (possibly {@code null}) passed to {@link #ReactiveBlockingException(String)} - * @return never returns - * - * @see #ReactiveBlockingException(String) - */ - public static ReactiveBlockingException block(String message) { - throw block(message, null); - } - /** - * Marks the current {@link ReactiveScope} as blocking - * and then throws {@code ReactiveBlockingException} with the specified cause. - * Current reactive scope is blocked by calling {@link CurrentReactiveScope#block()}. - *

- * This method always throws and thus never returns. Declared return type is just a convenience - * that lets callers avoid unreachable code errors by placing the call in a {@code throw} statement, - * e.g. {@code throw ReactiveBlockingException.block(...)}. - * - * @param cause - * cause (possibly {@code null}) passed to {@link #ReactiveBlockingException(Throwable)} - * @return never returns - * - * @see #ReactiveBlockingException(Throwable) - */ - public static ReactiveBlockingException block(Throwable cause) { - throw block(null, cause); - } - /** - * Marks the current {@link ReactiveScope} as blocking - * and then throws {@code ReactiveBlockingException}. - * Current reactive scope is blocked by calling {@link CurrentReactiveScope#block()}. - *

- * This method always throws and thus never returns. Declared return type is just a convenience - * that lets callers avoid unreachable code errors by placing the call in a {@code throw} statement, - * e.g. {@code throw ReactiveBlockingException.block(...)}. - * - * @return never returns - * - * @see #ReactiveBlockingException() - */ - public static ReactiveBlockingException block() { - throw block(null, null); - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactiveExecutor.java b/src/main/java/com/machinezoo/hookless/ReactiveExecutor.java deleted file mode 100644 index 343c40b..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactiveExecutor.java +++ /dev/null @@ -1,187 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.*; -import com.machinezoo.stagean.*; -import io.micrometer.core.instrument.*; -import io.micrometer.core.instrument.Timer; - -/* - * Latency-optimized executor designed for hookless. Currently it's just standard ThreadPoolExecutor with custom queue. - * - * Reactive executor has an event concept. Event is a group of related tasks that likely originated from single UI event. - * When event's task schedules another task (e.g. due to reactive invalidation), the new task becomes part of the same event. - * On the other hand, when task is scheduled from outside of the thread pool, it is assumed to be part of the next event. - * - * When not under load, not even intermittently, latency is low regardless of executor. - * This executor is designed to provide good latencies even under load. - * The problem it is solving is that the usual FIFO queuing causes cascading tasks to have latency that is a multiple of the FIFO queue length. - * This is a problem for hookless, which encourages construction of deep networks of reactive computations. - * LIFO queuing is not a solution either as it has exponential complexity in the worst case. - * Reactive executor solves the problem by keeping one global FIFO queue of events that each have their own local FIFO queue of tasks. - * This has the effect that cascading tasks inside one event will all run together, yielding short latencies independent of cascading depth. - * There is a reasonable limit on cascading depth to prevent buggy code from creating events that never stop. - */ -/** - * Latency-optimized executor designed for reactive programs. - */ -@StubDocs -public class ReactiveExecutor extends ThreadPoolExecutor { - private static ThreadLocal running = new ThreadLocal<>(); - /* - * Event and task FIFOs do not physically exist. We just assign increasing event and task IDs to all tasks - * and then use priority queue to execute tasks in event order followed by task order. - * Event counter is executor-local, so that slow thread pools do not impede progress in fast thread pools. - */ - private final AtomicLong eventCounter = new AtomicLong(); - private static final AtomicLong taskCounter = new AtomicLong(); - /* - * Expose event counter, so that non-default reactive thread pools can be monitored. - * There's no need to expose task counter in the same way, - * because ThreadPoolExecutor already exposes getTaskCount() that returns about the same number. - */ - public long getEventCount() { - return eventCounter.get(); - } - private static Timer taskTimer = Metrics.timer("hookless.executor.tasks"); - /* - * We will wrap every task submitted to the executor in order to make the tasks sortable by event/task ID. - */ - private static class ReactiveTask implements Runnable, Comparable { - final ReactiveExecutor executor; - final long eventId; - final long taskId = taskCounter.incrementAndGet(); - /* - * This is cascade depth. Child tasks have depth one higher than their parent task. - */ - final int depth; - final Runnable runnable; - final Timer.Sample sample; - ReactiveTask(ReactiveExecutor executor, long eventId, int depth, Runnable runnable) { - this.executor = executor; - this.eventId = eventId; - this.runnable = runnable; - this.depth = depth; - /* - * Task timers are only enabled for the common thread pool. - * Tasks in other thread pools can be timed for example by creating ExecutorService wrapper - * and then wrapping every task submitted to the executor. - * - * By starting the timer sample here, we include queuing latency in the measurement. - * Total latency is usually more important than throughput in hookless applications. - */ - sample = executor == common ? Timer.start() : null; - } - @Override - public int compareTo(ReactiveTask other) { - if (eventId != other.eventId) - return eventId < other.eventId ? -1 : 1; - else - return Long.compare(taskId, other.taskId); - } - @Override - public void run() { - /* - * While incrementing task ID is straightforward, we have several options as to when to increment event ID. - * Here we increment it when the first task of the current event starts execution. - * This way we will typically have only two events: current one and the next one. - * Multi-threaded execution can however cause some older events to be still finishing their tasks. - * - * Incrementing event ID in this way makes it likely that a train of consecutive task submissions will share one event ID. - * Related tasks that are likely coming from the same UI interaction will form single event in the executor. - * - * When the executor is overloaded, multiple UI-level events get aggregated under the next event ID. - * That prevents buildup of a queue of tiny events that would each expand into a large tree of tasks. - * This kind of inefficiency is also present when we have only two events (current and the next one), - * but its impact on throughput is tolerable when there are only two active events. - * Normal FIFO queue of ThreadPoolExecutor would eliminate all such inefficiency, - * but reactive executor is designed to reduce latency and we are willing to sacrifice some throughput for it. - */ - executor.eventCounter.compareAndSet(eventId, eventId + 1); - /* - * Expose reference to the current task via thread-local variable, so that child tasks spawned from this task inherit event ID. - */ - running.set(this); - try { - runnable.run(); - } finally { - /* - * Remove. Do not set to null as that might result in accumulation of state when threads are stopped and started in the pool. - */ - running.remove(); - if (sample != null) - sample.stop(taskTimer); - } - } - } - /* - * Unbounded queue means that ThreadPoolExecutor can run only as a fixed-size thread pool. - */ - public ReactiveExecutor(int parallelism, ThreadFactory threads) { - /* - * Leaving the queue unbounded involves only a small risk of memory exhaustion, - * because reactive objects are designed to create at most one task at a time. - * The only way we could exhaust memory here is if the reactive objects - * are gargbage-collected faster than we can execute their callbacks. - */ - super(parallelism, parallelism, 0, TimeUnit.MILLISECONDS, new PriorityBlockingQueue<>(), threads); - } - public ReactiveExecutor(int parallelism) { - this(parallelism, Executors.defaultThreadFactory()); - } - public ReactiveExecutor() { - this(Runtime.getRuntime().availableProcessors()); - } - /* - * We have to choose maximum cascading depth to prevent infinite cascades (busy-looping reactive code) - * from creating infinite events that would live-lock all other reactive computations. - * Almost all task cascades are less than 10 tasks deep. If we set the limit to 30, it will be sufficient with wide margin. - * Even if the cascade is longer, tasks will still be aggregated in groups of 30, reducing total latency by a factor of 30. - */ - private static final int MAX_DEPTH = 30; - /* - * We could also override newTaskFor(), but then tasks submitted directly via execute() would not be wrapped. - */ - @Override - public void execute(Runnable runnable) { - Objects.requireNonNull(runnable); - ReactiveTask current = running.get(); - /* - * We will check whether the current task belongs to this executor, because every executor has separate event counter. - */ - if (current != null && current.executor == this && current.depth < MAX_DEPTH) - super.execute(new ReactiveTask(this, current.eventId, current.depth + 1, runnable)); - else - super.execute(new ReactiveTask(this, eventCounter.get(), 0, runnable)); - } - public static ReactiveExecutor current() { - ReactiveTask task = running.get(); - return task != null ? task.executor : null; - } - /* - * We will define one common reactive executor that will be used as default in all reactive primitives that need an executor. - * This executor will be compute-optimized with thread count equal to core count. Submitting blocking operations here will undermine performance. - */ - private static final ReactiveExecutor common = new ReactiveExecutor(Runtime.getRuntime().availableProcessors(), new ThreadFactory() { - @Override - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(runnable); - /* - * Do not block process termination just because some reactive computations are still running in the background. - */ - thread.setDaemon(true); - thread.setName("hookless-" + thread.getId()); - return thread; - } - }); - static { - Metrics.gauge("hookless.executor.events", common, x -> x.getEventCount()); - Metrics.gauge("hookless.executor.threads", common, x -> x.getPoolSize()); - Metrics.gauge("hookless.executor.queue", common, x -> x.getQueue().size()); - } - public static ReactiveExecutor common() { - return common; - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactiveFreezes.java b/src/main/java/com/machinezoo/hookless/ReactiveFreezes.java deleted file mode 100644 index 8fc40eb..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactiveFreezes.java +++ /dev/null @@ -1,93 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import java.util.*; -import java.util.function.*; -import com.machinezoo.stagean.*; - -/* - * Freezes get their own object, so that they can be shared between nested scopes if needed. - * We can also make the internal state observable and modifiable by placing extra methods here. - * We could have just exposed Map>, but that would prevent future API improvements. - */ -/** - * Container for frozen outputs of reactive computations. - */ -@StubDocs -public class ReactiveFreezes { - /* - * The map is lazily constructed to get some small performance gain, - * because freezes are relatively rarely used. - * - * We are storing reactive value rather than just plain value, so that we can freeze exceptions. - * Blocking flag is captured too, but since the value is unpacked immediately, - * the blocking flag gets propagated to the current reactive scope and its storage here is redundant. - */ - private Map> map; - /* - * Read-only parent collection can be configured. - * If it already contains freeze for given key, that freeze is returned instead. - * This is useful for controlling how state from nested scopes propagates to the parent scope. - */ - private ReactiveFreezes parent; - public ReactiveFreezes parent() { - return parent; - } - void parent(ReactiveFreezes parent) { - this.parent = parent; - } - /* - * Most application code calls freeze() on reactive scope for convenience, but this is the implementation. - * - * We have to perform an unchecked cast here. Compiler ensures that callers perform the actual type check. - * It's a runtime check, so there could be some surprises at runtime, - * but the structure of the API makes such bugs unlikely. - */ - @SuppressWarnings("unchecked") - public T freeze(Object key, Supplier supplier) { - Objects.requireNonNull(key); - Objects.requireNonNull(supplier); - for (ReactiveFreezes ancestor = this; ancestor != null; ancestor = ancestor.parent) { - if (ancestor.map != null) { - ReactiveValue stored = ancestor.map.get(key); - if (stored != null) - return ((ReactiveValue)stored).get(); - } - } - if (map == null) - map = new HashMap<>(); - ReactiveValue captured = ReactiveValue.capture(supplier); - map.put(key, captured); - return captured.get(); - } - /* - * Internal state of the freeze collection should be fully observable and modifiable. - * These methods do not touch parent freeze collection, because that one is accessible through parent(). - */ - public Set keys() { - if (map == null) - return Collections.emptySet(); - return map.keySet(); - } - public ReactiveValue get(Object key) { - if (map == null) - return null; - return map.get(key); - } - public void set(Object key, ReactiveValue value) { - Objects.requireNonNull(key); - /* - * Allow removing items by setting null value. It keeps the API simpler and shorter. - */ - if (value != null) { - if (map == null) - map = new HashMap<>(); - map.put(key, value); - } else if (map != null) - map.remove(key); - } - @Override - public String toString() { - return getClass().getSimpleName() + ": " + (map != null ? map.toString() : "(empty)"); - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactiveFuture.java b/src/main/java/com/machinezoo/hookless/ReactiveFuture.java deleted file mode 100644 index 2e43782..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactiveFuture.java +++ /dev/null @@ -1,294 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import java.lang.ref.*; -import java.time.*; -import java.util.*; -import java.util.concurrent.*; -import java.util.function.*; -import com.google.common.util.concurrent.*; -import com.machinezoo.hookless.time.*; -import com.machinezoo.hookless.util.*; -import com.machinezoo.stagean.*; - -/* - * Reactive wrapper around CompletableFuture. - */ -/** - * Reactive wrapper for {@link CompletableFuture}. - * - * @param - * type of result returned by the future - */ -@StubDocs -public class ReactiveFuture { - /* - * Strong reference to CompletableFuture. If something references us, CompletableFuture must stay alive. - */ - private final CompletableFuture completable; - public CompletableFuture completable() { - return completable; - } - /* - * Reactive future works by automatically transferring CompletableFuture state to reactive variable. - * CompletableFuture states are a subset of reactive values (including blocking), which makes this very simple. - * - * There's no need to keepalive() the reactive future, because completion callback - * keeps the reactive future alive for as long as is necessary to ensure reactivity. - * And CompletableFuture always stays alive long enough to invoke all chained actions. - */ - private final ReactiveVariable variable = OwnerTrace - .of(new ReactiveVariable(new ReactiveValue(null, null, true))) - .parent(this) - .target(); - /* - * This is a separate method to make absolutely sure the completion callback created in constructor - * will hold strong reference to 'this' (the reactive future) rather than just to the embedded reactive variable. - * Two-way strong reference between reactive future and CompletableFuture ties lifetimes of the two objects, - * which is essential to make deduplication of reactive future instances work in the WeakHashMap below. - */ - private void complete(T result, Throwable exception) { - variable.value(new ReactiveValue<>(result, exception, false)); - } - private ReactiveFuture(CompletableFuture completable) { - OwnerTrace.of(this).alias("future"); - Objects.requireNonNull(completable); - this.completable = completable; - OwnerTrace.of(completable).parent(this); - /* - * If the future is already completed, the callback is invoked before whenComplete() returns. - * If it is not completed yet, callback is invoked synchronously. - * That means there's no latency difference between CompletableFuture and its wrapping reactive future. - */ - completable.whenComplete(this::complete); - } - /* - * Give users freedom to choose whether to create CompletableFuture first or reactive future first. - */ - public ReactiveFuture() { - this(new CompletableFuture<>()); - synchronized (ReactiveFuture.class) { - associations.put(completable, new WeakReference<>(this)); - } - } - /* - * Creating new reactive future adds a completion handler to the wrapped CompletableFuture. - * If we allowed multiple reactive futures for one CompletableFuture, these completion handlers would pile up. - * We will therefore enforce single wrapper per CompletableFuture. This is also more convenient to use. - * - * WeakHashMap essentially adds a dynamic field to CompletableFuture that references the associated reactive future. - * The value of WeakHashMap however cannot be a strong reference, because WeakHashMap causes memory leaks - * when value (reactive future) holds strong reference to its key (CompletableFuture), which is our case. - * Only ephemerons can be used in such scenario, but those cannot be implemented in Java. - * See: - * https://en.wikipedia.org/wiki/Ephemeron - * https://stackoverflow.com/a/9166730 - * - * So we either remove strong back-reference to CompletableFuture, make it weak, or make WeakHashMap values weak. - * We cannot remove reference to CompletableFuture, because we offer API where reactive future is constructed first - * and then CompletableFuture is retrieved from it. We cannot make the CompletableFuture reference weak, - * because callers expect the CompletableFuture to exist as long as they hold a reference to its associated reactive future. - * So the only remaining option is to make WeakHashMap values weak. - * - * Reactive future is held alive by completion callback from reachable CompletableFuture (see complete() above). - * Completion callback exists at least for as long as the CompletableFuture is not completed. - * Reactive future therefore becomes unreachable when (1) it is not referenced directly - * and (2) its associated CompletableFuture is either unreachable or already completed. - * If the CompletableFuture is unreachable, then there is no one to complete it and reactivity is irrelevant. - * It the CompletableFuture is already completed, no state change can happen anymore and reactivity is again irrelevant. - * It is therefore safe for reactive future to be collected under these circumstances. - * Its reactive variable might live longer if it is a dependency of some reactive computation, which is harmless. - * - * These rules however allow reactive future to be collected while its (completed) CompletableFuture is still reachable. - * That is okay, because the completed CompletableFuture no longer holds the completion callback - * (as otherwise the reactive future would not be collected), so creating new reactive future for the CompletableFuture - * will not cause accumulation of the completion callbacks that we were trying to prevent with this. - * Callers would not ever observe two instances of reactive future for single CompletableFuture, - * because the first reactive future is collected only after there are no references to it, - * so callers cannot compare the second reactive future to anything they know. - */ - private static final Map, WeakReference>> associations = new WeakHashMap<>(); - @SuppressWarnings("unchecked") - public static synchronized ReactiveFuture wrap(CompletableFuture completable) { - Objects.requireNonNull(completable); - WeakReference> weak = associations.get(completable); - ReactiveFuture cached = weak != null ? weak.get() : null; - if (cached != null) - return (ReactiveFuture)cached; - ReactiveFuture reactive = new ReactiveFuture(completable); - associations.put(completable, new WeakReference<>(reactive)); - return reactive; - } - /* - * We can now expose reactive variants of CompletableFuture methods. - * Only read methods are provided. Writes and continuations can be done via the associated CompletableFuture. - * Method names have been adapted to fit hookless style. - */ - public boolean done() { - return !variable.value().blocking(); - } - public boolean failed() { - return variable.value().exception() != null; - } - public boolean cancelled() { - return variable.value().exception() instanceof CancellationException; - } - /* - * There is no join() method, because that one is just a workaround for checked exceptions thrown by CompletableFuture's get(). - * We do the right thing from the beginning and consistently throw CompletionException instead of ExecutionException. - * - * These methods don't need to be synchronized, because reactive variable is already synchronized. - */ - private T unpack(ReactiveValue value) { - if (value.exception() instanceof CancellationException) - throw new CancellationException(); - if (value.exception() != null) - throw new CompletionException(value.exception()); - return value.result(); - } - public T get() { - ReactiveValue value = variable.value(); - if (value.blocking()) { - /* - * Propagate blocking just like in CompletableFuture. - * We don't have any fallback value and it might not be correct to fallback to null, so just throw. - */ - throw ReactiveBlockingException.block(); - } - return unpack(value); - } - public T getNow(T fallback) { - ReactiveValue value = variable.value(); - if (value.blocking()) { - /* - * Don't propagate blocking. This method is specifically intended to avoid all blocking. - * CompletableFuture does not block (synchronously) either. - */ - return fallback; - } - return unpack(value); - } - /* - * Timeout should be counted from the first call to the timeouting overload in order to avoid cascading of timeouts. - * Implementing timeouts via reactive pins would never work reliably if at all. - */ - private Instant start; - /* - * CompletableFuture uses the legacy TimeUnit enum. This is a new API, so use the new Duration class instead. - * This is the only synchronized get* method due to the timestamp field access. - */ - public synchronized T get(Duration timeout) { - Objects.requireNonNull(timeout); - ReactiveValue value = variable.value(); - /* - * First check whether we have value available even if the timeout has been already reached. - * This results in somewhat odd behavior when the future first throws due to timeout and later returns the actual result. - * But then this is reactive API and function results are expected to change over time. - */ - if (value.blocking()) { - /* - * This is a rarely used feature. Initialize timestamp lazily to avoid burdening the typical case. - * This also lets us count time from the first moment this method was called rather than since object creation. - * That works better when this future is precreated and then lies around for some time. - */ - if (start == null) - start = Instant.now(); - if (ReactiveInstant.now().isAfter(start.plus(timeout))) { - /* - * Do not reactively block. The whole point of the timeout is to limit the duration of blocking. - * CompletableFuture does not block (synchronously) in this case either. - * - * Throw unchecked variant of TimeoutException to simplify use of the API and to stay consistent with other hookless APIs. - * This exception type comes from Guava, which means we are creating hard dependency on Guava. - * That's somewhat controversial, but it's better than declaring our own or throwing checked exceptions. - */ - throw new UncheckedTimeoutException(); - } - /* - * If timeout has not been reached yet, continue like in get(). - */ - throw ReactiveBlockingException.block(); - } - return unpack(value); - } - /* - * Offer the TimeUnit-based API as well for compatibility with CompletableFuture. - */ - public T get(long timeout, TimeUnit unit) { - /* - * Java 9 has TimeUnit.toChronoUnit(), which could be then used with Duration.of(). - * In Java 8, roundtrip via nanoseconds will suffice for timeouts up to 292 years. - */ - return get(Duration.ofNanos(unit.toNanos(timeout))); - } - @Override - public String toString() { - ReactiveValue value = variable.value(); - if (value.blocking()) - CurrentReactiveScope.block(); - return OwnerTrace.of(this) + " = " + (value.blocking() ? "(pending)" : Objects.toString(value.exception(), Objects.toString(value.result()))); - } - /* - * The following methods are equivalents of CompletableFuture's run/supplyAsync methods. - * Provided reactive supplier/runnable is executed repeatedly until it completes without blocking. - * - * These methods serve as a bridge from reactive computations to the async world of CompletableFuture, - * which is why they return CompletableFuture instead of reactive future. - */ - public static CompletableFuture supplyReactive(Supplier supplier, Executor executor) { - Objects.requireNonNull(supplier); - Objects.requireNonNull(executor); - CompletableFuture future = new CompletableFuture(); - /* - * We have to return only CompletableFuture, but it is convenient to have reactive future too. - * We can then avoid complicated callbacks needed just to implement cancellation. - */ - ReactiveFuture reactive = wrap(future); - /* - * The reactive thread is configured to run in non-daemon mode, so that it keeps running until the CompletableFuture is completed. - * This reflects the way corresponding run/supplyAsync methods in CompletableFuture work. - * This is not much of an issue, because the reactive computations are typically very short. - * There is unfortunately still a small risk that these reactive computations will block forever. - * If that is a concern for the application, it should wrap the supplier/runnable with custom timeout check. - */ - ReactiveThread thread = new ReactiveThread() - .runnable(() -> { - /* - * Allow explicit cancellation. CompletableFuture also supports this feature but only before the supplier is started. - * Since the reactive supplier is executed multiple times, it is possible to cancel it when already running. - * This is a little bit incorrect, because we are ignoring the flag that was passed to future's cancel() method. - * Checking state of reactive future instead of the CompletableFuture speeds up termination and release of resources. - * We also allow cancellation by normal or exceptional completion of the future just like run/supplyAsync does. - */ - if (reactive.done()) { - ReactiveThread.current().stop(); - return; - } - ReactiveValue value = ReactiveValue.capture(supplier); - if (!CurrentReactiveScope.blocked()) { - if (value.exception() != null) - future.completeExceptionally(value.exception()); - else - future.complete(value.result()); - ReactiveThread.current().stop(); - } - }) - .executor(executor); - OwnerTrace.of(thread).parent(future); - thread.start(); - return future; - } - public static CompletableFuture supplyReactive(Supplier supplier) { - return supplyReactive(supplier, ReactiveExecutor.common()); - } - public static CompletableFuture runReactive(Runnable runnable, Executor executor) { - Objects.requireNonNull(runnable); - return supplyReactive(() -> { - runnable.run(); - return null; - }, executor); - } - public static CompletableFuture runReactive(Runnable runnable) { - return runReactive(runnable, ReactiveExecutor.common()); - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactiveLazy.java b/src/main/java/com/machinezoo/hookless/ReactiveLazy.java deleted file mode 100644 index 96746fb..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactiveLazy.java +++ /dev/null @@ -1,65 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import java.util.*; -import java.util.function.*; -import com.machinezoo.hookless.util.*; -import com.machinezoo.stagean.*; - -/* - * Reactive computation graph needs intermediate nodes that serve as reactive consumers and sources at the same time. - * These intermediate nodes are usually asynchronous, reflecting changes in dependencies after some delay due to queuing. - * This class is a synchronous version of such an intermediate node. Reads from lazy objects reflect recent writes. - * Synchronous nature of this class comes at the cost of numerous limitations. - * - * Lazy or "memo" (from memoization) calls provided supplier once upon first access, caches the result, - * and then keeps returning the cached result without further invocations of the supplier. - * The special thing about reactive lazy, besides support for reactive values (especially reactive blocking), - * is that it can be invalidated when dependencies change. It therefore avoids staleness of its non-reactive counterpart. - * Invalidation resets reactive lazy to its initial state except that reactive pins are preserved when necessary. - */ -/** - * Single-value synchronous cache for results of reactive computations. - * - * @param - * type of cached result - */ -@StubDocs -public class ReactiveLazy implements Supplier { - /* - * Reactive lazy is a special case of reactive state machine. Its implementation is therefore a trivial wrapper. - */ - private final ReactiveStateMachine generator; - public ReactiveLazy(Supplier supplier) { - Objects.requireNonNull(supplier); - OwnerTrace.of(this).alias("lazy"); - generator = OwnerTrace.of(ReactiveStateMachine.supply(supplier)) - .parent(this) - .target(); - } - /* - * We will expose only unpacked reactive value as is common in reactive code. - * Application can always capture the full reactive value explicitly if it wants to. - * - * No need to synchronize here, because reactive state machine is already synchronized. - * The code below will have the same effect regardless of whether this method is synchronized - * and that is true also in case the state is invalidated immediately after being computed. - */ - @Override - public T get() { - /* - * Always advance to ensure the returned value reflects latest writes. - * Reactive state machine is smart enough to avoid unnecessary advancement and to handle concurrent invocations. - */ - generator.advance(); - /* - * By the time we get here, another thread might have performed another advancement. - * We don't care, because we are returning fresh value in either case. - */ - return generator.output().get(); - } - @Override - public String toString() { - return OwnerTrace.of(this) + " = " + generator.output(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactivePins.java b/src/main/java/com/machinezoo/hookless/ReactivePins.java deleted file mode 100644 index 62fdd7e..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactivePins.java +++ /dev/null @@ -1,156 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import java.util.*; -import java.util.function.*; -import com.machinezoo.stagean.*; - -/* - * Container for reactive pins that span multiple blocking computations. Rationale is similar to the one for reactive freezes. - * - * Contrary to older versions of hookless, pinned objects are no longer notified when reactive computation completes. - * This "close" event is theoretically useful, but it adds a lot of complexity and makes reactive scopes hard to compose. - * It was previously used in reactive time, but it was removed at the cost of small CPU/memory overhead. - */ -/** - * Container for pinned outputs of reactive computations. - */ -@StubDocs -public class ReactivePins { - /* - * Equivalents of corresponding code in reactive freezes. - */ - private Map> map; - private ReactivePins parent; - public ReactivePins parent() { - return parent; - } - void parent(ReactivePins parent) { - this.parent = parent; - } - @SuppressWarnings("unchecked") - public T pin(Object key, Supplier supplier) { - Objects.requireNonNull(key); - Objects.requireNonNull(supplier); - for (ReactivePins ancestor = this; ancestor != null; ancestor = ancestor.parent) { - if (ancestor.map != null) { - ReactiveValue stored = ancestor.map.get(key); - if (stored != null) - return ((ReactiveValue)stored).get(); - } - } - if (map == null) - map = new HashMap<>(); - ReactiveValue captured = ReactiveValue.capture(supplier); - /* - * If the computation inside the supplier blocks, there's no point in creating a pin for it. - * Pins don't get an opportunity to complete their blocking supplier in future computation. - * Pins can only represent finished operations that don't wait for anything anymore. - * That's why we avoid recording blocking pins here and return supplier's result directly every time. - * Code in the supplier is then allowed to complete its blocking operations in future computations. - * Once that happens, we happily record the result of the supplier as a new pin. - * - * Reactive scope's pin() method records every accessed pin as a freeze. - * As a side-effect of that, blocking computations are downgraded to freezes automatically. - * So even though we return early here, the returned result will be stable throughout the current computation. - */ - if (captured.blocking()) - return captured.get(); - map.put(key, captured); - return captured.get(); - } - public Set keys() { - if (map == null) - return Collections.emptySet(); - return map.keySet(); - } - public ReactiveValue get(Object key) { - if (map == null) - return null; - return map.get(key); - } - public void set(Object key, ReactiveValue value) { - Objects.requireNonNull(key); - if (value != null) { - /* - * Pins cannot capture blocking. It only makes sense that blocking value cannot be explicitly set. - */ - if (value.blocking()) - throw new IllegalArgumentException(); - if (map == null) - map = new HashMap<>(); - map.put(key, value); - } else if (map != null) - map.remove(key); - } - /* - * Pins are constructed using lambdas that may reference final variables previously produced by current reactive computation. - * Even if we wrap the lambda in nested reactive scope, we wouldn't be able to capture dependencies for these variables. - * Pins therefore implicitly depend on all data that was previously accessed by current reactive computation. - * - * Worse yet, since pins can access data from other pins that may have been captured in some previous blocking computation, - * pins implicitly depend on all data that was accessed so far in any of the previous blocking computations. - * And since the current computation is free to use the pins to derive more data, - * the entirety of the current computation depends on all data accessed during current or any of the previous blocking computations. - * - * If we set up a trigger to listen on all versions collected during all previous blocking computations, - * the trigger will fire immediately, because some version from past computations is always outdated - * as otherwise we wouldn't be running new computation now. - * Such immediate firing would create busy loop where the same computation would run again and again. - * - * In order to allow the computation to halt for a while, we set up triggers for blocking computations - * to only monitor versions collected during that one blocking computation. - * We don't let triggers monitor versions from previous computations. - * - * The case for final computation (the one that doesn't block) is a bit more complicated. - * In this case, we should set up trigger that monitors versions collected during any previous blocking computation - * in addition to versions collected during the current non-blocking computation. - * This would be conceptually correct, because the final result of several such computations - * depends on everything accessed during prior blocking computations if pins were used. - * But it would be also inefficient, because we know the trigger would fire immediately. - * We will instead short-circuit this process by returning single outdated version from the final non-blocking scope. - * - * This will force the computation to run once again, but without having any history of past pins or blocking computations. - * This time, hopefully, all the data is already cached, so the computation completes on the first try without any blocking. - * Even though pins will be collected, they can only depend on the current computation. - * We can then finally set up proper trigger with a list of versions from the current computation. - * - * And this is where the validity flag defined below comes in. Conceptually, pins can be assumed to be up-to-date - * only if this is a non-blocking computation and there was no past blocking computation. - * So we start with validity flag set to true and clear it when we encounter blocking computation. - * This is done by scope's block() method, which calls invalidate() method on this pin collection. - * - * Considering this strange behavior of pins, it is usually better to freeze unless there is a good reason to pin. - * When pinning, one must keep in mind that any blocking will cause the whole computation to repeat at least one more time. - * Pinning is thus a bit inefficient. On the other hand, if there are blocking computations, - * then the final result already requires more than one computation and adding one more is not so bad. - * When there are no blocking computations, pins are as efficient as freezes. - * If the final result is not monitored for changes (for example in case of reactive servlets), - * then pin invalidation is ignored and there is no performance impact. - */ - private boolean valid = true; - public boolean valid() { - if (parent != null && !parent.valid()) - return false; - /* - * Conceptually, we are invalidating pins, not the pin collection. - * So if there are no pins, there can be no invalidated pins either. - * That's why we return true here in case the pin collection is empty. - */ - return valid || map == null || map.isEmpty(); - } - public void invalidate() { - /* - * We can skip invalidation if there are no pins since there is nothing to invalidate. - * No new pins will be created during this computation, because it has been marked as blocked. - * If pins are created during later computations, they can be still invalidated by blocking in that computation. - * It should be noted though that it is fairly unlikely for pins to appear only in some computations for the same code. - * This is therefore a corner case optimization and we better avoid it in order to spare us some corner case bugs. - */ - valid = false; - } - @Override - public String toString() { - return getClass().getSimpleName() + ": " + (map != null ? map.toString() : "(empty)"); - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactiveScope.java b/src/main/java/com/machinezoo/hookless/ReactiveScope.java deleted file mode 100644 index b78fab9..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactiveScope.java +++ /dev/null @@ -1,317 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static java.util.stream.Collectors.*; -import java.util.*; -import java.util.function.*; -import com.machinezoo.closeablescope.*; -import com.machinezoo.hookless.util.*; -import com.machinezoo.stagean.*; -import it.unimi.dsi.fastutil.objects.*; - -/* - * This is the thread-local thing that is essential for hookless way of doing reactivity. - * Any computation running within the scope will log dependencies in it, making seamless reactivity possible. - * - * It is tempting to generalize reactive scope into universal thread-local context and use it to solve a variety of problems, - * for example widget dependencies (CSS/JS in the document head) in pushmode or propagating deadlines. - * This would however introduce a lot of complexity without really solving any of those complex problems. - * It is better to have multiple libraries, each with its own context object and let apps combine them. - * In case of hookless, specialized context class allows us to manipulate the scope while it is not active - * and to inspect it in detail and modify it intelligently. General context would make all of that very difficult. - * - * Reactive scope is deliberately not thread-safe. People nearly always want to run reactive computations single-threaded. - * Supporting the rare case of parallelized computations would cost us a lot of locking overhead and some complexity. - * Parallelized computations should instead create new scope (each with its own freezes and pins) for every thread. - * Dependencies from these scopes can then be merged into single resulting scope. - * The only downside of this solution is that parallelized computations have limited access to freeze and pin functionality. - */ -/** - * Thread-local context for reactive computations that collects reactive dependencies. - */ -@StubDocs -public class ReactiveScope { - public ReactiveScope() { - OwnerTrace.of(this).alias("scope"); - } - /* - * Scope must be the active scope for dependency logging to occur. - * There is at most one active scope per thread, pointed to by a thread-local variable. - * - * It is possible for thread to have no active scope. We don't bother to provide default scope, - * because CurrentReactiveScope already provides much better fallback mechanism. - * ReactiveScope therefore shouldn't be used directly, because hard-coded references - * to ReactiveScope.current() would result in null pointer exceptions, - * which is particularly likely and problematic in unit tests. - */ - private static final ThreadLocal current = new ThreadLocal(); - public static ReactiveScope current() { - return current.get(); - } - /* - * Scopes happily nest on a single stack. - * This is done by making each scope remember its parent and restore the parent upon completion. - */ - private ReactiveScope parent; - /* - * Scopes are designed to be used with Java's try-with-resources construct. - * We could also offer run(Runnable) and supply(Supplier) methods, but those are more suitable for APIs that are used frequently. - * ReactiveScope is a fairly low-level API, so we will just provide the try-with-resources variant. - * - * We do not create tracing scope for every reactive scope/computation even though it feels natural. - * Reactive computations generally shouldn't have side effects, - * which means nothing interesting happens while they run and there is nothing interesting to log in the trace. - * Computations might take a long time, which is interesting, but they usually consume nearly all the time - * of their containing thread pool task, which is already traced via reactive trigger. - */ - public CloseableScope enter() { - if (parent != null) - throw new IllegalStateException("Cannot enter the same reactive scope recursively."); - parent = current.get(); - current.set(this); - return () -> { - current.set(parent); - parent = null; - }; - } - /* - * It is often useful to prevent dependency logging within some scope. - * This could be done by temporarily activating a throwaway scope, - * but it is more efficient and arguably cleaner to set active scope to null. - */ - public static CloseableScope ignore() { - ReactiveScope parent = current.get(); - current.set(null); - return () -> current.set(parent); - } - /* - * We can only depend on one version of every variable, which means we need a map from variables to their versions. - */ - private Object2LongMap> dependencies = new Object2LongOpenHashMap<>(); - /* - * However, we cannot expose a data structure like this through the API, because it can easily change. - * We will instead let callers iterate over a sequence of version objects. - * Efficiency is not that important. We can provide ReactiveTrigger with direct access if needed. - * We most importantly care about clean API here. - */ - public Collection versions() { - /* - * If there are invalidated pins from previous blocking computations, - * we have to assume that our version list is incomplete, because pin dependencies have not been preserved. - * We will return single out-of-date version in order to force reevaluation of the reactive computation, - * which will hopefully complete without blocking and there will be therefore no more invalidated pins. - * - * Invalidated pins do not invalidate blocking computations, because the next blocking computation will have the same pins. - * If we invalidated blocking computations too, it would result in busy looping since the pins wouldn't get updated by more computations. - * Blocking computations only need to wait for completion of their blocking reads in order to make progress. - */ - if (!blocked && pins != null && !pins.valid()) { - return Collections.singletonList(new ReactiveVariable.Version(invalidated, invalidated.version() - 1)); - } - /* - * This is quite inefficient, but the API allows for very high efficiency. We could have a custom collection. - */ - List versions = dependencies.object2LongEntrySet().stream() - .map(e -> new ReactiveVariable.Version(e.getKey(), e.getLongValue())) - .collect(toList()); - return Collections.unmodifiableCollection(versions); - } - private static ReactiveVariable invalidated = new ReactiveVariable<>(); - static { - invalidated.set(new Object()); - } - /* - * ReactiveVariable will call this method to add itself to the list of dependencies. - * If the same variable is read twice, we want to remember version from the first access. - * That's why we first check the dependency map for duplicates. - * - * ReactiveVariable could have just as well called the method with explicit version, - * but that one is a tiny bit smaller, because it cannot assume the supplied version is the latest one. - */ - public void watch(ReactiveVariable variable) { - Objects.requireNonNull(variable); - if (!dependencies.containsKey(variable)) - dependencies.put(variable, variable.version()); - } - /* - * It is also possible to add specific version to the dependency list. - * This is useful when manipulating the scope explicitly, for example when merging dependencies from inner scope. - * Since version numbers are increasing, we can easily determine - * whether the inserted or the already stored version is the earlier one. - * - * We might theoretically need other methods to manipulate dependencies, but ReactiveScope is conceptually a builder, - * so we are satisfied with an API that lets callers rebuild the scope with some dependencies modified or filtered out. - */ - public void watch(ReactiveVariable variable, long version) { - Objects.requireNonNull(variable); - long previous = dependencies.getLong(variable); - if (previous == 0 || version < previous) - dependencies.put(variable, version); - } - /* - * Blocking is necessary to prevent jerky display of incomplete results followed by complete results a split-second later. - * It also prevents propagation of incomplete results through machine interfaces like the initial HTTP GET in PushMode. - * Any reactive data source can block the computation if it cannot return complete data immediately. - * Caller is then expected to re-run the computation later (when current one is invalidated). - */ - private boolean blocked; - public boolean blocked() { - return blocked; - } - /* - * We don't provide corresponding unblock() method, because blocking can be temporarily disabled using nonblocking() - * and the builder-like nature of ReactiveScope means that callers can rebuild the scope when they want the blocking flag cleared. - */ - public void block() { - blocked = true; - /* - * If we block, then there will be another computation. - * That other computation wouldn't be able to track dependencies for pins created in this one. - * So let's mark all the pins as invalidated, so that following computations know about this. - */ - pins().invalidate(); - } - /* - * Instead of having special non-blocking flag on the scope (as it used to be in past versions), - * nonblocking() uses nesting and inspection of scopes to achieve the same in a very general way. - * It creates nested scope and runs the non-blocking operations within the nested scope. - * When done, it copies dependencies to the current scope and importantly avoids copying the blocked flag. - */ - public static CloseableScope nonblocking() { - /* - * It is quite possible the parent scope is null, especially during test runs. - * We should avoid creating nested scope in that case, - * because the non-blocking context is expected to be transparent in every way except for blocking. - */ - ReactiveScope parent = current(); - if (parent == null) { - return () -> {}; - } - ReactiveScope scope = OwnerTrace.of(new ReactiveScope()) - .parent(parent) - .alias("nonblocking") - .generateId() - .target(); - /* - * Share freezes and pins with parent scope. Callers expect non-blocking context to be transparent for freezes and pins. - * For pins, don't propagate changes automatically to the parent scope, because we will want to filter them later. - */ - scope.freezes(parent.freezes()); - scope.pins().parent(parent.pins()); - /* - * Inherit blocking from parent if that one is already blocked. - * This will prevent confusion inside the non-blocking scope, including incorrect pinning of blocking results. - * Non-blocking scope only prevents blocking from propagating from the inner scope to the outer one. - */ - if (parent.blocked()) - scope.block(); - CloseableScope computation = scope.enter(); - return () -> { - computation.close(); - /* - * Propagate pins to parent scope. If there are any, then it means the parent scope is not blocked. - * Any pins collected in the nested scope must have been collected before the nested scope was marked as blocking. - * So these are effectively standard pins computed without reliance on any blocking operations. - * - * While pins are propagated, pin invalidation is not. - * If pins were invalidated in nested scope, it must have happened because of blocking in the nested scope. - * Since this is non-blocking scope, we don't want to propagate any effects of blocking, including pin invalidation. - */ - for (Object key : scope.pins().keys()) - parent.pins().set(key, scope.pins().get(key)); - /* - * We must be careful here. Nested scope's versions() could return single out-of-date version due to invalidated pins. - * Nevertheless, if pins have been invalidated, then nested scope must have been blocked. - * If it was blocked, then checking of pin invalidation is disabled and versions() behaves normally. - * All that means we can safely query versions() of the nested scope here and assume standard behavior. - */ - for (ReactiveVariable.Version version : scope.versions()) - parent.watch(version.variable(), version.number()); - }; - } - /* - * Since other threads can change global state at any time, we would normally have to safeguard against it. - * - * Code with race rules: - * - * if (query() > 0) - * process(query()) - * - * Safeguarding against race rules: - * - * int value = query(); - * if (value > 0) - * process(value); - * - * Freezing offers another solution. It evaluates some code only once per scope's lifetime, - * making sure there is only one stable result. If query() internally freezes its result, - * we don't have to safeguard against race rules. - * - * Freezing implementation: - * - * int query() { - * return CurrentReactiveScope.freeze("query", () -> someDatabaseRead()); - * } - * - * We can then rely on the value to not change during one computation: - * - * if (query() > 0) - * process(query()) - * - * Freezing is therefore a non-essential convenience, but it is nevertheless very useful. - */ - private ReactiveFreezes freezes; - public T freeze(Object key, Supplier supplier) { - return freezes().freeze(key, supplier); - } - public ReactiveFreezes freezes() { - /* - * Lazily created to avoid the overhead for computations that don't need freezes. - */ - if (freezes == null) - freezes = new ReactiveFreezes(); - return freezes; - } - public void freezes(ReactiveFreezes freezes) { - /* - * The supplied freeze container may be null. In that case, we create new container the first time it is needed. - */ - this.freezes = freezes; - } - /* - * Blocking may never stop if the reactive computation changes dependencies every time, - * perhaps because some continuously changing dependency is used as a pointer to other blocking dependencies. - * We therefore allow the reactive computation to "pin" some values. - * All caches and other scope-manipulating code is expected to share pins over a sequence of blocking computations. - * Pins are a separate class to aid in this sharing and to also hide internal representation. - * - * Pins are a fundamental construct. Synchronous code can remember data it acquired before executing blocking operations. - * Reactive code similarly needs a way to remember its own decisions and data acquired before blocking, - * which, in the case of reactive code, happened in previous reactive computation that was marked as blocked. - * - * Code below is equivalent to corresponding freeze code above. - */ - private ReactivePins pins; - public T pin(Object key, Supplier supplier) { - /* - * Forward all pinning requests through freeze(), so that freezes are a superset of pins, - * at least of those pins accessed during current computation. - * - * This has the side-effect that blocking computation in the supplier, after being rejected as a pin, - * will be accepted as a freeze. This way blocking computations are automatically downgraded from pins to freezes. - */ - return freeze(key, () -> pins().pin(key, supplier)); - } - public ReactivePins pins() { - if (pins == null) - pins = new ReactivePins(); - return pins; - } - public void pins(ReactivePins pins) { - this.pins = pins; - } - @Override - public String toString() { - return OwnerTrace.of(this).toString(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactiveStateMachine.java b/src/main/java/com/machinezoo/hookless/ReactiveStateMachine.java deleted file mode 100644 index 7c419ac..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactiveStateMachine.java +++ /dev/null @@ -1,210 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import java.util.*; -import java.util.function.*; -import com.machinezoo.closeablescope.*; -import com.machinezoo.hookless.util.*; -import com.machinezoo.stagean.*; - -/* - * Reactive computations sometimes need to control other reactive computations. - * This can be done with reactive primitives (scope, trigger, pins), - * but these primitives are intended for integration with non-reactive code and for implementing new reactive constructs. - * It is also possible to run the controlled reactive computation in its own reactive thread, - * but such solution sacrifices too much control and makes synchronous interactions with the controlled computation hard. - * - * This class is a high-level wrapper around reactive primitives (scope, trigger, pins, variable) - * that exposes high-level reactive API to the controlling reactive computation. - * The API allows single-stepping through the controlled reactive computation. Hence the state machine metaphor. - */ -/** - * Monitoring and control over another reactive computation. - * - * @param - * output type of the computation - */ -@StubDocs -public class ReactiveStateMachine { - /* - * Last output of the controlled reactive computation. - */ - private final ReactiveVariable output; - /* - * False when the state machine needs to make another step. - * - * We could alternatively hold the entire publicly visible state in one reactive variable to minimize number of dependencies. - * We choose two reactive variables mostly for simplicity with minor benefits in traceability and fine-grained dependencies. - */ - private final ReactiveVariable valid = OwnerTrace - .of(new ReactiveVariable<>(false)) - .parent(this) - .tag("role", "valid") - .target(); - /* - * The controlling reactive computation is expected to query the state. - * This creates dependency on the state machine, so that we can wake up the controlling computation - * when step is made or when last output is invalidated due to dependency change. - */ - public ReactiveValue output() { - /* - * This records dependency on the state machine. No dependency is recorded on the controlled computation itself. - */ - return output.value(); - } - public boolean valid() { - return valid.get(); - } - /* - * Controlling reactive computation needs to inspect output at least to observe exceptions and blocking. - * We can just as well add result to make the reactive value complete, which means the controlled computation is defined by Supplier. - * - * We however want to allow Runnable. Constructor overload would work despite some ambiguity, - * but it would be unwieldy for Runnable because of the type parameter. - * We have to use named constructors. Constructor taking the Supplier is also named for consistency. - */ - private final Supplier supplier; - private ReactiveStateMachine(ReactiveValue initial, Supplier supplier) { - Objects.requireNonNull(initial); - Objects.requireNonNull(supplier); - OwnerTrace.of(this).alias("statemachine"); - this.supplier = supplier; - output = OwnerTrace - .of(new ReactiveVariable<>(initial) - /* - * Disable equality checking in the variable. - * Some uses of this class (e.g. ReactiveWorker) need to inspect every output even if it is equal. - * This is also a safe choice for performance as equality checks can be slow. - * - * We could expose equality configuration API in the future, - * but it is unlikely to be useful for the relatively low-level tasks this class is used for. - */ - .equality(false)) - .parent(this) - .tag("role", "output") - .target(); - } - public static ReactiveStateMachine supply(ReactiveValue initial, Supplier supplier) { - return new ReactiveStateMachine<>(initial, supplier); - } - public static ReactiveStateMachine supply(Supplier supplier) { - /* - * We don't know whether null is a reasonable fallback. Throwing is always correct (although not very efficient). - * Constructing exceptions is expensive, but we will favor useful stack trace over fast preallocated exception object. - */ - return supply(new ReactiveValue<>(new ReactiveBlockingException(), true), supplier); - } - public static ReactiveStateMachine run(ReactiveValue initial, Runnable runnable) { - Objects.requireNonNull(runnable); - return supply(initial, () -> { - runnable.run(); - return null; - }); - } - public static ReactiveStateMachine run(Runnable runnable) { - return run(new ReactiveValue<>(new ReactiveBlockingException(), true), runnable); - } - /* - * Reactive state machine is implemented using reactive primitives: scope, trigger, and pins. - */ - private ReactiveTrigger trigger; - private ReactivePins pins; - /* - * After the application detects that the current state is no longer valid, it is expected to trigger the next iteration. - * Application can do this on its own schedule or possibly never. - * - * We have to lock out other threads to avoid concurrent advancement that would corrupt the state machine. - * We can afford to synchronize on the whole state machine for a possibly long time, - * because reads from reactive variables are unsynchronized (since reactive variable itself is synchronized) - * and invalidation callback never executes concurrently with full advancement. - */ - @SuppressWarnings("resource") - public synchronized void advance() { - /* - * Do not advance the state machine if it is still valid. This is a convenience to application code - * that can now try to advance the state machine redundantly without it getting costly. - * - * Here we have to be careful. We cannot just read valid() flag as that would create reactive dependency - * that would be invalidated a few lines below when the valid() flag is set to true. - * Such immediate invalidation would force another controlling (outer) reactive computation to run immediately after the current one ends. - * While such overhead is common in reactive code and it is usually acceptable, it can be very wasteful here in some important use cases, - * for example in ReactiveLazy where it would double compute cost of all reactive computations that read new/changed ReactiveLazy. - * - * We cannot just check for non-null trigger either as that would make the check completely non-reactive. - * The controlling (outer) computation wouldn't run again when the state is invalidated and advancement would stop forever. - * - * Instead of creating dependency on the full valid() flag, we will reactively depend only on trigger state. - * The difference is that trigger state only tracks validity of the last controlled (inner) computation - * while the valid() flag tracks all current and future state of the whole reactive state machine. - * Trigger state can change only in one direction from not fired to fired while valid() changes both ways. - * This reduction in the scope of the dependency is sufficient to avoid redundant reactive computations - * while keeping the dependency wide enough to ensure the state machine appears to be fully reactive. - * - * Trigger itself is of course non-reactive, because it is a low-level reactive primitive. - * So how do we depend on its state? We will read valid() flag but only after we have already set it to true. - * This breaks the basic principle of reactive programming that dependencies are recorded before reads, - * but it is safe here, because it is equivalent to a scenario, in which another thread advances the state machine - * and the current thread executed shortly afterwards, reads the valid() flag (which is true), and returns without advancing. - * - * This solution is so efficient that a lot of code can just blindly advance all the time without ever checking valid(). - * This may be actually more performant thanks to the reduced dependency optimization. - */ - if (trigger != null) { - /* - * If the trigger is non-null, then valid() is true and we can just return without advancing. - * We will record dependency on valid() to ensure that the controlling (outer) computation - * tries to advance the state machine again when valid() becomes false. - */ - valid.get(); - return; - } - ReactiveScope scope = OwnerTrace.of(new ReactiveScope()) - .parent(this) - .target(); - if (pins != null) - scope.pins(pins); - pins = null; - try (CloseableScope computation = scope.enter()) { - ReactiveValue value = ReactiveValue.capture(supplier); - /* - * We will be sending two invalidations to the controlling reactive computation. - * We will first discourage redundant advancement by setting valid() to true. - * Only then we set the output. This prevents unnecessary attempts to advance the state machine. - */ - valid.set(true); - output.value(value); - } - /* - * As mentioned above, we will create dependency on valid() to ensure the controlling (outer) computation - * tries to advance again when the current state is invalidated, i.e. when valid() is set to false. - * We have to do this after setting valid() to true above to avoid immediately invalidating current computation. - * We also have to do it before arming the trigger, because trigger could fire immediately (inline) - * and such firing involves setting valid() to false, by which time the dependency on valid() must already exist. - * We have to be additionally careful not to create the dependency inside the controlled (inner) computation. - */ - valid.get(); - if (scope.blocked()) - pins = scope.pins(); - trigger = OwnerTrace - .of(new ReactiveTrigger() - .callback(this::invalidate)) - .parent(this) - .target(); - /* - * Arming the trigger can cause it to fire immediately. - * We don't worry about that, because our invalidation callback is very fast and non-conflicting. - */ - trigger.arm(scope.versions()); - } - private synchronized void invalidate() { - if (trigger != null) { - trigger.close(); - trigger = null; - valid.set(false); - } - } - @Override - public String toString() { - return OwnerTrace.of(this) + " = " + output.value(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactiveThread.java b/src/main/java/com/machinezoo/hookless/ReactiveThread.java deleted file mode 100644 index 598b206..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactiveThread.java +++ /dev/null @@ -1,314 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import java.util.*; -import java.util.concurrent.*; -import java.util.function.*; -import org.slf4j.*; -import com.machinezoo.closeablescope.*; -import com.machinezoo.hookless.util.*; -import com.machinezoo.hookless.utils.*; -import com.machinezoo.noexception.slf4j.*; -import com.machinezoo.stagean.*; -import io.micrometer.core.instrument.*; -import io.micrometer.core.instrument.Timer; - -/* - * Java's Thread cannot have direct reactive wrapper, because its Runnable is by nature blocking. - * Reactive thread is instead only a conceptual equivalent of Java Thread. - * It takes a Runnable that has no return value and instead acts through side-effects of its execution. - * While Java Thread runs a blocking Runnable once, reactive thread runs Runnable whenever its dependencies change. - * - * This difference in behavior unfortunately leads to the problem of how to terminate the reactive thread. - * Java Thread terminates simply when its Runnable terminates, but that's not an option for reactive threads. - * Reactive thread certainly terminates when non-blocking exception is caught, but exceptions cannot be used for regular termination. - * Reactive thread always keeps running when the Runnable reactively blocks, but we cannot require thread bodies to block forever. - * We will instead keep calling the Runnable until stop() is called. Threads can self-terminate by calling current() and then stop(). - * - * API details are kept consistent with the other reactive classes rather than with Java's Thread. - * That's why method names are slightly different and some methods are fluent. - */ -/** - * Reactive substitute for Java's {@link Thread}. - */ -@StubDocs -public class ReactiveThread { - /* - * The thread can be in three states: initialized, running, and stopped. Initialized state allows changes to configuration. - */ - private boolean started; - private boolean stopped; - private void ensureNotStarted() { - if (started) - throw new IllegalStateException(); - } - private Runnable runnable; - /* - * This method has no equivalent in Java Thread, but our fluent API works better with it. - */ - public synchronized ReactiveThread runnable(Runnable runnable) { - Objects.requireNonNull(runnable); - ensureNotStarted(); - this.runnable = runnable; - return this; - } - public synchronized Runnable runnable() { - return runnable; - } - public ReactiveThread() { - OwnerTrace.of(this).alias("thread"); - runnable = () -> {}; - } - public ReactiveThread(Runnable runnable) { - /* - * Make sure OwnerTrace is set up. - */ - this(); - Objects.requireNonNull(runnable); - this.runnable = runnable; - } - protected void run() { - runnable.run(); - } - /* - * We will mirror Java Thread's exception handlers. We cannot use Java's exception handler interface, - * because we need different type for the first parameter. We will use BiConsumer instead of specialized type - * in order to keep the API simple and minimal. - * - * Global handler is volatile to avoid excessive synchronization. - */ - private static final Logger logger = LoggerFactory.getLogger(ReactiveThread.class); - private static volatile BiConsumer handlerDefault = (t, ex) -> logger.error("Unhandled exception in reactive thread.", ex); - public static void handlerDefault(BiConsumer handler) { - Objects.requireNonNull(handler); - handlerDefault = handler; - } - public static BiConsumer handlerDefault() { - return handlerDefault; - } - private BiConsumer handler = (t, ex) -> handlerDefault.accept(t, ex); - public synchronized ReactiveThread handler(BiConsumer handler) { - Objects.requireNonNull(handler); - ensureNotStarted(); - this.handler = handler; - return this; - } - public synchronized BiConsumer handler() { - return handler; - } - /* - * Since reactive thread is not really a thread but rather a fiber, it needs an actual thread to run on. - * We allow configuration of executor, so that heavy reactive threads can be kept off the main reactive executor. - */ - private Executor executor = ReactiveExecutor.common(); - public synchronized ReactiveThread executor(Executor executor) { - Objects.requireNonNull(executor); - ensureNotStarted(); - this.executor = executor; - return this; - } - public synchronized Executor executor() { - return executor; - } - /* - * While most reactive objects are garbage-collected automatically, GCing running reactive threads would be counterintuitive. - * We will therefore keep all running reactive threads reachable even if the application doesn't bother to keep a reference to them. - * This is consistent with behavior of Java Thread except that all reactive threads are GCed when this class is unloaded. - * There are however times when the application needs to create object-local reactive threads that should be GCed with the object. - * Reactive thread can be optionally configured in "daemon" mode that allows garbage collection of running reactive threads. - */ - private static final Set running = - Metrics.gaugeCollectionSize("hookless.thread.running", Collections.emptyList(), ConcurrentHashMap.newKeySet()); - private boolean daemon; - public synchronized ReactiveThread daemon(boolean daemon) { - ensureNotStarted(); - this.daemon = daemon; - return this; - } - public synchronized boolean daemon() { - return daemon; - } - /* - * Java Thread has several methods for monitoring thread state. We can create their reactive equivalents. - * TODO: Add monitoring methods when there's time and need. Use reactive variable, possibly lazily initialized. - * - * Supportable state monitoring methods (all reactive): - * - getState() - * - isAlive() - * - join() - reactive blocking - * - join(long) - timeout relative to first such call to avoid cascading timeouts - * - join(long, int) - * - * Supportable thread states: - * - NEW - * - RUNNABLE - when new iteration is scheduled or already running - * - BLOCKED - when waiting on reactive trigger and last value is blocking - * - WAITING - when waiting on reactive trigger and last value is not blocking - * - TIMED_WAITING - not applicable to reactive threads - * - TERMINATED - * - * It is tempting to provide this functionality using futures (completable or reactive), - * but that is hopelessly buggy for daemon reactive threads (that might leave futures uncompleted when GCed), - * unnecessarily expands features beyond Java's Thread, and it duplicates functionality from reactive futures. - * - * We will not create equivalents of thread enumeration methods from Java Thread, but we will offer current() method, - * which is indispensable, because it allows calling stop() on the current thread without holding a reference to it. - */ - private static final ThreadLocal current = new ThreadLocal<>(); - public static ReactiveThread current() { - return current.get(); - } - /* - * Reactive thread is implemented using reactive scope and trigger with support for pinning. - */ - private ReactiveTrigger trigger; - private ReactivePins pins; - /* - * Timer sample is kept on instance level, so that we can also capture latency, - * i.e. how long it took for the thread's runnable to be scheduled for execution - * as well as contribution of any blocking computations. - */ - private Timer.Sample sample; - private static final Timer timer = Metrics.timer("hookless.thread.computations"); - /* - * Suppress resource warnings caused by closeable trigger not being closed after being constructed. - */ - @SuppressWarnings("resource") - private void iterate() { - ReactiveScope scope; - synchronized (this) { - /* - * In case stop() was called while we were waiting in executor queue. - */ - if (stopped) - return; - scope = OwnerTrace.of(new ReactiveScope()) - .parent(this) - .target(); - if (pins != null) - scope.pins(pins); - pins = null; - } - Throwable exception = null; - try (CloseableScope computation = scope.enter()) { - try { - try { - current.set(this); - run(); - } finally { - current.remove(); - } - } catch (Throwable ex) { - exception = ex; - } - } - /* - * Silently ignore exceptions when the reactive computation is blocking, because blocking exceptions are normal. - */ - if (exception != null && !scope.blocked()) { - /* - * Non-blocking exception has the same effect as calling stop() except that uncaught exception handler is called as well. - */ - stop(); - /* - * Run the handler outside of the synchronized block, because it could be an expensive operation. - */ - ExceptionLogging.log(logger).fromBiConsumer(handler).accept(this, exception); - } - synchronized (this) { - /* - * In case stop() was called while the runnable was running. Or in case non-blocking exception was thrown. - */ - if (stopped) - return; - if (scope.blocked()) - pins = scope.pins(); - /* - * Include all prior blocking computations in total latency. - */ - if (sample != null && !scope.blocked()) { - sample.stop(timer); - sample = null; - } - trigger = OwnerTrace - .of(new ReactiveTrigger() - .callback(this::invalidate)) - .parent(this) - .target(); - /* - * Normally we would arm outside of the synchronized block, but we have to watch out for concurrent stop(). - * Our invalidation callback might run during arm() call, but it doesn't do anything unsafe. - */ - trigger.arm(scope.versions()); - } - } - @DraftCode("handle RejectedExecutionException") - private void schedule() { - /* - * Include scheduling latency in execution time. Latency is what we care about in UIs. - * Do not overwrite existing timer sample though, because blocking computations should be included in thread's latency. - * This method is always called in synchronized context, so the comparison is safe. - */ - if (sample == null) - sample = Timer.start(Clock.SYSTEM); - /* - * Method iterate() should never throw, but let's make sure. - * Use weak Runnable to allow GCing of reactive threads that are only referenced from thread pool queue. - */ - executor.execute(ExceptionLogging.log(logger).runnable(new WeakRunnable<>(this, ReactiveThread::iterate))); - } - private synchronized void invalidate() { - /* - * We could receive invalidation callback after stop() was called. - * In that case the trigger was already destroyed and we have nothing to do here. - */ - if (stopped) - return; - trigger.close(); - schedule(); - } - public synchronized ReactiveThread start() { - /* - * It is allowed to start the thread twice. The second call has no effect. - */ - if (started) - return this; - started = true; - /* - * It is allowed to stop the thread before it is started. In that case we don't run even a single iteration. - */ - if (stopped) - return this; - if (!daemon) - running.add(this); - schedule(); - return this; - } - public synchronized void stop() { - /* - * All of the code below has to tolerate double stop() calls. - */ - stopped = true; - running.remove(this); - /* - * Eagerly clean up all the reactive resources. - * This is useful when the reactive thread object lingers in the object graph after it is closed. - */ - if (trigger != null) { - trigger.close(); - trigger = null; - } - if (sample != null) { - /* - * Record the last computation in the timer. For some threads, it might be the only computation. - */ - sample.stop(timer); - sample = null; - } - pins = null; - } - @Override - public String toString() { - return OwnerTrace.of(this).toString(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactiveTrigger.java b/src/main/java/com/machinezoo/hookless/ReactiveTrigger.java deleted file mode 100644 index 212d86f..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactiveTrigger.java +++ /dev/null @@ -1,233 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import java.util.*; -import java.util.concurrent.*; -import org.slf4j.*; -import com.machinezoo.hookless.util.*; -import com.machinezoo.noexception.slf4j.*; -import com.machinezoo.stagean.*; -import io.opentracing.*; -import io.opentracing.util.*; - -/* - * Reactive trigger is a low-level reactive primitive that is usually hidden inside higher-level reactive constructs. - * It is the only way to get change notifications from reactive variables, but it is easy to set it up - * to monitor single variable. Custom callbacks for variable changes thus aren't lost. - * It is also used to create callbacks for all high-level reactive constructs, which can be done - * by reading their state inside reactive scope and arming a trigger with variables collected by the scope. - * This is why no other reactive construct offers any callbacks. - * - * Trigger has many advantages over plain callbacks: - * - monitoring multiple variables - * - protection from race rules between version read and subscription to changes - * - protection from accidental garbage collection due to weak references in variables - * - choice between callback and state polling (armed(), fired(), closed()) - * - explicit forced firing even if no variable changes - * - * The standard sequence of calls is: arm(), fire(), close(). - * Some calls can be skipped and the trigger will still behave reasonably. - * Methods fire() and close() may be called repeatedly. - * - * Trigger implements AutoCloseable, so that it can be used in try-with-resources, - * although that's usually only useful in unit tests. - */ -/** - * Callback for changes in {@link ReactiveVariable}s. - */ -@StubDocs -public class ReactiveTrigger implements AutoCloseable { - public ReactiveTrigger() { - OwnerTrace.of(this).alias("trigger"); - } - /* - * We will keep a list of reactive variables we have subscribed to, so that we can unsubscribe from them. - * Null variable list in combination with 'armed' flag also indicates that subscription is still in progress. - * - * Variable list is implemented as a plain array to save a little memory since triggers stay around for a long time. - */ - private ReactiveVariable[] variables; - /* - * Arming is separate from constructor, so that callback and tags can be set first. - * The version list usually comes straight from reactive scope, - * but we don't reference scope anywhere, so that reactive trigger can be also used without it. - * - * Arming detects outdated versions and it might call fire() immediately. - * Callers must ensure they are prepared to receive callback when they call arm(), - * which usually means that arm() is the last step they are taking before waiting for the callback. - */ - private boolean armed; - public synchronized boolean armed() { - return armed; - } - public void arm(Collection versions) { - Objects.requireNonNull(versions); - synchronized (this) { - /* - * Contrary to fire() and close() calls, we put some constraints on when arm() can be called, - * because misplaced arm() call is a sign of a bug. - */ - if (armed) - throw new IllegalStateException("Cannot arm the trigger twice."); - if (closed) - throw new IllegalStateException("Trigger was already closed."); - armed = true; - } - /* - * Subscription runs unsynchronized, because it could take some time and we might need to fire() during it. - */ - List> subscribed = new ArrayList<>(); - for (ReactiveVariable.Version version : versions) { - version.variable().subscribe(this); - subscribed.add(version.variable()); - /* - * If the variable has already changed, fire immediately. - * This check must be done only after subscription to avoid race rules. - */ - if (version.number() != version.variable().version()) { - fire(); - break; - } - } - ReactiveVariable[] compact = subscribed.toArray(new ReactiveVariable[subscribed.size()]); - ReactiveVariable[] unsubscribed = null; - synchronized (this) { - /* - * We have to immediately unsubscribe all variables if the trigger was closed meantime. - * If we just fired without closing, we keep the variables until close() is called. - */ - if (closed) - unsubscribed = compact; - else - variables = compact; - } - /* - * Unsubscription runs unsynchronized, because it could take some time. - */ - if (unsubscribed != null) - unsubscribe(unsubscribed); - } - /* - * Reactive variables keep a set of all subscribed triggers. - * Lookups in this set are sped up a bit by precomputing hashCode(). - */ - private final int hashCode = ThreadLocalRandom.current().nextInt(); - @Override - public int hashCode() { - return hashCode; - } - /* - * Callers can configure callback to be run when fire() is called. - * If left null, firing can be still detected by polling fired() method. - * - * The callback has to run close() as otherwise the trigger would stay subscribed to variables until GC deletes it. - * Not calling close() from callback also risks premature GC of the trigger, causing the callback to never run. - */ - private Runnable callback; - public synchronized Runnable callback() { - return callback; - } - public synchronized ReactiveTrigger callback(Runnable callback) { - this.callback = callback; - return this; - } - private static final Logger logger = LoggerFactory.getLogger(ReactiveTrigger.class); - /* - * Method fire() is usually called by reactive variables when they change, - * but we also allow manual firing and fire() may be also called early when arm() detects outdated version. - */ - private boolean fired; - public synchronized boolean fired() { - return fired; - } - public void fire() { - Runnable callback = null; - synchronized (this) { - /* - * Do not invoke the callback if the trigger was already closed. - * Closing the trigger indicates that the owner is no longer interested in the callback. - * We however avoid throwing an exception, because fire() could be called by changed variables - * that are unaware of trigger closing and even manual calls to fire() may come from other threads - * that don't necessarily know that some other thread has already closed the trigger. - */ - if (!fired && !closed) { - fired = true; - callback = this.callback; - } - } - /* - * Callback must be invoked outside of the synchronized block, because it could take a long time. - */ - if (callback != null) { - /* - * Create new tracing span whenever invoking a callback. - * Reactive trigger usually inherits tags from all ancestors, - * so this trace span will describe what was invalidated by recent variable change. - */ - Span span = GlobalTracer.get().buildSpan("hookless.fire") - .withTag("component", "hookless") - .start(); - OwnerTrace.of(this).fill(span); - try (Scope trace = GlobalTracer.get().activateSpan(span)) { - /* - * Don't let exceptions escape from callbacks. Callbacks are supposed to handle their own exceptions. - * If they fail to do so, we will just catch and log the exception. - */ - ExceptionLogging.log(logger).run(callback); - } - } - } - /* - * We require users of reactive trigger to explicitly close() it. - * Theoretically, that's not necessary, because the calling code can just set the callback and forget the trigger. - * The problem is that with no reference to the trigger and only weak backreferences from variables, - * the trigger can be garbage-collected early, which will automatically remove it from reactive variables, - * and the callback will then never run. This can lead to surprising unreproducible bugs that are hard to fix. - * - * If callers are required to close() the trigger after use, then they have to hold a reference to it. - * Furthermore, since close() synchronizes on the trigger, the trigger is required to exist until close() is called. - * Without synchronization, GC would be free to collect it if trigger's fields are not used or they are cached in registers/stack. - * We could also use Java 9's Reference.reachabilityFence​(), but locking appears to be sufficient. - * - * Reactive trigger implements Closeable in order to make close() easier to use in tests and for other method-local use cases. - */ - private boolean closed; - public synchronized boolean closed() { - return closed; - } - @Override - public void close() { - ReactiveVariable[] unsubscribed = null; - synchronized (this) { - /* - * Tolerate multiple close() calls. This can happen when cleanup is done "just in case". - */ - if (!closed) { - closed = true; - /* - * If the trigger wasn't closed yet and variable list is null, - * it means that either arm() is in progress or that it was never called. - * If it was never called, then merely setting the 'closed' flag will prevent future arm() calls. - * If it is in progress, then setting the 'closed' flag will inform it that it has to unsubscribe when finished. - */ - if (variables != null) { - unsubscribed = variables; - variables = null; - } - } - } - /* - * Unsubscription runs unsynchronized, because it could take some time. - */ - if (unsubscribed != null) - unsubscribe(unsubscribed); - } - private void unsubscribe(ReactiveVariable[] unsubscribed) { - for (ReactiveVariable variable : unsubscribed) - variable.unsubscribe(this); - } - @Override - public String toString() { - return OwnerTrace.of(this).toString(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactiveValue.java b/src/main/java/com/machinezoo/hookless/ReactiveValue.java deleted file mode 100644 index 47b232e..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactiveValue.java +++ /dev/null @@ -1,374 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import java.io.*; -import java.util.*; -import java.util.concurrent.*; -import java.util.function.*; -import com.machinezoo.closeablescope.*; -import com.machinezoo.stagean.*; - -/* - * This is the equivalent of CompletableFuture for reactive programming: - * - CompletableFuture.get() = ReactiveValue.get() - * - CompletableFuture.getNow() = ReactiveValue.result() - * - CompletableFuture.isDone() = !ReactiveValue.blocking() - * Better not say that in the javadoc as it might be more confusing than enlightening. - */ -/** - * Container for output of reactive computation consisting of return value, exception, - * and reactive blocking flag. - * {@code ReactiveValue} can be split into its constituent components by calling - * {@link #result()}, {@link #exception()}, and {@link #blocking()}. - * It can be recreated from these components by calling {@link #ReactiveValue(Object, Throwable, boolean)} - * or some other constructor. {@code ReactiveValue} is immutable. - *

- * Reactive code usually takes the form of a method and communicates its output like a method, - * i.e. via return value or an exception. Reactive code may additionally signal - * reactive blocking - * by calling {@link CurrentReactiveScope#block()}. - * Return value, exception, and signaling of reactive blocking constitutes implicit output of reactive computation. - * {@code ReactiveValue} offers an explicit representation of the same. - * Conversion between explicit and implicit representations is performed - * by methods {@link #get()} and {@link #capture(Supplier)}. - *

- * {@code ReactiveValue} does not carry reactive dependencies. Use {@link ReactiveScope} for that. - * - * @param - * type of the result carried by this {@code ReactiveValue} - * - * @see ReactiveVariable - * @see ReactiveScope - */ -@DraftDocs("link to reactive value/output articles") -public class ReactiveValue { - private final T result; - /** - * Gets the return value component of this {@code ReactiveValue}. - * Only one of {@link #result()} and {@link #exception()} can be non-{@code null}. - * - * @return return value component of {@code ReactiveValue} - * - * @see #exception() - * @see #get() - */ - public T result() { - return result; - } - private final Throwable exception; - /** - * Gets the exception component of this {@code ReactiveValue}. - * Only one of {@link #result()} and {@link #exception()} can be non-{@code null}. - * - * @return exception component of {@code ReactiveValue} - * - * @see #result() - * @see #get() - */ - public Throwable exception() { - return exception; - } - private final boolean blocking; - /** - * Gets the reactive blocking flag from this {@code ReactiveValue}. - * Blocking flag is set if this {@code ReactiveValue} represents output of reactive computation - * that signaled blocking during its execution by calling {@link CurrentReactiveScope#block()}. - * - * @return {@code true} if this {@code ReactiveValue} represents output of blocking reactive computation, {@code false} otherwise - * - * @see #get() - * @see Reactive blocking - */ - public boolean blocking() { - return blocking; - } - /* - * We can either tolerate non-null result combined with non-null exception or throw. - * We decide to throw, because this is unlikely to be accidental. It is nearly always indicative of a bug. - */ - /** - * Constructs new {@code ReactiveValue} from return value, exception, and reactive blocking flag. - * Only one of {@code result} and {@code exception} can be non-{@code null}. - * The parameters can be later retrieved via {@link #result()}, {@link #exception()}, and {@link #blocking()}. - * - * @param result - * component representing return value of reactive computation that can be later retrieved via {@link #result()} - * @param exception - * component representing exception thrown by reactive computation that can be later retrieved via {@link #exception()} - * @param blocking - * {@code true} if the constructed {@code ReactiveValue} should represent - * blocking reactive computation, {@code false} otherwise - * @throws IllegalArgumentException - * if both {@code result} and {@code exception} are non-{@code null} - * - * @see #capture(Supplier) - */ - public ReactiveValue(T result, Throwable exception, boolean blocking) { - if (result != null && exception != null) - throw new IllegalArgumentException("Cannot set both the result and the exception."); - this.result = result; - this.exception = exception; - this.blocking = blocking; - } - /* - * If necessary, reactive value can be unpacked into current reactive scope by calling get(). - * It is therefore a bridge between reactive and non-reactive world. - */ - /** - * Unpacks explicit reactive output represented by this {@code ReactiveValue} into implicit reactive output. - * If {@link #exception()} is not {@code null}, it is thrown wrapped in {@link CompletionException}. Otherwise {@link #result()} is returned. - * In either case, if {@link #blocking()} is {@code true}, {@link CurrentReactiveScope#block()} is called. - * - * @return value of {@link #result()} - * @throws CompletionException - * if {@link #exception()} is not {@code null} - * - * @see #result() - * @see #exception() - * @see #blocking() - */ - public T get() { - if (blocking) - CurrentReactiveScope.block(); - if (exception != null) - throw new CompletionException(exception); - return result; - } - /* - * We also provide the opposite operation. Reactive value can capture value or exception and blocking flag. - * - * We provide only capture from Supplier, because there is usually some value to capture. - * Operations without result can have their exception and blocking captured by using Void result type. - */ - /** - * Captures implicit reactive output of provided {@link Supplier} and returns it encapsulated in new {@code ReactiveValue}. - * If the {@code supplier} throws, returned {@code ReactiveValue} will have {@link #exception()} set to the caught exception. - * Otherwise the {@code ReactiveValue} will have {@link #result()} set to value returned from the {@code supplier}. - *

- * If the {@code supplier} reactively blocks by calling {@link CurrentReactiveScope#block()}, - * {@link #blocking()} flag will be set on the returned {@code ReactiveValue}. - * This method obtains blocking flag by calling {@link CurrentReactiveScope#blocked()} after calling the {@code supplier}, - * which means the returned {@code ReactiveValue} will have {@link #blocking()} flag set also - * if the current {@link ReactiveScope} was already blocked by the time this method was called. - * This is reasonable behavior, because the {@code supplier} might be itself derived from information - * produced by blocking operations executed earlier during the current reactive computation, - * which means that {@code supplier}'s output cannot be trusted to be non-blocking. - *

- * If there is no current {@link ReactiveScope}, i.e. {@link ReactiveScope#current()} returns {@code null}, - * this method creates temporary {@link ReactiveScope} and executes the {@code supplier} in it, - * so that blocking flag can be captured. This is particularly useful in unit tests. - * - * @param - * type of value returned by the {@code supplier} - * @param supplier - * reactive code to execute - * @return {@code ReactiveValue} encapsulating implicit reactive output of the {@code supplier} - * - * @see #ReactiveValue(Object, Throwable, boolean) - */ - public static ReactiveValue capture(Supplier supplier) { - if (ReactiveScope.current() != null) - return captureScoped(supplier); - else { - /* - * Some code, especially tests, runs without reactive scope but still needs to capture blocking flag. - * We will create temporary scope for such cases. Everything in the scope is discarded except the blocking flag. - */ - try (CloseableScope computation = new ReactiveScope().enter()) { - return captureScoped(supplier); - } - } - } - private static ReactiveValue captureScoped(Supplier supplier) { - try { - /* - * Due to java evaluation order, blocking is checked only after the supplier runs. - */ - return new ReactiveValue<>(supplier.get(), CurrentReactiveScope.blocked()); - } catch (Throwable ex) { - return new ReactiveValue<>(ex, CurrentReactiveScope.blocked()); - } - } - /* - * Equality testing is a difficult choice. Comparing the result objects may be too expensive. - * Exceptions normally cannot be compared. We have to examine the full stack trace in order to compare them, which is expensive. - * The other option is to compare exceptions by reference only, but that is inconsistent. - * But without equality comparisons, we would get too many invalidations everywhere. - * So we support equality here and let callers decide whether to use it. - */ - /** - * Compares this {@code ReactiveValue} to another object for equality. - * {@code ReactiveValue} can only equal another {@code ReactiveValue}. - * Two {@code ReactiveValue} instances are equal if their {@link #result()}, {@link #exception()}, and {@link #blocking()} flags are equal. - * Value equality is used for both {@link #result()} and {@link #exception()}. - * Two exceptions are equal when their stringified form (including stack trace and causes) compares equal. - *

- * Full value equality checking may be expensive or even undesirable. - * Use {@link #same(ReactiveValue)} to compute shallow reference equality. - * - * @param obj - * object to compare this {@code ReactiveValue} to or {@code null} - * @return {@code true} if the objects compare equal, {@code false} otherwise - * - * @see #same(ReactiveValue) - * @see #hashCode() - */ - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null || !(obj instanceof ReactiveValue)) - return false; - @SuppressWarnings("unchecked") ReactiveValue other = (ReactiveValue)obj; - /* - * Cheapest comparisons first. This speeds up comparisons with negative result. - */ - if (blocking != other.blocking) - return false; - if ((exception != null) != (other.exception != null)) - return false; - if ((result != null) != (other.result != null)) - return false; - return Objects.equals(result, other.result) && Objects.equals(dump(exception), dump(other.exception)); - } - /** - * Computes hash code of this {@code ReactiveValue}. - * Hash code is calculated in such a way that if two {@code ReactiveValue} instances are equal - * as checked by {@link #equals(Object)}, then their hash codes are equal too. - * This makes {@code ReactiveValue} usable as a key in a {@link Map}. - *

- * Both {@link #result()} and {@link #exception()} are included in hash code calculation. - * Exceptions are hashed in such a way that two exceptions with the same stringified form - * (including stack traces and causes) will have the same hash code. - * - * @return hash code of this {@code ReactiveValue} - * - * @see #equals(Object) - */ - @Override - public int hashCode() { - /* - * Reactive value is unlikely to be used as a hash key. We are free to make this inefficient. - */ - return Objects.hash(result, dump(exception), blocking); - } - /* - * Some reactive computations only use fast reference equality. - * This method does as much equality checking as possible without running any expensive operations. - */ - /** - * Checks reference equality between two {@code ReactiveValue} instances. - * Another {@code ReactiveValue} is reference-equal to this instance according to this method - * if it is not {@code null} and its {@link #result()}, {@link #exception()}, and {@link #blocking()} - * components are all reference-equal to corresponding components of this {@code ReactiveValue}. - *

- * This method is useful when {@link #equals(Object)} would be too expensive or where reference equality is desirable. - * - * @param other - * {@code ReactiveValue} to compare this instance to or {@code null} - * @return {@code true} if the two {@code ReactiveValue} instances are reference-equal, {@code false} otherwise - * - * @see #equals(Object) - */ - public boolean same(ReactiveValue other) { - return other != null && result == other.result && exception == other.exception && blocking == other.blocking; - } - /* - * There are more efficient ways to compare exceptions, but this crude solution will work for now. - * It has no impact on performance in case there is no exception. - * If there is an exception, nobody expects stellar performance. - */ - private static String dump(Throwable exception) { - if (exception == null) - return null; - StringWriter writer = new StringWriter(); - exception.printStackTrace(new PrintWriter(writer)); - return writer.toString(); - } - /** - * Returns a string representation of this {@code ReactiveValue}. - * The returned string is suitable for debug output and includes - * string representation of {@link #result()} or {@link #exception()} - * as well as indication whether the {@code ReactiveValue} is {@link #blocking()}. - * - * @return string representation of this {@code ReactiveValue} - */ - @Override - public String toString() { - return (exception == null ? Objects.toString(result) : exception.toString()) + (blocking ? " [blocking]" : ""); - } - /* - * Convenience constructors. - */ - /** - * Constructs new {@code ReactiveValue}. - * The new {@code ReactiveValue} represents reactive computation that successfully completed with {@code null} result - * and without blocking. - * The {@code ReactiveValue} will have {@code null} {@link #result()} and {@link #exception()} and {@code false} {@link #blocking()} flag. - * - * @see #ReactiveValue(Object, Throwable, boolean) - */ - public ReactiveValue() { - this(null, null, false); - } - /** - * Constructs new {@code ReactiveValue} from return value. - * The new {@code ReactiveValue} represents reactive computation that successfully completed - * without blocking. - * The {@code ReactiveValue} will have {@code null} {@link #exception()} and {@code false} {@link #blocking()} flag. - * - * @param result - * return value of the reactive computation the new {@code ReactiveValue} represents - * - * @see #ReactiveValue(Object, Throwable, boolean) - */ - public ReactiveValue(T result) { - this(result, null, false); - } - /** - * Constructs new {@code ReactiveValue} from exception. - * The new {@code ReactiveValue} represents reactive computation that threw an exception - * without blocking. - * The {@code ReactiveValue} will have {@code null} {@link #result()} and {@code false} {@link #blocking()} flag. - * - * @param exception - * exception thrown by the reactive computation the new {@code ReactiveValue} represents - * - * @see #ReactiveValue(Object, Throwable, boolean) - */ - public ReactiveValue(Throwable exception) { - this(null, exception, false); - } - /** - * Constructs new {@code ReactiveValue} from return value and blocking flag. - * The new {@code ReactiveValue} represents reactive computation that successfully completed and possibly signaled blocking. - * The {@code ReactiveValue} will have {@code null} {@link #exception()}. - * - * @param result - * return value of the reactive computation the new {@code ReactiveValue} represents - * @param blocking - * {@code true} if the constructed {@code ReactiveValue} should represent - * blocking reactive computation, {@code false} otherwise - * - * @see #ReactiveValue(Object, Throwable, boolean) - */ - public ReactiveValue(T result, boolean blocking) { - this(result, null, blocking); - } - /** - * Constructs new {@code ReactiveValue} from exception and blocking flag. - * The new {@code ReactiveValue} represents reactive computation that threw an exception and possibly signaled blocking. - * The {@code ReactiveValue} will have {@code null} {@link #result()}. - * - * @param exception - * exception thrown by the reactive computation the new {@code ReactiveValue} represents - * @param blocking - * {@code true} if the constructed {@code ReactiveValue} should represent - * blocking reactive computation, {@code false} otherwise - * - * @see #ReactiveValue(Object, Throwable, boolean) - */ - public ReactiveValue(Throwable exception, boolean blocking) { - this(null, exception, blocking); - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactiveVariable.java b/src/main/java/com/machinezoo/hookless/ReactiveVariable.java deleted file mode 100644 index 142241c..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactiveVariable.java +++ /dev/null @@ -1,567 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import java.util.*; -import java.util.concurrent.*; -import com.machinezoo.hookless.util.*; -import com.machinezoo.stagean.*; -import io.opentracing.*; -import io.opentracing.util.*; - -/** - * Reactive data source holding single {@link ReactiveValue}. - * Changes can be observed by using {@link ReactiveTrigger} directly or - * by implementing reactive computation using one of the higher level APIs, for example {@link ReactiveThread}. - *

- * {@link ReactiveVariable} is also used as a bridge between event-driven code and hookless-based code. - * Event-driven code can instantiate {@code ReactiveVariable} and call {@code set(new Object())} - * on it whenever it wants to wake up dependent reactive computations. - *

- * {@link ReactiveVariable} is thus a universal reactive primitive rather than just one kind of reactive data source. - * It is referenced directly in {@link ReactiveScope} and {@link ReactiveTrigger}. - * All other reactive data sources internally use {@code ReactiveVariable} either to directly store state or to trigger invalidations. - *

- * {@link ReactiveVariable} is thread-safe. All methods are safe to call concurrently from multiple threads. - * - * @param - * type of the stored value - * - * @see ReactiveTrigger - * @see ReactiveCollections - */ -@DraftDocs("link to docs for reactive data sources, reactive computation") -public class ReactiveVariable { - /* - * There is no perfect solution for equality testing, so we resort to configuration. - * - * Case for full equality (via Object.equals): - * A lot of code does trivial writes like overwriting boolean flag with the same value. - * This would cause lots of unnecessary invalidations, making it hard for reactive computations to settle. - * Full equality testing prevents unnecessary invalidations. - * It is also likely to be the less surprising and the more useful option, which is why we make it the default. - * - * Case for reference equality (via == operator): - * Full equality testing can be computationally expensive. It can cause surprising performance problems. - * Reference equality might be also expected for something that is called "variable". - * Since there are sufficiently many situations where it could be useful, we expose it as a configurable option. - * - * Other options (not offered): - * - no equality testing: unnecessarily wasteful, reference equality is already cheap enough - */ - private volatile boolean equality = true; - /** - * Returns {@code true} if this {@link ReactiveVariable} performs full equality check on assignment. - * This method merely returns what was last passed to {@link #equality(boolean)}. Defaults to {@code true}. - * - * @return {@code true} if full equality check is enabled, {@code false} if reference equality is used - * - * @see #equality(boolean) - * @see ReactiveValue#equals(Object) - * @see ReactiveValue#same(ReactiveValue) - */ - public boolean equality() { - return equality; - } - /** - * Configures full or reference equality. - * When this {@link ReactiveVariable} is assigned, dependent reactive computations are notified about the change if there is any. - * In order to check whether the stored value has changed, {@link ReactiveVariable} can either perform - * full equality check via {@link ReactiveValue#equals(Object)} - * or simple reference equality check via {@link ReactiveValue#same(ReactiveValue)}. - * This method can be used to configure which equality test is used. Default is to do full equality check. - *

- * This method should be called before the {@link ReactiveVariable} is used for the first time. - * - * @param equality - * {@code true} to do full equality check, {@code false} to test only reference equality - * @return {@code this} (fluent method) - * - * @see #equality() - * @see ReactiveValue#equals(Object) - * @see ReactiveValue#same(ReactiveValue) - */ - public ReactiveVariable equality(boolean equality) { - this.equality = equality; - return this; - } - /* - * We start with version 1, so that we can use 0 as a special value elsewhere, especially in reactive scope's dependency map. - * - * The alternative solution, one used in past versions of hookless, is to have a version object. - * New version object would be created after every change. The upside is that version objects are easier to handle. - * The main downside of version objects is that they are not sorted, making it impossible to tell which version is earlier. - * Version numbers enable correct merging of multiple dependency sets, for example from parallel computations. - * Version numbers also look better in the debugger and other tools and they show how many times the variable changed. - * Version numbers might also improve performance of dependency tracking a tiny bit. - */ - private volatile long version = 1; - /** - * Returns current version of this {@link ReactiveVariable}. - * Every {@link ReactiveVariable} has an associated version number, which is incremented after every change. Initial version is 1. - * Version number does not change if there was no actual change as determined by {@link #equality()} setting. - * Reading the version number with this method does not create reactive dependency on the {@link ReactiveVariable}. - * - * @return current version of this {@link ReactiveVariable} - * - * @see Version - */ - public long version() { - return version; - } - /* - * The object representation of version is nevertheless still needed in various APIs. - * We will provide an inner class that represents either latest or any specified version. - */ - /** - * Reference to particular version of {@link ReactiveVariable}. - * Version of the {@link ReactiveVariable} is already exposed via {@link ReactiveVariable#version()}. - * This is just a convenience wrapper. - * It represents particular version of particular {@link ReactiveVariable}. - * - * @see ReactiveVariable#version() - */ - public static class Version { - private final ReactiveVariable variable; - /** - * Returns the {@link ReactiveVariable} this is a version of. - * This is the {@link ReactiveVariable} that was passed to the constructor. - * - * @return {@link ReactiveVariable} this object is a version of - */ - public ReactiveVariable variable() { - return variable; - } - private final long number; - /** - * Returns the version number of the version this object represents. - * This is the version number that was passed to the constructor - * or determined at construction time by calling {@link ReactiveVariable#version()}. - * - * @return version number of the version this object represents - * - * @see ReactiveVariable#version() - */ - public long number() { - return number; - } - /** - * Creates new {@link Version} object representing specified version of the {@link ReactiveVariable}. - * This constructor is useful in rare cases, - * for example when multiple versions have to be merged by taking minimum or maximum. - * Constructor {@link Version#Version(ReactiveVariable)} - * should be used when just capturing current version. - *

- * Return values of {@link ReactiveVariable#version()} are always positive. - * The {@code version} parameter of this constructor additionally allows special zero value. - * - * @param variable - * {@link ReactiveVariable} that will have its version tracked by this object - * @param version - * non-negative version number tracked by this object - * @throws NullPointerException - * if {@code variable} is {@code null} - * @throws IllegalArgumentException - * if {@code version} is negative - */ - public Version(ReactiveVariable variable, long version) { - Objects.requireNonNull(variable); - if (version < 0) - throw new IllegalArgumentException(); - this.variable = variable; - number = version; - } - /** - * Creates new {@link Version} object representing current version of {@link ReactiveVariable}. - * Current version is determined by calling {@link ReactiveVariable#version()}. - * Constructor {@link Version#Version(ReactiveVariable, long)} - * can be used to specify different version number. - * - * @param variable - * {@link ReactiveVariable} that will have its version tracked by this object - * @throws NullPointerException - * if {@code variable} is {@code null} - */ - public Version(ReactiveVariable variable) { - this(variable, variable.version); - } - /* - * Make version objects directly comparable. - */ - /** - * Compares this version with another {@link Version}. - * Two versions are equal if they the same {@link #variable()} and {@link #number()}. - * - * @param obj - * object to compare this version to - * @return {@code true} if {@code obj} represents the same version, {@code false} otherwise - */ - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null || !(obj instanceof Version)) - return false; - Version other = (Version)obj; - return variable() == other.variable() && number == other.number; - } - /** - * Calculates hash code of this version. - * Hash code incorporates {@link #variable()} identity and version {@link #number}. - * - * @return hash code of this {@link Version} - */ - @Override - public int hashCode() { - return Objects.hash(variable(), number); - } - /** - * Returns diagnostic string representation of this version. - * {@link ReactiveVariable} and its {@link ReactiveValue} is included in the result, - * but no reactive dependency is created by calling this method. - * - * @return string representation of this version - */ - @Override - public String toString() { - return "Version " + number + " of " + variable; - } - } - /* - * Triggers can subscribe to receive change notifications from the variable. - * We do not offer general subscription API, because it adds complexity. - * People can always just create new trigger and receive notifications through it. - * - * When trigger is fired, it is automatically unsubscribed. - * This is expected by trigger, which cannot be rearmed once triggered. - * But even if we allowed anyone to subscribe, we wouldn't want to send change notifications repeatedly, - * because dependent objects might be slow to react and we don't want to overwhelm them when changes happen rapidly. - * We instead wait for dependent objects to resubscribe (via new trigger) and only notify them of subsequent changes. - * - * Since triggers subscribe long after variable read, the variable might have changed meantime. - * That's why triggers must double-check version number after they subscribe to detect such changes. - * - * Triggers are smart enough not to subscribe themselves twice. We don't need the set for subscriptions. - * But triggers can unsubscribe in random order, which makes the set necessary for fast unsubscription. - * - * The set is weak, so that variables don't block garbage collection of triggers. - * In the hookless world, strong references only go in the direction from reactive consumers to reactive sources. - * Weak references are always used in the opposite direction. - * That means the trigger must be held alive by something. Subscription alone wouldn't protect it from GC. - * Reactive consumers must take care to keep reference to their trigger. - * That makes triggers hard to use, but event-driven programming is always hard. - * - * Assisticant and maybe other reactive frameworks offer an API that produces notifications - * when the list of listeners (triggers in hookless) becomes empty or non-empty. - * While this looks like a convenient way to deallocate native resources tied to a cache when no one uses it, - * it is inherently unsafe, because the trigger list can become empty through garbage collection of triggers - * rather than explicit removal and the empty list situation might not be detected. - * While we could use Java API's features to monitor garbage collection of weak references to compensate for this, - * we would still deliver empty trigger list notification long after the triggers stopped being used, - * because garbage collector can take a long time to get around to collecting unused (and thus unreferenced) triggers. - * Such late notification is nearly useless, so it's better to not offer the API at all - * and save ourselves some complexity and performance issues. - */ - private Set triggers = newTriggerSet(); - private static Set newTriggerSet() { - return Collections.newSetFromMap(new WeakHashMap()); - } - synchronized void subscribe(ReactiveTrigger result) { - Objects.requireNonNull(result); - triggers.add(result); - } - synchronized void unsubscribe(ReactiveTrigger result) { - triggers.remove(result); - } - /* - * Storing reactive value in the reactive variable has the advantage - * that complex reactive sources like caches can store exceptions and blocking flag in the variable. - * Reading the cache (or other source) can then be implemented as a read from reactive variable. - * This simplifies implementation of reactive sources. - * - * The value field is volatile in order to allow reads without the overhead of locking. - * - * The value field is never null. - */ - private volatile ReactiveValue value; - /** - * Reads the current {@link ReactiveValue} from this {@link ReactiveVariable} and sets up reactive dependency. - *

- * This {@link ReactiveVariable} is recorded as a dependency in current {@link ReactiveScope} - * (as identified by {@link ReactiveScope#current()}. - * If there is no current {@link ReactiveScope}, no dependency is recorded - * and this method just returns the {@link ReactiveValue}. - *

- * {@link ReactiveValue}'s {@link ReactiveValue#get()} is not called, - * so reactive blocking and exception, if any, are not propagated. - * They are left encapsulated in the returned {@link ReactiveValue}. - * Call {@link #get()} if propagation of reactive blocking and exceptions is desirable. - * - * @return current {@link ReactiveValue} of this {@link ReactiveVariable}, never {@code null} - * - * @see #get() - * @see #value(ReactiveValue) - */ - public ReactiveValue value() { - /* - * This is what makes the variable reactive. We let reactive scope track the variable as a dependency. - * - * The variable must be added to the scope before value field is read in order to avoid race rules, - * because we are running this method unsynchronized for performance reasons (relying on volatile flag) - * and there could be a concurrent write before the variable is tracked (and version read) and reading the value. - * If that happens, the tracked version will be old and trigger will detect it when it is armed. - */ - ReactiveScope current = ReactiveScope.current(); - if (current != null) - current.watch(this); - /* - * Unsynchronized field read relies on volatile flag on the field. - */ - return value; - } - /** - * Sets current {@link ReactiveValue} of this {@link ReactiveVariable} and notifies dependent reactive computations. - * This is the more general version of {@link #set(Object)} that allows setting arbitrary {@link ReactiveValue}. - *

- * The {@code value} may have {@link ReactiveValue#blocking()} flag set and it may contain {@link ReactiveValue#exception()}. - * This is useful in situations when {@link ReactiveVariable} is used as a bridge between hookless-based code and event-driven code. - * The event-driven code may signal that it is not yet ready by setting its {@link ReactiveVariable} - * to a blocking value. - * It may also communicate exceptions reactively by wrapping them in {@link ReactiveValue} and storing it in {@link ReactiveVariable}. - *

- * If current {@link ReactiveValue} is actually changed as determined by {@link #equality()} setting, - * {@link #version()} is incremented and dependent reactive computations - * (the ones that accessed {@link #value()} or {@link #get()}) are notified about the change. - * If there is no actual change (new value compares equal to the old value per {@link #equality()} setting), - * then the state of this {@link ReactiveVariable} does not change, old {@link ReactiveValue} is kept, - * {@link #version()} remains the same, and no change notifications are sent. - * - * @param value - * new {@link ReactiveValue} to store in this {@link ReactiveVariable} - * @throws NullPointerException - * if {@code value} is {@code null} - * - * @see #set(Object) - * @see #value() - */ - public void value(ReactiveValue value) { - Objects.requireNonNull(value); - /* - * Since full equality checking can be slow, we will perform it outside of any synchronized section to avoid blocking. - * - * While this makes the write behavior more complicated in case of concurrent writes, - * the behavior is still correct in the sense that one write wins. - * If all the concurrent writes compare unequal, one of them will win the subsequent modification of the variable. - * if all writes compare equal, no change will occur and that is correct behavior for all of the writes. - * If some writes compare equal and some unequal, then one of the unequal writes is allowed to win. - * - * Reads of the fields 'equality' and 'value' don't need to be synchronized, because the fields are volatile, - * but we have to make a copy of 'value' field, because we are going to access it several times. - */ - ReactiveValue previous = this.value; - if (!(equality ? previous.equals(value) : previous.same(value))) { - Set notified = null; - synchronized (this) { - /* - * It is important to avoid assigning new value when equality test is positive. - * Value change must happen only if there is corresponding version change. - * Otherwise consecutive reads from the variable could return different objects for the same version. - * This would cause numerous such objects to be cached in dependent caches for a long time, wasting memory. - * - * The worst case scenario is a 100MB value that is subsequently cached by thousands of dependent caches. - * If every one of those caches reads different (but equal) instance of the value, a terabyte of RAM could be wasted. - * Changing the value only when version changes ensures that all these caches hold reference to the same value. - */ - this.value = value; - ++version; - if (!triggers.isEmpty()) { - /* - * Completely replacing the set lets us fire triggers below without synchronization. - * It also resets set capacity to zero, so oversized trigger sets don't linger around. - */ - notified = triggers; - triggers = newTriggerSet(); - } - } - /* - * This is where reactivity happens. We will notify reactive triggers about the change in this variable. - * - * We are firing triggers immediately even though this write might be a part of a larger batch of changes. - * Older versions of hookless had the concept of "transactions" that would fire all triggers at the very end. - * This was intended to prevent invalidating the same cache twice with the same high-level write. - * It turns out this is not very useful. It doesn't improve throughput much, but it spreads complexity everywhere. - * We are now firing triggers without delay and rely on executor's FIFO processing to limit double invalidations. - * - * Fire triggers outside of the synchronized block. Triggers might run their callbacks inline, - * which might take a lot of time and these callbacks may perform writes back to the variable. - */ - if (notified != null) { - /* - * We don't want to trace every variable write, because tracing is expensive. - * We only enable it here when we are sure that at least trigger will fire. - * This is not a problem, because the tracing is intended primarily for callback graph anyway. - */ - Span span = GlobalTracer.get().buildSpan("hookless.change") - .withTag("component", "hookless") - .start(); - OwnerTrace.of(this).fill(span); - try (Scope trace = GlobalTracer.get().activateSpan(span)) { - for (ReactiveTrigger trigger : notified) { - /* - * Normally, we would wrap callbacks in Exceptions.log(), but calling reactive trigger is safe. - * It is our code and we know it wouldn't throw exceptions. - */ - trigger.fire(); - } - } - } - } - } - /* - * We are registering the variable in reactive scope before every read. - * The variable is recorded only once, but reactive scope has to check every time that the variable is already tracked. - * This check is implemented as a hash lookup. Providing fast hashCode() implementation speeds it up considerably. - */ - private final int hashCode = ThreadLocalRandom.current().nextInt(); - /** - * Returns identity hash code. - * This implementation is semantically identical to {@link Object#hashCode()}. - * It is just a little faster in order to speed up operations that use {@link ReactiveVariable} as a key in {@link Map}. - * - * @return identity hash code - */ - @Override - public int hashCode() { - return hashCode; - } - /* - * Methods set() and get() are packing/unpacking variants of corresponding value() methods above. - * These are the more commonly used ones and they thus get the nicer names. - * - * Method get() unpacks the reactive value into current reactive scope. - * This is what's normally expected when accessing reactive variable. - * - * Method set() wraps supplied object as reactive value. - * We are ignoring blocking flag in current reactive computation here, which means the packing is not complete. - * This is because setting blocking flag inside of a reactive variable might be surprising and result in subtle bugs. - * Most uses of set() intend to set non-blocking value. When blocking is intended, callers can use value(...) method instead. - */ - /** - * Returns the value held by this {@link ReactiveVariable}. - * In most situations, this is the more convenient alternative to {@link #value()}. - * It is equivalent to calling {@link #value()} and then calling {@link ReactiveValue#get()} on the returned {@link ReactiveValue}. - *

- * If the stored {@link ReactiveValue} has {@link ReactiveValue#blocking()} flag set, - * reactive blocking is propagated into current {@link ReactiveScope} if there is any. - * If the stored {@link ReactiveValue} holds an exception, the exception is propagated wrapped in {@link CompletionException}. - * Otherwise this method just returns {@link ReactiveValue#result()}. - *

- * This {@link ReactiveVariable} is recorded as a dependency in current {@link ReactiveScope} - * (as identified by {@link ReactiveScope#current()} if there is any. - * - * @throws CompletionException - * if the stored {@link ReactiveValue} holds an exception - * - * @return current value stored in this {@link ReactiveVariable} - * - * @see #value() - * @see ReactiveValue#get() - * @see #set(Object) - */ - public T get() { - return value().get(); - } - /** - * Sets current value of this {@link ReactiveVariable}. - * In most situations, this is the more convenient alternative to {@link #value(ReactiveValue)}. - * It is equivalent to wrapping {@code value} in {@link ReactiveValue} and passing it to {@link #value(ReactiveValue)}. - *

- * If current {@link ReactiveValue} is actually changed as determined by {@link #equality()} setting, - * {@link #version()} is incremented and dependent reactive computations - * (the ones that accessed {@link #value()} or {@link #get()}) are notified about the change. - * Note that change is detected if the old {@link ReactiveValue} has - * {@link ReactiveValue#blocking()} flag set or if it holds an exception ({@link ReactiveValue#exception()}). - * If there is no actual change (new {@link ReactiveValue} compares equal to the old {@link ReactiveValue} per {@link #equality()} setting), - * then the state of this {@link ReactiveVariable} does not change, old {@link ReactiveValue} is kept, - * {@link #version()} remains the same, and no change notifications are sent. - * - * @param value - * new value to store in this {@link ReactiveVariable} - * - * @see #value(ReactiveValue) - * @see ReactiveValue#ReactiveValue(Object) - * @see #get() - */ - public void set(T value) { - value(new ReactiveValue<>(value)); - } - /* - * We provide some convenience constructors. Besides convenience, they are also faster than writing the variable after construction. - */ - /** - * Creates new instance holding specified {@link ReactiveValue}. - * - * @param value - * initial value of the {@link ReactiveVariable} - * @throws NullPointerException - * if {@code value} is {@code null} - */ - public ReactiveVariable(ReactiveValue value) { - Objects.requireNonNull(value); - this.value = value; - OwnerTrace.of(this).alias("var"); - } - /** - * Creates new instance holding specified {@code value}. - * The {@link ReactiveValue} in the new {@link ReactiveVariable} will have {@code false} {@link ReactiveValue#blocking()} flag. - * - * @param value - * initial value of the {@link ReactiveVariable} - */ - public ReactiveVariable(T value) { - this(new ReactiveValue<>(value)); - } - /** - * Creates new instance with {@code null} value. - * The new {@link ReactiveVariable} will contain {@link ReactiveValue} with {@code null} {@link ReactiveValue#result()}. - */ - public ReactiveVariable() { - this(new ReactiveValue<>()); - } - @SuppressWarnings("unused") - private Object keepalive; - /** - * Adds strong reference to the specified target object. - * This is sometimes useful to control garbage collection. - * Only one target object is supported. If this method is called twice, the first target is discarded. - *

- * Hookless keeps strong references in the direction from reactive computations to their reactive dependencies - * and weak references in opposite direction. This usually results in expected garbage collector behavior. - *

- * However, when {@link ReactiveVariable} is embedded in a higher level reactive object, - * reactive computations hold strong references to the embedded {@link ReactiveVariable} - * instead of pointing to the outer reactive object, which makes the outer object vulnerable to premature collection. - * If the outer object is supposed to exist as long as its embedded {@link ReactiveVariable} is referenced, - * for example when it has {@link ReactiveTrigger} subscribed to changes in the {@link ReactiveVariable}, - * the outer object should call this method on its embedded {@link ReactiveVariable}, passing itself as the target object. - * This will ensure the outer object will live for as long as its embedded {@link ReactiveVariable}. - * - * @param keepalive - * target object that will be strongly referenced by this {@link ReactiveVariable} - * @return {@code this} (fluent method) - */ - public ReactiveVariable keepalive(Object keepalive) { - this.keepalive = keepalive; - return this; - } - /** - * Returns diagnostic string representation of this {@link ReactiveVariable}. - * Stored {@link ReactiveValue} is included in the result, - * but no reactive dependency is created by calling this method. - * - * @return string representation of this {@link ReactiveVariable} - */ - @Override - public String toString() { - return OwnerTrace.of(this) + " = " + value; - } -} diff --git a/src/main/java/com/machinezoo/hookless/ReactiveWorker.java b/src/main/java/com/machinezoo/hookless/ReactiveWorker.java deleted file mode 100644 index 010993d..0000000 --- a/src/main/java/com/machinezoo/hookless/ReactiveWorker.java +++ /dev/null @@ -1,289 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import java.util.*; -import java.util.concurrent.*; -import java.util.function.*; -import com.machinezoo.hookless.util.*; -import com.machinezoo.stagean.*; - -/* - * Reactive computation graph needs intermediate nodes that are both consumers and sources of reactivity. - * Intermediate nodes mostly serve as compute caches improving performance, a kind of reactive memoization. - * Reactive worker allows applications to easily define such intermediate nodes in the reactive computation graph. - * Contrary to the synchronous reactive lazy, asynchronousness of reactive workers allows them - * to behave like normal reactive computations and to shield dependents from invalidations that do not change the result. - * - * The worker metaphor comes from workers in many UI toolkits. These workers run on a thread contrary to synchronous memoization. - * Unlike non-reactive UI workers, reactive worker keeps updating the result whenever dependencies change. - * - * Reactive worker is similar to reactive thread, but it is optimized for value-returning reactive computations, - * which among other things allows it to pause the reactive computation when nobody reads the result. - * This similarity allows us to implement reactive worker as an instance of reactive thread. - */ -/** - * Single-value asynchronous cache for results of reactive computations. - * - * @param - * type of cached result - */ -@StubDocs -public class ReactiveWorker implements Supplier { - /* - * Worker has a configuration phase when its properties can be changed. It is started automatically on first access. - */ - private boolean started; - void ensureNotStarted() { - if (started) - throw new IllegalStateException(); - } - /* - * Just like in reactive threads, we give the application choice between override and lambda via constructor or setter. - */ - private Supplier supplier = () -> null; - public synchronized ReactiveWorker supplier(Supplier supplier) { - Objects.requireNonNull(supplier); - ensureNotStarted(); - this.supplier = supplier; - return this; - } - public synchronized Supplier supplier() { - return supplier; - } - public ReactiveWorker() { - OwnerTrace.of(this).alias("worker"); - } - public ReactiveWorker(Supplier supplier) { - /* - * Ensure OwnerTrace is set up. - */ - this(); - supplier(supplier); - } - protected T supply() { - return supplier.get(); - } - /* - * As an intermediate node in the reactive computation graph, we need reactive variable that will hold worker's output. - */ - private final ReactiveVariable output = OwnerTrace - /* - * We don't know whether null is a reasonable fallback. Blocking exception is always correct. - * Application can call initial() to change this. - */ - .of(new ReactiveVariable<>(new ReactiveValue(new ReactiveBlockingException(), true)) - /* - * Since the worker is based on "dameon" reactive thread, it would be garbage collected if it is not directly referenced. - * It can be however referenced indirectly by dependent computations waiting on the variable. - * Even though such dependent computations cannot retrieve new values since they don't have worker reference, - * they might still need to be informed about changes in their dependencies, which wouldn't happen if the worker is GC-ed. - * So link the variable to the worker to avoid premature garbage collection. - */ - .keepalive(this) - /* - * Disable equality in the variable, because we are already implementing it ourselves. - */ - .equality(false)) - .parent(this) - .tag("role", "output") - .target(); - /* - * Equality is configurable just like in reactive variable. - * We don't forward it to the reactive variable, because we have to do equality checks ourselves. - */ - private boolean equality = true; - public synchronized ReactiveWorker equality(boolean equality) { - ensureNotStarted(); - this.equality = equality; - return this; - } - public synchronized boolean equality() { - return equality; - } - /* - * Since blocking exception is the default, the only other common alternative is blocking fallback. - * If none of these two options work, application can set arbitrary initial reactive value. - */ - public synchronized ReactiveWorker initial(ReactiveValue value) { - ensureNotStarted(); - output.value(value); - return this; - } - public ReactiveWorker initial(T result) { - return initial(new ReactiveValue<>(result, true)); - } - /* - * Using reactive thread in daemon mode ensures that unused reactive workers will not waste memory. - * Unused worker can however still waste CPU until it is GC-ed, which might happen after a very long time. - * We cannot directly check whether the worker is used, because dependent computations themselves could be garbage. - * We will instead produce probing invalidations time to time that wake up dependent computations. - * Dependents are then expected to reread worker's output, which is something we can detect. - * If no dependent computation reads worker's output, we will leave the worker in paused state. - * We cannot destroy the worker yet, because we could later get a read, which causes the worker to resume activity. - * - * Now the question is what does "time to time" mean. Invalidations cause expensive reevaluation of dependent computations. - * We will space them exponentially in time to limit their cost. Time here means worker activity measured in number of iterations. - * - * This strategy is less effective against deep graphs of reactive workers - * where worker longevity may grow exponentially with graph depth. - * Dealing with such situations is still possible, but it would require more frequent pausing, likely guided by real time. - * - * Age below is the number of iterations, i.e. invocations of run() method. It doesn't need to be reactive. - * Generation is incremented when the highest bit of age moves up. This results in exponential spacing of invalidations. - * We will initialize age to non-zero value, so that a number of iterations is allowed to run before first pause. - */ - private long age = 4; - private static int generation(long age) { - /* - * Zero age results in generation 0. 1 -> 1, 2..3 -> 2, 4..7 -> 3, etc. - */ - return 64 - Long.numberOfLeadingZeros(age); - } - /* - * Current generation is written into this variable to ping all dependent computations. - */ - private final ReactiveVariable ping = OwnerTrace - .of(new ReactiveVariable<>(generation(age)) - /* - * This variable is accessed from get(). Make sure that reference to it will keep the whole worker alive. - * This is not strictly necessary since access to this variable is coupled with access to output variable, - * but we want to be sure. - */ - .keepalive(this)) - .parent(this) - .tag("role", "ping") - .target(); - /* - * Dependent computations acknowledge the ping by rereading worker's output. - * When get() is called, value of ping variable is copied into ack variable. - * This way the worker knows whether the last ping was acknowledged and thus whether it should be paused or not. - */ - private final ReactiveVariable ack = OwnerTrace - .of(new ReactiveVariable<>(generation(age))) - .parent(this) - .tag("role", "ack") - .target(); - /* - * We will use reactive thread and state machine instead of reactive primitives (scope, trigger, pins). - * This is lazy, costing us some performance. In the future, we might want to use the primitives instead. - */ - private final ReactiveStateMachine generator = OwnerTrace.of(ReactiveStateMachine.supply(this::supply)) - .parent(this) - .target(); - /* - * We don't have to synchronize anything, because reactive variables used here are already synchronized - * and we never rely on simultaneous changes to multiple variables. Some variables are also used exclusively here. - */ - private void run() { - /* - * Don't ever do anything if we already use the last output from the state machine. - */ - if (generator.valid()) - return; - ReactiveValue last; - /* - * Synchronized to ensure correct state transitions for output+ping+ack trio. - */ - synchronized (this) { - last = output.value(); - /* - * Cease all activity when nobody is using worker's output. - */ - boolean used = ping.get() == (int)ack.get(); - if (!used) { - /* - * Since we cease activity despite state machine invalidation, the output is now stale. We will mark it as blocking. - * Blocking output can be left stale, because we will get an opportunity to refresh it when someone requests it. - */ - if (!last.blocking()) - output.value(new ReactiveValue<>(last.result(), last.exception(), true)); - return; - } - } - /* - * Advancement of the generator as well as equality check are performed unsynchronized, because they can be slow. - */ - generator.advance(); - ReactiveValue fresh = generator.output(); - /* - * Accept blocking output only if the last output was blocking too. - * Once non-blocking output is produced, we don't want to ever revert to blocking output. - * We will just keep advancing the state machine until we reach the next non-blocking output. - */ - if (fresh.blocking() && !last.blocking()) - return; - /* - * Equality logic is copied from reactive variable, but we have to guard against exceptions, - * because contrary to reactive variable, we cannot afford to propagate exceptions here. - */ - boolean equal; - if (equality) { - try { - equal = fresh.equals(last); - } catch (Throwable ex) { - /* - * Assuming change is the safe thing to do in case application-defined equals() throws. - */ - equal = false; - } - } else - equal = fresh.same(last); - /* - * Synchronized to ensure correct state transitions for output+ping+ack trio. - */ - synchronized (this) { - /* - * Don't change the output variable if advancement of the state machine had no real effect on the output. - */ - if (!equal) - output.value(fresh); - /* - * We have to prevent waste of CPU, which may occur if GC is slow to remove unused worker. - * Send probing invalidations time to time to check whether the worker is still used. - */ - ++age; - ping.set(generation(age)); - } - } - private final ReactiveThread thread = OwnerTrace - .of(new ReactiveThread(this::run) - /* - * Always a "daemon" reactive thread. Worker should be garbage collected when nobody reads its output. - */ - .daemon(true)) - .parent(this) - .target(); - /* - * We will not expose the thread, because it's an implementation detail. We will just forward executor setting to it. - */ - public ReactiveWorker executor(Executor executor) { - thread.executor(executor); - return this; - } - public Executor executor() { - return thread.executor(); - } - /* - * Synchronized to ensure correct state transitions for output+ping+ack trio. - */ - @Override - public synchronized T get() { - /* - * Start the reactive thread on first access. - */ - if (!started) { - started = true; - thread.start(); - } - /* - * Someone requested the output. The worker is now considered used until next generation increment. - * This creates dependency on ping variable that will allow the worker to send probing invalidations to all dependent computations. - */ - ack.set(ping.get()); - return output.get(); - } - @Override - public String toString() { - return OwnerTrace.of(this) + " = " + output.value(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveCache.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveCache.java deleted file mode 100644 index 84be381..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveCache.java +++ /dev/null @@ -1,13 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -import java.util.*; - -/* - * Cache does not really have to keep the mapping from keys to nodes. - * Some nodes do not track any state, so they can be completely transient, although that's not ideal for debugging. - */ -public interface ReactiveCache { - ReactiveObjectNode materialize(ReactiveObject key); - Collection nodes(); -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveComputation.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveComputation.java deleted file mode 100644 index 9f88094..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveComputation.java +++ /dev/null @@ -1,5 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -public interface ReactiveComputation extends ReactiveObject { -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveComputationNode.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveComputationNode.java deleted file mode 100644 index e1b4468..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveComputationNode.java +++ /dev/null @@ -1,51 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -import java.util.stream.*; - -/* - * Computations can theoretically terminate when there are no dependencies to watch. - * Dependencies can be smart enough to not report their version via track() method when they know that version will not change. - * In practice however, dependencies cannot know whether their version will change across program runs, due to code change, for example. - * And since computations can be persistent, all dependencies have to continue reporting their versions to handle changes across runs. - * We could have a special flag attached to version that indicates the version will not change until the program terminates, - * but the added complexity is not worth the benefit, because most computations will depend on something that can change. - */ -public interface ReactiveComputationNode extends ReactiveObjectNode { - ReactiveComputation key(); - /* - * Number of times this computation has run. Non-negative. Informative. Non-reactive. - * There's no computation hash, because many computations do not produce any data. - * This is the number of completed iterations, so it's zero while the computation runs for the first time. - */ - long iteration(); - /* - * Throws if the computation is not currently running. - */ - void track(ReactiveDataNode dependency, ReactiveVersion version); - /* - * Throws if the computation is not currently running. - * Tolerates null parameter, which is interpreted as a no-op side effect. - */ - void consume(ReactiveSideEffect effect); - /* - * Redundant invalidations are silently ignored, because it is normal for several sources to change at the same time. - * Iteration number is necessary, because invalidations might arrive when the computation has already moved to the next iteration. - * - * Invalidations travel in reverse stack order, which creates the possibility of deadlock. - * Computations have to be designed to lock only for very short duration. - * They certainly cannot be locked while the computation is running. - * If minimal locking is not an option, invalidation handler must forward the invalidation into a thread pool. - * Thread pool is an elegant solution, but it prevents the computation from responding instantly, which is often important. - */ - void invalidate(long iteration); - /* - * Force refresh. Useful for global invalidations, debugging, and troubleshooting. - */ - void invalidate(); - /* - * Provide access to observed side effects while the computation is running. - */ - ReactiveSideEffect effect(ReactiveSideEffectKey key); - Stream effects(); -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveData.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveData.java deleted file mode 100644 index ac5599b..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveData.java +++ /dev/null @@ -1,5 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -public interface ReactiveData extends ReactiveObject { -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveDataNode.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveDataNode.java deleted file mode 100644 index e33602b..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveDataNode.java +++ /dev/null @@ -1,41 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -import java.util.*; - -/* - * Data nodes must be synchronized to ensure consistency between stored data, version, and subscriber map. - */ -public interface ReactiveDataNode extends ReactiveObjectNode { - ReactiveData key(); - /* - * Ideally content hash, but it can be also recursive dependency hash or a random hash indicating change. - * This method is guaranteed to return the current version. There is no delay of visibility of changes between data and version. - * Calls are non-reactive. To track the version as a dependency, call touch(). - */ - ReactiveVersion version(); - /* - * For diagnostic purposes only. - */ - Collection subscribers(); - /* - * If the version is wrong, invalidation is triggered immediately. - * Throws if the subscriber is already subscribed. Both old and new subscription is removed in that case. - * This node holds only weak reference to the subscriber. - */ - void subscribe(ReactiveComputationNode subscriber, long iteration, ReactiveVersion version); - /* - * Redundant unsubscription is ignored, because subscribers can be removed when the data changes. - */ - void unsubscribe(ReactiveComputationNode subscriber); - /* - * Enforce touch() API on all data nodes. - * This gives persistent caches a way to probe their dependencies without loading them to memory. - * The default implementation should work for most reactive data nodes as long as version() is efficient. - */ - default void touch() { - var consumer = ReactiveStack.top(); - if (consumer != null) - consumer.track(this, version()); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveIntermediary.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveIntermediary.java deleted file mode 100644 index 2510594..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveIntermediary.java +++ /dev/null @@ -1,5 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -public interface ReactiveIntermediary extends ReactiveData, ReactiveComputation { -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveIntermediaryNode.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveIntermediaryNode.java deleted file mode 100644 index 96ddd6d..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveIntermediaryNode.java +++ /dev/null @@ -1,6 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -public interface ReactiveIntermediaryNode extends ReactiveDataNode, ReactiveComputationNode { - ReactiveIntermediary key(); -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveKey.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveKey.java deleted file mode 100644 index 3c713ef..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveKey.java +++ /dev/null @@ -1,22 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -import java.io.*; - -/* - * Keys must be immutable, equatable, hashable, stringifiable, serializable, and transparent. - * Most keys should be records that embed enough information to uniquely identify something in the application. - * Keys might contain parent key. Parent keys form a chain that identifies whole context of the key. - * - * Some key types might be reusable, relying on opaque parameters for identifying information. - * It is however preferable to define a lot of specialized keys to maximize diagnostic information. - * - * Java records have poorly implemented hashCode(), which does not take into account class name. - * Records with the same parameters, especially empty records, consequently share the same hash code. - * If the key does not have any unique parameters, it is recommended to override hashCode(). - * - * Built-in Java serialization support is enforced, but keys should preferably support custom serialization. - * Records will seamlessly support Java serialization as well as most serialization libraries. - */ -public interface ReactiveKey extends Serializable { -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveObject.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveObject.java deleted file mode 100644 index 14390f9..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveObject.java +++ /dev/null @@ -1,6 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -public interface ReactiveObject extends ReactiveKey { - ReactiveObjectConfig reactiveConfig(); -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveObjectConfig.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveObjectConfig.java deleted file mode 100644 index 62d29af..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveObjectConfig.java +++ /dev/null @@ -1,11 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -public interface ReactiveObjectConfig { - ReactiveObject key(); - ReactiveCache cache(); - /* - * To be used by reactive caches only. App code should use ReactiveNode.of(key). - */ - ReactiveObjectNode instantiate(); -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveObjectNode.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveObjectNode.java deleted file mode 100644 index 8a77ad4..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveObjectNode.java +++ /dev/null @@ -1,9 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -public interface ReactiveObjectNode { - ReactiveObject key(); - static ReactiveObjectNode of(ReactiveObject key) { - return key.reactiveConfig().cache().materialize(key); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveSideEffect.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveSideEffect.java deleted file mode 100644 index 96e0ee0..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveSideEffect.java +++ /dev/null @@ -1,43 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -import java.io.*; - -/* - * Side effect represents function output that bypasses the formal arguments, return value, and exceptions. - * It's intended to capture function output that would be otherwise written into a buffer, context object, or console. - * Side effect is never a no-op. Null is used in place of no-op side effects. - * - * Side effect is not itself a key, because keys are expected to be small, which might not be the case for some side effects. - * Side effects are however required to be serializable and sufficiently transparent to support custom serialization. - * Side effects also have to be immutable and preferably also stringifiable. - * Equatability can be offered where it simplifies code that handles side effects. - */ -public interface ReactiveSideEffect extends Serializable { - /* - * Side effects with the same key are merged. - * - * Although side effects can be often identified by type, i.e. via instanceof on the side effect itself, - * this cannot be guaranteed to always be the case, for example in case of reusable wrappers or keyed side effects. - * Keys are therefore the ultimate way to identify side effects. This does not however preclude simpler mechanisms. - * - * Side effect identification in order of preference: - * - instanceof on the data object - * - instanceof on the key - * - examining contents of the key - */ - ReactiveSideEffectKey key(); - /* - * Merges this side effect with a subsequent one and returns the result. - * Side effects are immutable. This side effect is not modified. - * Throws if the two side effects have different keys. - * Returns null if the result is a no-op. Returns this if the other side effect is null. - * - * If merging is performed often on large side effects, persistent collections can help with performance. - * Appending one-element side effects to a long list would result in quadratic performance with standard collections. - * We could provide some sort of accumulator API to help with merging efficiency, - * but performance improvement is questionable and simpler API is currently preferred. - * Side effect data object itself cannot be mutable, because that would prevent clean serialization with records. - */ - ReactiveSideEffect merge(ReactiveSideEffect other); -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveSideEffectKey.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveSideEffectKey.java deleted file mode 100644 index 509b9fb..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveSideEffectKey.java +++ /dev/null @@ -1,8 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -/* - * Just a key. Actual data is in separate data object. - */ -public interface ReactiveSideEffectKey extends ReactiveKey { -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveStack.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveStack.java deleted file mode 100644 index af5710b..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveStack.java +++ /dev/null @@ -1,46 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -import java.util.*; -import com.machinezoo.closeablescope.*; -import com.machinezoo.stagean.*; - -/* - * This could be later replaced with JEP 429: Scoped Values (https://openjdk.org/jeps/429). - * Scoped values support rebinding and they work better with JEP 425: Virtual Threads. - * The try-with-resources API would then have to be abandoned in favor of ScopedValue.run(). - * Dependency reporting by multiple threads can be handled by having a map of collectors keyed by thread ID. - * Pins would not be necessary and freezes could be done with scoped values as well. - */ -@ApiIssue("Alternative to top() that returns no-op consumer instead of null.") -@ApiIssue("Stack trace of the current thread, of other threads, and a way to enumerate threads.") -public class ReactiveStack { - private static ThreadLocal> current = ThreadLocal.withInitial(ArrayDeque::new); - public static ReactiveComputationNode top() { - return current.get().peekLast(); - } - public static CloseableScope push(ReactiveComputationNode computation) { - Objects.requireNonNull(computation); - var stack = current.get(); - /* - * No checks. Allow starting the same computation twice. - */ - stack.addLast(computation); - return () -> { - /* - * Try the fast path first. - */ - if (stack.peekLast() == computation) - stack.removeLast(); - else { - /* - * Tolerate double closing. Do not report any error if the computation is not on the stack anymore. - * Remove from the end in order to tolerate the rare case of doubly started computations. - * Tolerate stopping computations out of order. This may happen if computations are stopped explicitly - * rather than via try-with-resources. - */ - stack.removeLastOccurrence(computation); - } - }; - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveVersion.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveVersion.java deleted file mode 100644 index f16ff26..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveVersion.java +++ /dev/null @@ -1,21 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -public interface ReactiveVersion extends ReactiveKey { - /* - * All version objects are convertible to a hash. - * - * Hashes are allowed to be equal only if versions are equal. - * There must be no way to accidentally create the same hash from different version objects, - * even from two different types of version objects that just happen to share parameters. - * - * That implies: - * - Hash must be generated using cryptographic hashing algorithm like SHA-256, even for data that fits in 256 bits. - * - Type of version object must be included in the hash. Hashing parameters alone is not enough. - * - Null parameters must result in unique hashes distinct from hashing empty arrays/strings. - * - * Hashes are perfectly reproducible during single program run and mostly reproducible between runs. - * Occasional changes are permitted between runs, especially after code changes. - */ - ReactiveVersionHash toHash(); -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/ReactiveVersionHash.java b/src/main/java/com/machinezoo/hookless/experimental/ReactiveVersionHash.java deleted file mode 100644 index a26200d..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/ReactiveVersionHash.java +++ /dev/null @@ -1,132 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; - -import java.nio.*; -import java.nio.charset.*; -import java.security.*; -import java.util.*; -import com.machinezoo.noexception.*; - -/* - * This is usually output of some cryptographic hash like SHA-256. - * Word 0 contains lowest bits. Word 3 contains highest bits. - * When serialized, the whole 256-bit hash is saved in big-endian byte order. - */ -public record ReactiveVersionHash(long word3, long word2, long word1, long word0) implements ReactiveVersion { - /* - * May be used in some edge cases like hashing nulls. Nobody has ever found data that would be hashed to zero by SHA-256. - */ - public static final ReactiveVersionHash ZERO = new ReactiveVersionHash(0, 0, 0, 0); - public long word(int offset) { - return switch (offset) { - case 0 -> word0; - case 1 -> word1; - case 2 -> word2; - case 3 -> word3; - default -> throw new IllegalArgumentException(); - }; - } - public static ReactiveVersionHash fromWords(long[] words) { - return new ReactiveVersionHash(words[0], words[1], words[2], words[3]); - } - public long[] toWords() { - return new long[] { word3, word2, word1, word0 }; - } - public static ReactiveVersionHash fromBytes(byte[] bytes) { - if (bytes.length != 32) - throw new IllegalArgumentException(); - var buffer = ByteBuffer.wrap(bytes); - return new ReactiveVersionHash(buffer.getLong(), buffer.getLong(), buffer.getLong(), buffer.getLong()); - } - public byte[] toBytes() { - var buffer = ByteBuffer.allocate(32); - buffer.putLong(word3); - buffer.putLong(word2); - buffer.putLong(word1); - buffer.putLong(word0); - return buffer.array(); - } - public String toBase64() { - /* - * In BASE64, 32 bytes will be padded to 33 and the bits will be then spread over 44 characters. - * The last character will not encode anything, so it will be just '='. - */ - return Base64.getUrlEncoder().encodeToString(toBytes()).substring(0, 43); - } - private static final SecureRandom random = new SecureRandom(); - public static ReactiveVersionHash random() { - var bytes = new byte[32]; - random.nextBytes(bytes); - return ReactiveVersionHash.fromBytes(bytes); - } - public static ReactiveVersionHash hash(byte[] data) { - if (data == null) - return ZERO; - return fromBytes(Exceptions.sneak().get(() -> MessageDigest.getInstance("SHA-256")).digest(data)); - } - public static ReactiveVersionHash hash(String text) { - return hash(text != null ? text.getBytes(StandardCharsets.UTF_8) : null); - } - /* - * Computes hash from object's type and its toString() output. - * Assumes that two objects of the same type have the same toString() output iff the two objects are equal. - * - * This places a number of restrictions on the object and all objects it contains: - * - Implementation of toString() must be provided unless it is generated automatically like in enums and records. - * - If arrays are included in records, these records must override toString(). - * - Types of contained objects must be sufficiently constrained to avoid ambiguous toString() output. - * - * Generally, this works best for simple specialized records. - */ - public static ReactiveVersionHash hash(Object object) { - if (object == null) - return ZERO; - return hash(object.getClass().getName() + ": " + object.toString()); - } - public ReactiveVersionHash combine(ReactiveVersionHash other) { - Objects.requireNonNull(other); - var hasher = Exceptions.sneak().get(() -> MessageDigest.getInstance("SHA-256")); - hasher.update(toBytes()); - hasher.update(other.toBytes()); - return fromBytes(hasher.digest()); - } - public ReactiveVersionHash combine(byte[] data) { - var hasher = Exceptions.sneak().get(() -> MessageDigest.getInstance("SHA-256")); - hasher.update(toBytes()); - hasher.update(new byte[] { data != null ? (byte)1 : (byte)0 }); - if (data != null) - hasher.update(data); - return fromBytes(hasher.digest()); - } - public ReactiveVersionHash combine(String text) { - return combine(text != null ? text.getBytes(StandardCharsets.UTF_8) : null); - } - public ReactiveVersionHash combine(Object object) { - return combine(hash(object)); - } - @Override - public ReactiveVersionHash toHash() { - return this; - } - @Override - public boolean equals(Object obj) { - return obj instanceof ReactiveVersionHash other - && word0 == other.word0 - && word1 == other.word1 - && word2 == other.word2 - && word3 == other.word3; - } - @Override - public int hashCode() { - /* - * Here we optimistically assume the hash bits appear to be random. - * Explicitly constructed hashes with trivial content would need better hashCode(). - */ - var sum = word0 ^ word1 ^ word2 ^ word3; - return (int)(sum | (sum >>> 32)); - } - @Override - public String toString() { - return toBase64(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/package-info.java b/src/main/java/com/machinezoo/hookless/experimental/package-info.java deleted file mode 100644 index cb3a0f5..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental; \ No newline at end of file diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/StandardReactiveDataNode.java b/src/main/java/com/machinezoo/hookless/experimental/std/StandardReactiveDataNode.java deleted file mode 100644 index e80cf64..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/StandardReactiveDataNode.java +++ /dev/null @@ -1,78 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std; - -import java.lang.ref.*; -import java.util.*; -import com.machinezoo.hookless.experimental.*; - -public abstract class StandardReactiveDataNode implements ReactiveDataNode { - private ReactiveVersion version; - private record Subscriber(WeakReference computation, long iteration) {} - private Map subscribers = new HashMap<>(); - protected StandardReactiveDataNode(ReactiveVersion version) { - this.version = version; - } - @Override - public synchronized ReactiveVersion version() { - return version; - } - @Override - public synchronized Collection subscribers() { - return subscribers.values().stream() - .map(s -> s.computation.get()) - .filter(Objects::nonNull) - .toList(); - } - @Override - public void subscribe(ReactiveComputationNode subscriber, long iteration, ReactiveVersion version) { - boolean invalidate = false; - synchronized (this) { - if (this.version.equals(version)) { - if (subscribers.remove(subscriber.key()) != null) - throw new IllegalStateException("Computation is already subscribed."); - subscribers.put(subscriber.key(), new Subscriber(new WeakReference<>(subscriber), iteration)); - } else - invalidate = true; - } - /* - * Run invalidation unlocked, because it is a call in reverse stack order. - */ - if (invalidate) - subscriber.invalidate(iteration); - } - @Override - public synchronized void unsubscribe(ReactiveComputationNode subscriber) { - subscribers.remove(subscriber.key()); - } - /* - * Must be called from the same synchronized context that reads data - * to ensure consistency of data and tracked version. - */ - protected void track() { - var computation = ReactiveStack.top(); - if (computation != null) - computation.track(this, version); - } - /* - * Must be called from synchronized context, because it performs read-then-update on version and subscribers. - * Data changes should be performed in the same synchronized context to ensure consistency of data, version, and subscribers. - * Returns invalidation batch that must be called outside synchronized context. - */ - protected Runnable commit(ReactiveVersion version) { - if (!this.version.equals(version)) { - this.version = version; - if (subscribers.isEmpty()) - return null; - var invalidated = subscribers; - subscribers = new HashMap<>(); - return () -> { - for (var subscriber : invalidated.values()) { - var computation = subscriber.computation().get(); - if (computation != null) - computation.invalidate(subscriber.iteration()); - } - }; - } else - return null; - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/bells/ReactiveBell.java b/src/main/java/com/machinezoo/hookless/experimental/std/bells/ReactiveBell.java deleted file mode 100644 index 92beb65..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/bells/ReactiveBell.java +++ /dev/null @@ -1,20 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.bells; - -import com.machinezoo.hookless.experimental.*; - -public interface ReactiveBell extends ReactiveData { - @Override - default ReactiveObjectConfig reactiveConfig() { - return new ReactiveBellConfig(this); - } - private ReactiveBellNode node() { - return (ReactiveBellNode)ReactiveObjectNode.of(this); - } - default void listen() { - node().listen(); - } - default void ring() { - node().ring(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/bells/ReactiveBellConfig.java b/src/main/java/com/machinezoo/hookless/experimental/std/bells/ReactiveBellConfig.java deleted file mode 100644 index 69ffa1b..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/bells/ReactiveBellConfig.java +++ /dev/null @@ -1,26 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.bells; - -import java.util.*; -import com.machinezoo.hookless.experimental.*; -import com.machinezoo.hookless.experimental.std.caches.*; - -public class ReactiveBellConfig implements ReactiveObjectConfig { - private final ReactiveBell key; - public ReactiveBellConfig(ReactiveBell key) { - Objects.requireNonNull(key); - this.key = key; - } - @Override - public ReactiveBell key() { - return key; - } - @Override - public ReactiveCache cache() { - return PermanentReactiveCache.DEFAULT; - } - @Override - public ReactiveBellNode instantiate() { - return new ReactiveBellNode(key); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/bells/ReactiveBellNode.java b/src/main/java/com/machinezoo/hookless/experimental/std/bells/ReactiveBellNode.java deleted file mode 100644 index 7d89fe9..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/bells/ReactiveBellNode.java +++ /dev/null @@ -1,29 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.bells; - -import com.machinezoo.hookless.experimental.std.*; -import com.machinezoo.hookless.experimental.std.versions.*; - -public class ReactiveBellNode extends StandardReactiveDataNode { - private final ReactiveBell key; - private long iteration = 1; - public ReactiveBellNode(ReactiveBell key) { - super(new ReactiveVersionNumber(1)); - this.key = key; - } - @Override - public ReactiveBell key() { - return key; - } - public synchronized void listen() { - track(); - } - public void ring() { - Runnable invalidation; - synchronized (this) { - invalidation = commit(new ReactiveVersionNumber(++iteration)); - } - if (invalidation != null) - invalidation.run(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/bells/package-info.java b/src/main/java/com/machinezoo/hookless/experimental/std/bells/package-info.java deleted file mode 100644 index 16be306..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/bells/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.bells; \ No newline at end of file diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/blocking/ReactiveBlocking.java b/src/main/java/com/machinezoo/hookless/experimental/std/blocking/ReactiveBlocking.java deleted file mode 100644 index 6aeb46d..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/blocking/ReactiveBlocking.java +++ /dev/null @@ -1,28 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.blocking; - -import com.machinezoo.hookless.experimental.*; - -public record ReactiveBlocking() implements ReactiveSideEffect { - @Override - public ReactiveBlockingKey key() { - return ReactiveBlockingKey.INSTANCE; - } - @Override - public ReactiveSideEffect merge(ReactiveSideEffect other) { - if (!(other instanceof ReactiveBlocking)) - throw new IllegalArgumentException(); - return this; - } - public static void block() { - var computation = ReactiveStack.top(); - if (computation != null) - computation.consume(new ReactiveBlocking()); - } - public static boolean blocking() { - var computation = ReactiveStack.top(); - if (computation == null) - return false; - return computation.effect(ReactiveBlockingKey.INSTANCE) != null; - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/blocking/ReactiveBlockingException.java b/src/main/java/com/machinezoo/hookless/experimental/std/blocking/ReactiveBlockingException.java deleted file mode 100644 index 5d2c4ec..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/blocking/ReactiveBlockingException.java +++ /dev/null @@ -1,31 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.blocking; - -public class ReactiveBlockingException extends RuntimeException { - private static final long serialVersionUID = 1L; - public ReactiveBlockingException(String message, Throwable cause) { - super(message, cause); - } - public ReactiveBlockingException(String message) { - this(message, null); - } - public ReactiveBlockingException(Throwable cause) { - super(cause != null ? cause.toString() : null, cause); - } - public ReactiveBlockingException() { - this(null, null); - } - public static ReactiveBlockingException block(String message, Throwable cause) { - ReactiveBlocking.block(); - throw new ReactiveBlockingException(message, cause); - } - public static ReactiveBlockingException block(String message) { - throw block(message, null); - } - public static ReactiveBlockingException block(Throwable cause) { - throw block(null, cause); - } - public static ReactiveBlockingException block() { - throw block(null, null); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/blocking/ReactiveBlockingKey.java b/src/main/java/com/machinezoo/hookless/experimental/std/blocking/ReactiveBlockingKey.java deleted file mode 100644 index 3ee411d..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/blocking/ReactiveBlockingKey.java +++ /dev/null @@ -1,8 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.blocking; - -import com.machinezoo.hookless.experimental.*; - -public record ReactiveBlockingKey() implements ReactiveSideEffectKey { - public static final ReactiveBlockingKey INSTANCE = new ReactiveBlockingKey(); -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/blocking/package-info.java b/src/main/java/com/machinezoo/hookless/experimental/std/blocking/package-info.java deleted file mode 100644 index 7c268bd..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/blocking/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.blocking; \ No newline at end of file diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/caches/PermanentReactiveCache.java b/src/main/java/com/machinezoo/hookless/experimental/std/caches/PermanentReactiveCache.java deleted file mode 100644 index fd703f5..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/caches/PermanentReactiveCache.java +++ /dev/null @@ -1,22 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.caches; - -import java.util.*; -import java.util.concurrent.*; -import com.machinezoo.hookless.experimental.*; - -public class PermanentReactiveCache implements ReactiveCache { - public static final PermanentReactiveCache DEFAULT = new PermanentReactiveCache(); - private final ConcurrentMap nodes = new ConcurrentHashMap<>(); - @Override - public ReactiveObjectNode materialize(ReactiveObject key) { - return nodes.computeIfAbsent(key, k -> k.reactiveConfig().instantiate()); - } - @Override - public Collection nodes() { - /* - * Defensive copy to prevent mutations and to protect caller from concurrent changes. - */ - return new ArrayList<>(nodes.values()); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/caches/TransientReactiveCache.java b/src/main/java/com/machinezoo/hookless/experimental/std/caches/TransientReactiveCache.java deleted file mode 100644 index b8b851c..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/caches/TransientReactiveCache.java +++ /dev/null @@ -1,17 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.caches; - -import java.util.*; -import com.machinezoo.hookless.experimental.*; - -public class TransientReactiveCache implements ReactiveCache { - public static final TransientReactiveCache DEFAULT = new TransientReactiveCache(); - @Override - public ReactiveObjectNode materialize(ReactiveObject key) { - return key.reactiveConfig().instantiate(); - } - @Override - public Collection nodes() { - return Collections.emptyList(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/caches/package-info.java b/src/main/java/com/machinezoo/hookless/experimental/std/caches/package-info.java deleted file mode 100644 index 19be4c7..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/caches/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.caches; \ No newline at end of file diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/constants/HashedReactiveConstantObject.java b/src/main/java/com/machinezoo/hookless/experimental/std/constants/HashedReactiveConstantObject.java deleted file mode 100644 index f561ecd..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/constants/HashedReactiveConstantObject.java +++ /dev/null @@ -1,12 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.constants; - -/* - * With default configuration, object must satisfy requirements of ReactiveVersionObject. - */ -public interface HashedReactiveConstantObject extends ReactiveConstantObject { - @Override - default ReactiveConstantObjectConfig reactiveConfig() { - return new HashedReactiveConstantObjectConfig<>(this); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/constants/HashedReactiveConstantObjectConfig.java b/src/main/java/com/machinezoo/hookless/experimental/std/constants/HashedReactiveConstantObjectConfig.java deleted file mode 100644 index 1c52d76..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/constants/HashedReactiveConstantObjectConfig.java +++ /dev/null @@ -1,18 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.constants; - -import com.machinezoo.hookless.experimental.*; - -public class HashedReactiveConstantObjectConfig extends ReactiveConstantObjectConfig { - public HashedReactiveConstantObjectConfig(HashedReactiveConstantObject key) { - super(key); - } - @Override - public HashedReactiveConstantObject key() { - return (HashedReactiveConstantObject)super.key(); - } - @Override - public ReactiveVersion version() { - return super.version().toHash(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstant.java b/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstant.java deleted file mode 100644 index 9119719..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstant.java +++ /dev/null @@ -1,12 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.constants; - -import com.machinezoo.hookless.experimental.*; - -public interface ReactiveConstant extends ReactiveData { - @Override - ReactiveConstantConfig reactiveConfig(); - default void touch() { - new ReactiveConstantNode(this).track(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantConfig.java b/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantConfig.java deleted file mode 100644 index dd78cc7..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.constants; - -import com.machinezoo.hookless.experimental.*; -import com.machinezoo.hookless.experimental.std.caches.*; - -public interface ReactiveConstantConfig extends ReactiveObjectConfig { - @Override - ReactiveConstant key(); - ReactiveVersion version(); - @Override - default ReactiveCache cache() { - return TransientReactiveCache.DEFAULT; - } - @Override - default ReactiveConstantNode instantiate() { - return new ReactiveConstantNode(key()); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantNode.java b/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantNode.java deleted file mode 100644 index ec031ce..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantNode.java +++ /dev/null @@ -1,35 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.constants; - -import java.util.*; -import com.machinezoo.hookless.experimental.*; - -public class ReactiveConstantNode implements ReactiveDataNode { - private final ReactiveConstant key; - public ReactiveConstantNode(ReactiveConstant key) { - this.key = key; - } - @Override - public ReactiveConstant key() { - return key; - } - @Override - public ReactiveVersion version() { - return key.reactiveConfig().version(); - } - @Override - public Collection subscribers() { - return Collections.emptyList(); - } - @Override - public void subscribe(ReactiveComputationNode subscriber, long iteration, ReactiveVersion version) { - } - @Override - public void unsubscribe(ReactiveComputationNode subscriber) { - } - public void track() { - var computation = ReactiveStack.top(); - if (computation != null) - computation.track(this, key.reactiveConfig().version()); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantNumber.java b/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantNumber.java deleted file mode 100644 index a146ec8..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantNumber.java +++ /dev/null @@ -1,17 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.constants; - -public interface ReactiveConstantNumber extends ReactiveConstant { - @Override - default ReactiveConstantNumberConfig reactiveConfig() { - return new ReactiveConstantNumberConfig(this); - } - long compute(); - default long getAsLong() { - touch(); - return compute(); - } - default int getAsInt() { - return (int)getAsLong(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantNumberConfig.java b/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantNumberConfig.java deleted file mode 100644 index 03cd478..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantNumberConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.constants; - -import java.util.*; -import com.machinezoo.hookless.experimental.*; -import com.machinezoo.hookless.experimental.std.versions.*; - -public class ReactiveConstantNumberConfig implements ReactiveConstantConfig { - private final ReactiveConstantNumber key; - public ReactiveConstantNumberConfig(ReactiveConstantNumber key) { - Objects.requireNonNull(key); - this.key = key; - } - @Override - public ReactiveConstantNumber key() { - return key; - } - @Override - public ReactiveVersion version() { - return new ReactiveVersionNumber(key.compute()); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantObject.java b/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantObject.java deleted file mode 100644 index 35f9589..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantObject.java +++ /dev/null @@ -1,17 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.constants; - -/* - * With default configuration, object must satisfy requirements of ReactiveVersionObject. - */ -public interface ReactiveConstantObject extends ReactiveConstant { - @Override - default ReactiveConstantObjectConfig reactiveConfig() { - return new ReactiveConstantObjectConfig<>(this); - } - T compute(); - default T get() { - touch(); - return compute(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantObjectConfig.java b/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantObjectConfig.java deleted file mode 100644 index 5555add..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantObjectConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.constants; - -import java.util.*; -import com.machinezoo.hookless.experimental.*; -import com.machinezoo.hookless.experimental.std.versions.*; - -public class ReactiveConstantObjectConfig implements ReactiveConstantConfig { - private final ReactiveConstantObject key; - public ReactiveConstantObjectConfig(ReactiveConstantObject key) { - Objects.requireNonNull(key); - this.key = key; - } - @Override - public ReactiveConstantObject key() { - return key; - } - @Override - public ReactiveVersion version() { - return new ReactiveVersionObject(key.compute()); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantString.java b/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantString.java deleted file mode 100644 index d99ed89..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantString.java +++ /dev/null @@ -1,14 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.constants; - -public interface ReactiveConstantString extends ReactiveConstant { - @Override - default ReactiveConstantStringConfig reactiveConfig() { - return new ReactiveConstantStringConfig(this); - } - String compute(); - default String get() { - touch(); - return compute(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantStringConfig.java b/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantStringConfig.java deleted file mode 100644 index 5eb82f8..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/constants/ReactiveConstantStringConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.constants; - -import java.util.*; -import com.machinezoo.hookless.experimental.*; -import com.machinezoo.hookless.experimental.std.versions.*; - -public class ReactiveConstantStringConfig implements ReactiveConstantConfig { - private final ReactiveConstantString key; - public ReactiveConstantStringConfig(ReactiveConstantString key) { - Objects.requireNonNull(key); - this.key = key; - } - @Override - public ReactiveConstantString key() { - return key; - } - @Override - public ReactiveVersion version() { - return new ReactiveVersionString(key.compute()); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/constants/package-info.java b/src/main/java/com/machinezoo/hookless/experimental/std/constants/package-info.java deleted file mode 100644 index c121075..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/constants/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.constants; \ No newline at end of file diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/markers/ReactiveMarker.java b/src/main/java/com/machinezoo/hookless/experimental/std/markers/ReactiveMarker.java deleted file mode 100644 index 54cc660..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/markers/ReactiveMarker.java +++ /dev/null @@ -1,14 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.markers; - -import com.machinezoo.hookless.experimental.*; - -public interface ReactiveMarker extends ReactiveData { - @Override - default ReactiveMarkerConfig reactiveConfig() { - return new ReactiveMarkerConfig(this); - } - default void touch() { - new ReactiveMarkerNode(this).track(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/markers/ReactiveMarkerConfig.java b/src/main/java/com/machinezoo/hookless/experimental/std/markers/ReactiveMarkerConfig.java deleted file mode 100644 index 856ebfe..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/markers/ReactiveMarkerConfig.java +++ /dev/null @@ -1,26 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.markers; - -import java.util.*; -import com.machinezoo.hookless.experimental.*; -import com.machinezoo.hookless.experimental.std.caches.*; - -public class ReactiveMarkerConfig implements ReactiveObjectConfig { - private final ReactiveMarker key; - public ReactiveMarkerConfig(ReactiveMarker key) { - Objects.requireNonNull(key); - this.key = key; - } - @Override - public ReactiveMarker key() { - return key; - } - @Override - public ReactiveCache cache() { - return TransientReactiveCache.DEFAULT; - } - @Override - public ReactiveMarkerNode instantiate() { - return new ReactiveMarkerNode(key); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/markers/ReactiveMarkerNode.java b/src/main/java/com/machinezoo/hookless/experimental/std/markers/ReactiveMarkerNode.java deleted file mode 100644 index a8cd163..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/markers/ReactiveMarkerNode.java +++ /dev/null @@ -1,36 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.markers; - -import java.util.*; -import com.machinezoo.hookless.experimental.*; -import com.machinezoo.hookless.experimental.std.versions.*; - -public class ReactiveMarkerNode implements ReactiveDataNode { - private final ReactiveMarker key; - public ReactiveMarkerNode(ReactiveMarker key) { - this.key = key; - } - @Override - public ReactiveMarker key() { - return key; - } - @Override - public ReactiveVersion version() { - return NullReactiveVersion.INSTANCE; - } - @Override - public Collection subscribers() { - return Collections.emptyList(); - } - @Override - public void subscribe(ReactiveComputationNode subscriber, long iteration, ReactiveVersion version) { - } - @Override - public void unsubscribe(ReactiveComputationNode subscriber) { - } - public void track() { - var computation = ReactiveStack.top(); - if (computation != null) - computation.track(this, NullReactiveVersion.INSTANCE); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/markers/package-info.java b/src/main/java/com/machinezoo/hookless/experimental/std/markers/package-info.java deleted file mode 100644 index 843e7c1..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/markers/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.markers; \ No newline at end of file diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/package-info.java b/src/main/java/com/machinezoo/hookless/experimental/std/package-info.java deleted file mode 100644 index cd108b1..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std; \ No newline at end of file diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/versions/NullReactiveVersion.java b/src/main/java/com/machinezoo/hookless/experimental/std/versions/NullReactiveVersion.java deleted file mode 100644 index 9aa7332..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/versions/NullReactiveVersion.java +++ /dev/null @@ -1,20 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.versions; - -import com.machinezoo.hookless.experimental.*; - -/* - * Version that never changes. Useful for reactive markers. - */ -public record NullReactiveVersion() implements ReactiveVersion { - public static final NullReactiveVersion INSTANCE = new NullReactiveVersion(); - private static final ReactiveVersionHash HASH = ReactiveVersionHash.hash(NullReactiveVersion.class.getName()); - @Override - public ReactiveVersionHash toHash() { - return HASH; - } - @Override - public String toString() { - return "()"; - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/versions/ReactiveVersionNumber.java b/src/main/java/com/machinezoo/hookless/experimental/std/versions/ReactiveVersionNumber.java deleted file mode 100644 index a14ce3f..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/versions/ReactiveVersionNumber.java +++ /dev/null @@ -1,17 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.versions; - -import com.google.common.primitives.*; -import com.machinezoo.hookless.experimental.*; - -public record ReactiveVersionNumber(long number) implements ReactiveVersion { - private static final ReactiveVersionHash PREFIX = ReactiveVersionHash.hash(ReactiveVersionNumber.class.getName()); - @Override - public ReactiveVersionHash toHash() { - return PREFIX.combine(Longs.toByteArray(number)); - } - @Override - public String toString() { - return Long.toString(number); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/versions/ReactiveVersionObject.java b/src/main/java/com/machinezoo/hookless/experimental/std/versions/ReactiveVersionObject.java deleted file mode 100644 index 61c50a8..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/versions/ReactiveVersionObject.java +++ /dev/null @@ -1,21 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.versions; - -import java.util.*; -import com.machinezoo.hookless.experimental.*; - -/* - * Intended for small objects only. Large objects should be hashed. Object can be null. - * Object must have the same properties as ReactiveKey plus it must satisfy requirements of ReactiveVersionHash.hash(object). - */ -public record ReactiveVersionObject(Object object) implements ReactiveVersion { - private static final ReactiveVersionHash PREFIX = ReactiveVersionHash.hash(ReactiveVersionObject.class.getName()); - @Override - public ReactiveVersionHash toHash() { - return PREFIX.combine(object); - } - @Override - public String toString() { - return Objects.toString(object); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/versions/ReactiveVersionString.java b/src/main/java/com/machinezoo/hookless/experimental/std/versions/ReactiveVersionString.java deleted file mode 100644 index 02ff7af..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/versions/ReactiveVersionString.java +++ /dev/null @@ -1,20 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.versions; - -import java.util.*; -import com.machinezoo.hookless.experimental.*; - -/* - * Intended for short strings only. Long strings should be hashed. String can be null. - */ -public record ReactiveVersionString(String text) implements ReactiveVersion { - private static final ReactiveVersionHash PREFIX = ReactiveVersionHash.hash(ReactiveVersionString.class.getName()); - @Override - public ReactiveVersionHash toHash() { - return PREFIX.combine(text); - } - @Override - public String toString() { - return Objects.toString(text); - } -} diff --git a/src/main/java/com/machinezoo/hookless/experimental/std/versions/package-info.java b/src/main/java/com/machinezoo/hookless/experimental/std/versions/package-info.java deleted file mode 100644 index 3899324..0000000 --- a/src/main/java/com/machinezoo/hookless/experimental/std/versions/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.experimental.std.versions; \ No newline at end of file diff --git a/src/main/java/com/machinezoo/hookless/noexception/ReactiveExceptions.java b/src/main/java/com/machinezoo/hookless/noexception/ReactiveExceptions.java deleted file mode 100644 index bb99389..0000000 --- a/src/main/java/com/machinezoo/hookless/noexception/ReactiveExceptions.java +++ /dev/null @@ -1,76 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.noexception; - -import com.machinezoo.hookless.*; -import com.machinezoo.noexception.*; -import com.machinezoo.stagean.*; - -/** - * Wrappers for {@link ExceptionHandler} and {@link ExceptionFilter}. - */ -@StubDocs -@NoTests -@DraftApi("if this is used often, we might consider neater API, e.g. .silence().blocking().run(...)") -public class ReactiveExceptions { - /* - * We aren't using the more concise anonymous inner classes, because we want nice class names in stack traces. - */ - private static class BlockingExceptionHandler extends ExceptionHandler { - private final ExceptionHandler inner; - BlockingExceptionHandler(ExceptionHandler inner) { - this.inner = inner; - } - @Override - public boolean handle(Throwable exception) { - if (CurrentReactiveScope.blocked()) - return inner.handle(exception); - return false; - } - } - public static ExceptionHandler blocking(ExceptionHandler handler) { - return new BlockingExceptionHandler(handler); - } - private static class NonBlockingExceptionHandler extends ExceptionHandler { - private final ExceptionHandler inner; - NonBlockingExceptionHandler(ExceptionHandler inner) { - this.inner = inner; - } - @Override - public boolean handle(Throwable exception) { - if (!CurrentReactiveScope.blocked()) - return inner.handle(exception); - return false; - } - } - public static ExceptionHandler nonblocking(ExceptionHandler handler) { - return new NonBlockingExceptionHandler(handler); - } - private static class BlockingExceptionFilter extends ExceptionFilter { - private final ExceptionFilter inner; - BlockingExceptionFilter(ExceptionFilter inner) { - this.inner = inner; - } - @Override - public void handle(Throwable exception) { - if (CurrentReactiveScope.blocked()) - inner.handle(exception); - } - } - public static ExceptionFilter blocking(ExceptionFilter handler) { - return new BlockingExceptionFilter(handler); - } - private static class NonBlockingExceptionFilter extends ExceptionFilter { - private final ExceptionFilter inner; - NonBlockingExceptionFilter(ExceptionFilter inner) { - this.inner = inner; - } - @Override - public void handle(Throwable exception) { - if (!CurrentReactiveScope.blocked()) - inner.handle(exception); - } - } - public static ExceptionFilter nonblocking(ExceptionFilter handler) { - return new NonBlockingExceptionFilter(handler); - } -} diff --git a/src/main/java/com/machinezoo/hookless/noexception/package-info.java b/src/main/java/com/machinezoo/hookless/noexception/package-info.java deleted file mode 100644 index d022fd4..0000000 --- a/src/main/java/com/machinezoo/hookless/noexception/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -/** - * Reactive extensions for NoException. - */ -package com.machinezoo.hookless.noexception; \ No newline at end of file diff --git a/src/main/java/com/machinezoo/hookless/package-info.java b/src/main/java/com/machinezoo/hookless/package-info.java deleted file mode 100644 index d09ecfd..0000000 --- a/src/main/java/com/machinezoo/hookless/package-info.java +++ /dev/null @@ -1,19 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -/* - * Diagnostic functions supported by all reactive objects: - * - Null check is performed on method parameters where appropriate. - * - Exceptions from application-defined methods (especially equals()) are tolerated and always handled in some way. - * - If the reactive object has no way to propagate exceptions, it will log them. There's no other logging by default. - * - Metrics are exposed only by reactive objects that generate events, i.e. the ones that sit at the bottom of the stack. - * - Opentracing spans are created only where absolutely necessary, i.e. in ReactiveVariable and ReactiveTrigger. - * - Object's OwnerTrace has at least an alias. Identifying parameters of the object are added as tags. - * - Child reactive objects have their OwnerTrace parent set. - * - Method toString() is defined. It uses OwnerTrace.toString(). - * - Method toString() may create reactive dependencies, which may result in harmless phantom dependencies during debugging. - */ -/** - * Reactive primitives and core reactive classes. - * - * @see Hookless website - */ -package com.machinezoo.hookless; diff --git a/src/main/java/com/machinezoo/hookless/time/AlarmIndex.java b/src/main/java/com/machinezoo/hookless/time/AlarmIndex.java deleted file mode 100644 index 9817cef..0000000 --- a/src/main/java/com/machinezoo/hookless/time/AlarmIndex.java +++ /dev/null @@ -1,80 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.time; - -import static java.util.stream.Collectors.*; -import java.time.*; -import java.util.*; - -/* - * Helper to let us easily find all active ReactiveAlarm instances with one of their bounds inside some time range. - * Locking is done on AlarmScheduler level. - */ -class AlarmIndex { - /* - * TreeMap lets us find times in a range. At this level, we don't care that alarm consists of two times. - * There can be more than one alarm associated with time, so we store them in a set. - * Alarms are stored as weak references, so that this index doesn't prevent GC. - */ - private NavigableMap> sorted = new TreeMap<>(); - /* - * Since we have weak values, these values can randomly disappear as GC collects them. - * We need to periodically purge the tree of all entries with zero values. - * We could have used specialized data structure from a library, - * but I couldn't quickly find one and regular purging is simple enough. - */ - private int purgeAt = 1; - private void purge() { - if (sorted.size() >= purgeAt) { - List purged = sorted.entrySet().stream() - .filter(e -> e.getValue().isEmpty()) - .map(e -> e.getKey()) - .collect(toList()); - for (Instant time : purged) - sorted.remove(time); - purgeAt = 2 * sorted.size() + 1; - } - } - /* - * Alarm is always added and removed whole with both lower and upper bound. - */ - void add(ReactiveAlarm alarm) { - if (alarm.lower != null) - add(alarm.lower, alarm); - if (alarm.upper != null) - add(alarm.upper, alarm); - purge(); - } - private void add(Instant time, ReactiveAlarm alarm) { - Set alarms = sorted.get(time); - if (alarms == null) { - /* - * Weak set to allow GC to collect the alarms. - */ - sorted.put(time, alarms = Collections.newSetFromMap(new WeakHashMap())); - } - alarms.add(alarm); - } - void remove(ReactiveAlarm alarm) { - if (alarm.lower != null) - remove(alarm.lower, alarm); - if (alarm.upper != null) - remove(alarm.upper, alarm); - } - private void remove(Instant time, ReactiveAlarm alarm) { - Set alarms = sorted.get(time); - if (alarms != null) { - alarms.remove(alarm); - if (alarms.isEmpty()) - sorted.remove(time); - } - } - SortedSet sorted() { - return sorted.navigableKeySet(); - } - List at(Instant time) { - Set alarms = sorted.get(time); - if (alarms == null) - return Collections.emptyList(); - return new ArrayList<>(alarms); - } -} diff --git a/src/main/java/com/machinezoo/hookless/time/AlarmScheduler.java b/src/main/java/com/machinezoo/hookless/time/AlarmScheduler.java deleted file mode 100644 index ebf847e..0000000 --- a/src/main/java/com/machinezoo/hookless/time/AlarmScheduler.java +++ /dev/null @@ -1,106 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.time; - -import java.time.*; -import java.util.*; -import java.util.concurrent.*; - -class AlarmScheduler { - private Instant now = Instant.now(); - private AlarmIndex index = new AlarmIndex(); - private ScheduledFuture future; - private Instant schedule = Instant.MAX; - static final AlarmScheduler instance = new AlarmScheduler(); - private static final Duration poll = Duration.ofSeconds(1); - private static final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1, new ThreadFactory() { - @Override - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(runnable); - thread.setDaemon(true); - thread.setName("hookless-timing"); - return thread; - } - }); - synchronized void monitor(ReactiveAlarm alarm, ReactiveAlarm previous) { - if (alarm.lower == null && alarm.upper == null || alarm.lower != null && alarm.upper != null && alarm.lower.isAfter(alarm.upper)) - throw new IllegalArgumentException(); - /* - * Update 'now' before performing calculations below. - */ - tick(); - if (previous != null) - index.remove(previous); - /* - * If the current time is already outside of the alarm's bounds, signal it now. - * Don't allow invalid alarms in the index. They might be never signaled. - */ - if (alarm.lower != null && !reached(alarm.lower) || alarm.upper != null && reached(alarm.upper)) - alarm.ring(); - else { - index.add(alarm); - reschedule(); - } - } - private boolean reached(Instant time) { - return !now.isBefore(time); - } - private void tick() { - Instant fresh = Instant.now(); - int direction = fresh.compareTo(now); - if (direction == 0) - return; - /* - * All indexed alarms have now in range [lower,upper). - * We want to invalidate alarms that don't have fresh in [lower,upper). - * Consider two cases: - * 1. fresh > now: - * We want to invalidate alarms with fresh in range [upper,inf). - * That means upper is in range (now,fresh]. - * 2. fresh < now: - * We want to invalidate alarms with fresh in range (inf,lower). - * That means lower is in range (fresh,now]. - * Since SortedSet performs lookups in ranges like [from,to), - * we have to modify the above ranges to [now+1,fresh+1) and [fresh+1,now+1) respectively. - */ - Instant now1 = now.plusNanos(1); - Instant fresh1 = fresh.plusNanos(1); - List range = new ArrayList<>(direction > 0 ? index.sorted().subSet(now1, fresh1) : index.sorted().subSet(fresh1, now1)); - for (Instant time : range) { - for (ReactiveAlarm alarm : index.at(time)) { - index.remove(alarm); - alarm.ring(); - } - } - now = fresh; - } - private void reschedule() { - /* - * We want to run when the next alarm is invalidated, i.e. when its upper bound is reached. - * We therefore want to pick the first indexed time in range (now,inf) or [now+1,inf). - */ - SortedSet pending = index.sorted().tailSet(now.plusNanos(1)); - Instant target; - if (pending.isEmpty()) { - target = now.plus(poll); - } else { - Instant first = pending.first(); - if (Duration.between(now, first).compareTo(poll) > 0) - target = now.plus(poll); - else - target = first; - } - if (future != null) { - if (target.plusMillis(1).compareTo(schedule) >= 0) - return; - future.cancel(false); - future = null; - } - schedule = target; - future = executor.schedule(this::expire, Math.max(1, Duration.between(now, schedule).toMillis()), TimeUnit.MILLISECONDS); - } - private synchronized void expire() { - future = null; - tick(); - reschedule(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/time/GrowingReactiveDuration.java b/src/main/java/com/machinezoo/hookless/time/GrowingReactiveDuration.java deleted file mode 100644 index 4b9b9b6..0000000 --- a/src/main/java/com/machinezoo/hookless/time/GrowingReactiveDuration.java +++ /dev/null @@ -1,142 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.time; - -import java.math.*; -import java.time.*; -import java.time.temporal.*; -import com.machinezoo.stagean.*; - -/** - * Reactive version of {@link Duration}, positive (growing) variant. - */ -@DraftApi("requires review") -@DraftCode("requires review") -@NoTests -@StubDocs -public class GrowingReactiveDuration extends ReactiveDuration implements Comparable { - GrowingReactiveDuration(ReactiveClock clock, Instant zero) { - super(clock, zero); - } - @Override - public int compareTo(GrowingReactiveDuration other) { - clock.checkSame(other.clock); - return other.zero.compareTo(zero); - } - @Override - public int compareTo(Duration duration) { - return clock.compareTo(zero.plus(duration)); - } - @Override - public boolean isPositive() { - return clock.isAfter(zero); - } - @Override - public boolean isNegative() { - return clock.isBefore(zero); - } - @Override - public boolean isZero() { - return clock.isAt(zero); - } - @Override - public GrowingReactiveDuration plus(Duration duration) { - return new GrowingReactiveDuration(clock, zero.minus(duration)); - } - public Duration plus(ShrinkingReactiveDuration other) { - return Duration.between(zero, other.zero); - } - @Override - public GrowingReactiveDuration plus(long amount, TemporalUnit unit) { - return plus(Duration.of(amount, unit)); - } - @Override - public GrowingReactiveDuration plusDays(long days) { - return plus(Duration.ofDays(days)); - } - @Override - public GrowingReactiveDuration plusHours(long hours) { - return plus(Duration.ofHours(hours)); - } - @Override - public GrowingReactiveDuration plusMinutes(long minutes) { - return plus(Duration.ofMinutes(minutes)); - } - @Override - public GrowingReactiveDuration plusSeconds(long seconds) { - return plus(Duration.ofSeconds(seconds)); - } - @Override - public GrowingReactiveDuration plusMillis(long millis) { - return plus(Duration.ofMillis(millis)); - } - @Override - public GrowingReactiveDuration plusNanos(long nanos) { - return plus(Duration.ofNanos(nanos)); - } - @Override - public GrowingReactiveDuration minus(Duration duration) { - return plus(duration.negated()); - } - public Duration minus(GrowingReactiveDuration other) { - return Duration.between(zero, other.zero); - } - @Override - public GrowingReactiveDuration minus(long amount, TemporalUnit unit) { - return minus(Duration.of(amount, unit)); - } - @Override - public GrowingReactiveDuration minusDays(long days) { - return minus(Duration.ofDays(days)); - } - @Override - public GrowingReactiveDuration minusHours(long hours) { - return minus(Duration.ofHours(hours)); - } - @Override - public GrowingReactiveDuration minusMinutes(long minutes) { - return minus(Duration.ofMinutes(minutes)); - } - @Override - public GrowingReactiveDuration minusSeconds(long seconds) { - return minus(Duration.ofSeconds(seconds)); - } - @Override - public GrowingReactiveDuration minusMillis(long millis) { - return minus(Duration.ofMillis(millis)); - } - @Override - public GrowingReactiveDuration minusNanos(long nanos) { - return minus(Duration.ofNanos(nanos)); - } - @Override - public ShrinkingReactiveDuration negated() { - return new ShrinkingReactiveDuration(clock, zero); - } - @Override - public Duration truncatedTo(Duration unit) { - if (unit.isNegative() || unit.isZero()) - throw new IllegalArgumentException("Can only truncate with positive unit"); - Duration duration = Duration.between(zero, clock.instant()); - BigInteger bigUnit = big(unit); - Duration truncated = unbig(big(duration).divide(bigUnit).multiply(bigUnit)); - Instant constraint = zero.plus(truncated); - if (!duration.isNegative()) { - clock.constrainLeftClosed(constraint); - clock.constrainRightOpen(constraint.plus(unit)); - } else { - clock.constrainLeftOpen(constraint); - clock.constrainRightClosed(constraint.minus(unit)); - } - return truncated; - } - public Instant subtractFrom(ReactiveInstant instant) { - return instant.minus(this); - } - public ReactiveInstant addTo(Instant instant) { - return new ReactiveInstant(clock, Duration.between(zero, instant)); - } - @Override - public String toString() { - return "now - " + zero.toString(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/time/ReactiveAlarm.java b/src/main/java/com/machinezoo/hookless/time/ReactiveAlarm.java deleted file mode 100644 index 1e91f3e..0000000 --- a/src/main/java/com/machinezoo/hookless/time/ReactiveAlarm.java +++ /dev/null @@ -1,41 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.time; - -import java.time.*; - -/* - * Alarm is an immutable version of ReactiveClock for indexing in AlarmIndex. - * ReactiveClock must hold a reference to current ReactiveAlarm in order to protect it from GC. - */ -class ReactiveAlarm { - /* - * Alarm's range of valid times is half-closed: [lower, upper). - */ - final Instant lower; - final Instant upper; - private final ReactiveClock clock; - ReactiveAlarm(Instant lower, Instant upper, ReactiveClock clock) { - this.lower = lower; - this.upper = upper; - this.clock = clock; - } - void ring() { - clock.ring(); - } - ReactiveAlarm constrainUpper(Instant time) { - if (upper == null || time.isBefore(upper)) - return new ReactiveAlarm(lower, time, clock); - else - return this; - } - ReactiveAlarm constrainLower(Instant time) { - if (lower == null || time.isAfter(lower)) - return new ReactiveAlarm(time, upper, clock); - else - return this; - } - @Override - public String toString() { - return "[" + (lower != null ? lower.toString() : "infinity") + ", " + (upper != null ? upper.toString() : "infinity") + ")"; - } -} diff --git a/src/main/java/com/machinezoo/hookless/time/ReactiveClock.java b/src/main/java/com/machinezoo/hookless/time/ReactiveClock.java deleted file mode 100644 index ecd60f4..0000000 --- a/src/main/java/com/machinezoo/hookless/time/ReactiveClock.java +++ /dev/null @@ -1,127 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.time; - -import java.time.*; -import java.util.concurrent.*; -import com.machinezoo.hookless.*; -import com.machinezoo.hookless.util.*; - -class ReactiveClock implements Comparable { - private final Instant now = Instant.now(); - /* - * Lifetime of reactive clock is a bit tricky. Clock obviously stays alive during reactive computation that created it. - * This is ensured by having clock instance frozen in current reactive computation. - * After that, reactive clock should stay around as long as its reactive variable is referenced from some trigger. - * Once there is no such reference, there is no one to notify when the clock rings and therefore no need to keep it alive. - * We will use keepalive feature in reactive variable to ensure there's always a strong reference pointing at the clock. - */ - private final ReactiveVariable version = OwnerTrace - .of(new ReactiveVariable() - .keepalive(this)) - .parent(this) - .target(); - private ReactiveAlarm alarm = new ReactiveAlarm(null, null, this); - private ReactiveClock() { - OwnerTrace.of(this) - .alias("clock") - .tag("freeze", now); - } - static ReactiveClock get() { - /* - * Don't pin. Freeze. Pinning of reactive time is unsafe. - * It also causes time to behave oddly if lengthy blocking makes the pins long-lived. - */ - return CurrentReactiveScope.freeze(ClockKey.instance, ReactiveClock::new); - } - ReactiveInstant now() { - return new ReactiveInstant(this, Duration.ZERO); - } - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof ReactiveClock)) - return false; - return ((ReactiveClock)obj).now.equals(now); - } - @Override - public int hashCode() { - return now.hashCode(); - } - @Override - public int compareTo(Instant time) { - constrain(time); - if (now.isAfter(time)) - return 1; - constrain(time.plusNanos(1)); - if (now.isBefore(time)) - return -1; - return 0; - } - boolean isBefore(Instant time) { - constrain(time); - return now.isBefore(time); - } - boolean isAfter(Instant time) { - constrain(time.plusNanos(1)); - return now.isAfter(time); - } - boolean isAt(Instant time) { - return compareTo(time) == 0; - } - Instant instant() { - return now; - } - void checkSame(ReactiveClock other) { - if (this != other) - throw new IllegalArgumentException("Cannot mix different instances of " + ReactiveClock.class.getSimpleName()); - } - void ring() { - version.set(new Object()); - } - void constrain(Instant time) { - ReactiveAlarm previous = alarm; - if (time.isAfter(now)) - alarm = previous.constrainUpper(time); - else - alarm = previous.constrainLower(time); - if (alarm != previous) { - /* - * Read the reactive variable before invoking AlarmScheduler, which may invalidate the variable immediately. - */ - version.get(); - AlarmScheduler.instance.monitor(alarm, previous); - } - } - void constrainLeftClosed(Instant time) { - constrain(time); - } - void constrainRightOpen(Instant time) { - constrain(time); - } - void constrainLeftOpen(Instant time) { - constrain(time.plusNanos(1)); - } - void constrainRightClosed(Instant time) { - constrain(time.plusNanos(1)); - } - private static class ClockKey { - /* - * This is preferable to using ReactiveClock.class, - * because - */ - static ClockKey instance = new ClockKey(); - /* - * Pregenerated hash code speeds up lookups in the pinned object map. - */ - final int hashCode = ThreadLocalRandom.current().nextInt(); - @Override - public int hashCode() { - return hashCode; - } - } - @Override - public String toString() { - return OwnerTrace.of(this) + " = " + alarm; - } -} diff --git a/src/main/java/com/machinezoo/hookless/time/ReactiveDuration.java b/src/main/java/com/machinezoo/hookless/time/ReactiveDuration.java deleted file mode 100644 index eb9304a..0000000 --- a/src/main/java/com/machinezoo/hookless/time/ReactiveDuration.java +++ /dev/null @@ -1,106 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.time; - -import java.math.*; -import java.time.*; -import java.time.temporal.*; -import com.machinezoo.stagean.*; - -/** - * Reactive version of {@link Duration}. - */ -@DraftApi("requires review") -@DraftCode("requires review") -@NoTests -@StubDocs -public abstract class ReactiveDuration { - final ReactiveClock clock; - final Instant zero; - ReactiveDuration(ReactiveClock clock, Instant zero) { - this.clock = clock; - this.zero = zero; - } - public abstract int compareTo(Duration duration); - public abstract boolean isNegative(); - public abstract boolean isPositive(); - public abstract boolean isZero(); - public abstract ReactiveDuration plus(Duration duration); - public abstract ReactiveDuration plus(long amount, TemporalUnit unit); - public abstract ReactiveDuration plusDays(long days); - public abstract ReactiveDuration plusHours(long hours); - public abstract ReactiveDuration plusMinutes(long minutes); - public abstract ReactiveDuration plusSeconds(long seconds); - public abstract ReactiveDuration plusMillis(long millis); - public abstract ReactiveDuration plusNanos(long nanos); - public abstract ReactiveDuration minus(Duration duration); - public abstract ReactiveDuration minus(long amount, TemporalUnit unit); - public abstract ReactiveDuration minusDays(long days); - public abstract ReactiveDuration minusHours(long hours); - public abstract ReactiveDuration minusMinutes(long minutes); - public abstract ReactiveDuration minusSeconds(long seconds); - public abstract ReactiveDuration minusMillis(long millis); - public abstract ReactiveDuration minusNanos(long nanos); - public abstract ReactiveDuration negated(); - public abstract Duration truncatedTo(Duration unit); - public static Duration between(ReactiveInstant start, ReactiveInstant end) { - start.clock.checkSame(end.clock); - return end.shift.minus(start.shift); - } - public static ShrinkingReactiveDuration between(ReactiveInstant start, Instant end) { - return new ShrinkingReactiveDuration(start.clock, end.minus(start.shift)); - } - public static GrowingReactiveDuration between(Instant start, ReactiveInstant end) { - return new GrowingReactiveDuration(end.clock, start.minus(end.shift)); - } - public static Duration between(Instant start, Instant end) { - return Duration.between(start, end); - } - @Override - public boolean equals(Object obj) { - if (!(obj instanceof ReactiveDuration)) - return false; - ReactiveDuration other = (ReactiveDuration)obj; - if ((this instanceof GrowingReactiveDuration) != (other instanceof GrowingReactiveDuration)) - return false; - clock.checkSame(other.clock); - return zero.equals(other.zero); - } - @Override - public int hashCode() { - return zero.hashCode(); - } - public Duration truncatedTo(TemporalUnit unit) { - return truncatedTo(unit.getDuration()); - } - public long toUnits(Duration unit) { - BigInteger big = big(truncatedTo(unit)).divide(big(unit)); - if (big.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 || big.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) - throw new ArithmeticException(); - return big.longValue(); - } - public long toUnits(TemporalUnit unit) { - return toUnits(unit.getDuration()); - } - public long toDays() { - return toUnits(ChronoUnit.DAYS); - } - public long toHours() { - return toUnits(ChronoUnit.HOURS); - } - public long toMinutes() { - return toUnits(ChronoUnit.MINUTES); - } - public long getSeconds() { - return toUnits(ChronoUnit.SECONDS); - } - public long toMillis() { - return toUnits(ChronoUnit.MILLIS); - } - static BigInteger big(Duration duration) { - return BigInteger.valueOf(duration.getSeconds()).multiply(BigInteger.valueOf(1_000_000_000)).add(BigInteger.valueOf(duration.getNano())); - } - static Duration unbig(BigInteger big) { - BigInteger[] divrem = big.divideAndRemainder(BigInteger.valueOf(1_000_000_000)); - return Duration.ofSeconds(divrem[0].longValue(), divrem[1].longValue()); - } -} diff --git a/src/main/java/com/machinezoo/hookless/time/ReactiveInstant.java b/src/main/java/com/machinezoo/hookless/time/ReactiveInstant.java deleted file mode 100644 index 1aee1f1..0000000 --- a/src/main/java/com/machinezoo/hookless/time/ReactiveInstant.java +++ /dev/null @@ -1,149 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.time; - -import java.math.*; -import java.time.*; -import java.time.temporal.*; -import com.machinezoo.stagean.*; - -/** - * Reactive version of {@link Instant}. - */ -@DraftApi("requires review") -@DraftCode("requires review") -@NoTests -@StubDocs -public class ReactiveInstant implements Comparable { - final ReactiveClock clock; - final Duration shift; - ReactiveInstant(ReactiveClock clock, Duration shift) { - this.clock = clock; - this.shift = shift; - } - public static ReactiveInstant now() { - return ReactiveClock.get().now(); - } - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - else if (obj instanceof ReactiveInstant) { - ReactiveInstant other = (ReactiveInstant)obj; - clock.checkSame(other.clock); - return shift.equals(other.shift); - } else if (obj instanceof Instant) - return compareTo((Instant)obj) == 0; - else - return false; - } - @Override - public int hashCode() { - return shift.hashCode(); - } - @Override - public int compareTo(ReactiveInstant other) { - clock.checkSame(other.clock); - return shift.compareTo(other.shift); - } - public int compareTo(Instant instant) { - return clock.compareTo(instant.minus(shift)); - } - public boolean isAfter(ReactiveInstant other) { - clock.checkSame(other.clock); - return shift.compareTo(other.shift) > 0; - } - public boolean isAfter(Instant instant) { - return clock.isAfter(instant.minus(shift)); - } - public boolean isBefore(ReactiveInstant other) { - clock.checkSame(other.clock); - return shift.compareTo(other.shift) < 0; - } - public boolean isBefore(Instant instant) { - return clock.isBefore(instant.minus(shift)); - } - public Instant plus(ShrinkingReactiveDuration duration) { - clock.checkSame(duration.clock); - return duration.zero.plus(shift); - } - public ReactiveInstant plus(Duration duration) { - return new ReactiveInstant(clock, shift.plus(duration)); - } - public ReactiveInstant plus(long amount, TemporalUnit unit) { - return plus(Duration.of(amount, unit)); - } - public ReactiveInstant plusSeconds(long seconds) { - return plus(Duration.ofSeconds(seconds)); - } - public ReactiveInstant plusMillis(long millis) { - return plus(Duration.ofMillis(millis)); - } - public ReactiveInstant plusNanos(long nanos) { - return plus(Duration.ofNanos(nanos)); - } - public Instant minus(GrowingReactiveDuration duration) { - clock.checkSame(duration.clock); - return duration.zero.plus(shift); - } - public ReactiveInstant minus(Duration duration) { - return new ReactiveInstant(clock, shift.minus(duration)); - } - public ReactiveInstant minus(long amount, TemporalUnit unit) { - return minus(Duration.of(amount, unit)); - } - public ReactiveInstant minusSeconds(long seconds) { - return minus(Duration.ofSeconds(seconds)); - } - public ReactiveInstant minusMillis(long millis) { - return minus(Duration.ofMillis(millis)); - } - public ReactiveInstant minusNanos(long nanos) { - return minus(Duration.ofNanos(nanos)); - } - public Instant truncatedTo(Duration unit) { - if (unit.isNegative() || unit.isZero()) - throw new IllegalArgumentException("Can only truncate with positive unit"); - if (unit.compareTo(Duration.ofDays(1)) > 0) - throw new IllegalArgumentException("Cannot truncate with unit longer that one day"); - long nanoUnit = unit.toNanos(); - if (Duration.ofDays(1).toNanos() % nanoUnit != 0) - throw new IllegalArgumentException("Can only truncate with unit that divides day"); - Instant instant = clock.instant().plus(shift); - Instant days = instant.truncatedTo(ChronoUnit.DAYS); - long nanoTime = Duration.between(days, instant).toNanos(); - Instant truncated = days.plus(Duration.ofNanos(nanoTime / nanoUnit * nanoUnit)); - Instant constraint = truncated.minus(shift); - clock.constrainLeftClosed(constraint); - clock.constrainRightOpen(constraint.plus(unit)); - return truncated; - } - public Instant truncatedTo(TemporalUnit unit) { - return truncatedTo(unit.getDuration()); - } - public long getEpochSecond() { - return truncatedTo(ChronoUnit.SECONDS).getEpochSecond(); - } - public long toEpochMilli() { - return truncatedTo(ChronoUnit.MILLIS).toEpochMilli(); - } - public long until(Instant end, Duration unit) { - return ReactiveDuration.between(this, end).toUnits(unit); - } - public long until(Instant end, TemporalUnit unit) { - return ReactiveDuration.between(this, end).toUnits(unit); - } - public long until(ReactiveInstant end, Duration unit) { - Duration duration = ReactiveDuration.between(this, end); - BigInteger big = ReactiveDuration.big(duration).divide(ReactiveDuration.big(unit)); - if (big.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 || big.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) - throw new ArithmeticException(); - return big.longValue(); - } - public long until(ReactiveInstant end, TemporalUnit unit) { - return until(end, unit.getDuration()); - } - @Override - public String toString() { - return "now + " + shift.toString(); - } -} diff --git a/src/main/java/com/machinezoo/hookless/time/ShrinkingReactiveDuration.java b/src/main/java/com/machinezoo/hookless/time/ShrinkingReactiveDuration.java deleted file mode 100644 index cfd0288..0000000 --- a/src/main/java/com/machinezoo/hookless/time/ShrinkingReactiveDuration.java +++ /dev/null @@ -1,142 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.time; - -import java.math.*; -import java.time.*; -import java.time.temporal.*; -import com.machinezoo.stagean.*; - -/** - * Reactive version of {@link Duration}, negative (shrinking) variant. - */ -@DraftApi("requires review") -@DraftCode("requires review") -@NoTests -@StubDocs -public class ShrinkingReactiveDuration extends ReactiveDuration implements Comparable { - ShrinkingReactiveDuration(ReactiveClock clock, Instant zero) { - super(clock, zero); - } - @Override - public int compareTo(ShrinkingReactiveDuration other) { - clock.checkSame(other.clock); - return zero.compareTo(other.zero); - } - @Override - public int compareTo(Duration duration) { - return -clock.compareTo(zero.minus(duration)); - } - @Override - public boolean isPositive() { - return clock.isBefore(zero); - } - @Override - public boolean isNegative() { - return clock.isAfter(zero); - } - @Override - public boolean isZero() { - return clock.isAt(zero); - } - @Override - public ShrinkingReactiveDuration plus(Duration duration) { - return new ShrinkingReactiveDuration(clock, zero.plus(duration)); - } - public Duration plus(GrowingReactiveDuration other) { - return Duration.between(other.zero, zero); - } - @Override - public ShrinkingReactiveDuration plus(long amount, TemporalUnit unit) { - return plus(Duration.of(amount, unit)); - } - @Override - public ShrinkingReactiveDuration plusDays(long days) { - return plus(Duration.ofDays(days)); - } - @Override - public ShrinkingReactiveDuration plusHours(long hours) { - return plus(Duration.ofHours(hours)); - } - @Override - public ShrinkingReactiveDuration plusMinutes(long minutes) { - return plus(Duration.ofMinutes(minutes)); - } - @Override - public ShrinkingReactiveDuration plusSeconds(long seconds) { - return plus(Duration.ofSeconds(seconds)); - } - @Override - public ShrinkingReactiveDuration plusMillis(long millis) { - return plus(Duration.ofMillis(millis)); - } - @Override - public ShrinkingReactiveDuration plusNanos(long nanos) { - return plus(Duration.ofNanos(nanos)); - } - @Override - public ShrinkingReactiveDuration minus(Duration duration) { - return plus(duration.negated()); - } - public Duration minus(ShrinkingReactiveDuration other) { - return Duration.between(other.zero, zero); - } - @Override - public ShrinkingReactiveDuration minus(long amount, TemporalUnit unit) { - return minus(Duration.of(amount, unit)); - } - @Override - public ShrinkingReactiveDuration minusDays(long days) { - return minus(Duration.ofDays(days)); - } - @Override - public ShrinkingReactiveDuration minusHours(long hours) { - return minus(Duration.ofHours(hours)); - } - @Override - public ShrinkingReactiveDuration minusMinutes(long minutes) { - return minus(Duration.ofMinutes(minutes)); - } - @Override - public ShrinkingReactiveDuration minusSeconds(long seconds) { - return minus(Duration.ofSeconds(seconds)); - } - @Override - public ShrinkingReactiveDuration minusMillis(long millis) { - return minus(Duration.ofMillis(millis)); - } - @Override - public ShrinkingReactiveDuration minusNanos(long nanos) { - return minus(Duration.ofNanos(nanos)); - } - @Override - public GrowingReactiveDuration negated() { - return new GrowingReactiveDuration(clock, zero); - } - @Override - public Duration truncatedTo(Duration unit) { - if (unit.isNegative() || unit.isZero()) - throw new IllegalArgumentException("Can only truncate with positive unit"); - Duration duration = Duration.between(clock.instant(), zero); - BigInteger bigUnit = big(unit); - Duration truncated = unbig(big(duration).divide(bigUnit).multiply(bigUnit)); - Instant constraint = zero.minus(truncated); - if (!duration.isNegative()) { - clock.constrainLeftOpen(constraint.minus(unit)); - clock.constrainRightClosed(constraint); - } else { - clock.constrainLeftClosed(constraint); - clock.constrainRightOpen(constraint.plus(unit)); - } - return truncated; - } - public Instant addTo(ReactiveInstant instant) { - return instant.plus(this); - } - public ReactiveInstant subtractFrom(Instant instant) { - return new ReactiveInstant(clock, Duration.between(zero, instant)); - } - @Override - public String toString() { - return zero.toString() + " - now"; - } -} diff --git a/src/main/java/com/machinezoo/hookless/time/package-info.java b/src/main/java/com/machinezoo/hookless/time/package-info.java deleted file mode 100644 index c46a880..0000000 --- a/src/main/java/com/machinezoo/hookless/time/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -/** - * Reactive versions of classes from {@link java.time}. - */ -@com.machinezoo.stagean.DraftDocs("link to tutorial page") -package com.machinezoo.hookless.time; diff --git a/src/main/java/com/machinezoo/hookless/util/ConsistentRandom.java b/src/main/java/com/machinezoo/hookless/util/ConsistentRandom.java deleted file mode 100644 index 93be760..0000000 --- a/src/main/java/com/machinezoo/hookless/util/ConsistentRandom.java +++ /dev/null @@ -1,34 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.util; - -import java.util.*; -import com.machinezoo.stagean.*; - -/* - * It is desirable for reactive computations to produce the same output for the same state of their dependencies. - * Since random number generators are an invisible variable input to the computation, they break output consistency. - * This small utility class offers consistent source of randomness that depends only on specified parameters. - * - * It is named "consistent" rather than "reproducible" random, because it only needs to be consistent in one process. - * Reproducibility would imply repeatability of persistent program output, which would require not only seed consistency - * but also algorithm consistency, which is complicated to implement and likely less performant than native RNG. - */ -/** - * Provider of {@link Random} instances that return the same values for the same seeding key. - */ -@NoTests -@StubDocs -public class ConsistentRandom { - /* - * We will return plain Random for now. We could extend the functionality later just like ThreadLocalRandom does. - * - * Application is responsible for providing hashable keys that are non-random - * and yet unique enough to make consistent RNG produce different output in different contexts. - */ - public static Random of(Object... keys) { - /* - * Use Arrays.deepHashCode() instead of Objects.hash(), so that callers can pass in whole arrays. - */ - return new Random(Arrays.deepHashCode(keys)); - } -} diff --git a/src/main/java/com/machinezoo/hookless/util/OwnerTrace.java b/src/main/java/com/machinezoo/hookless/util/OwnerTrace.java deleted file mode 100644 index 68f7aa1..0000000 --- a/src/main/java/com/machinezoo/hookless/util/OwnerTrace.java +++ /dev/null @@ -1,274 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.util; - -import static java.util.stream.Collectors.*; -import java.util.*; -import java.util.concurrent.atomic.*; -import com.google.common.cache.*; -import com.machinezoo.stagean.*; -import io.opentracing.*; -import it.unimi.dsi.fastutil.objects.*; - -/* - * Opentracing doesn't work that well for reactive code, because reactive code inverts call stacks. - * If child object decides that the event doesn't have to be propagated to the parent, - * then information about the parent (in the form of tags) is omitted from the trace. - * Such headless traces without parent information are often meaningless. - * - * This is a non-standard extension to opentracing that models ownership (parent-child) relationships - * and allows child objects to include tags of all their ancestors in child spans. - * This makes traces understandable even if child object doesn't propagate the event to its parent. - * - * Some objects are short-lived and we should probably model them as spans rather than span owners. - * This is particularly true for sequences of blocking reactive computations. - * However, if we model such blocking computation sequences as spans, - * these overarching spans will be composed of smaller spans representing individual computations - * and these smaller spans will then have two parents: the overarching span (from above) and the span - * that woke up the reactive object and triggered the new computation (from below). - * It is not clear how well does opentracing and its implementations support multiple parents, - * especially when the wakeup span belongs to a different trace. - * This is why we currently don't try to represent short-lived objects as spans. - * It is nevertheless possible to add this feature in the future. - * - * But even with current implementation, seemingly independent events often end up in one trace. - * This is because blocking operations (like database reads) are expected to - * link the span that caused blocking to the span that represents end of the blocking operation. - * This only breaks down when two or more reactive computations trigger the same blocking operation. - * In that case only the first operation will have complete trace recorded. - * - * Tags and ownership information added here is also used to construct informative toString() methods. - * - * We will use volatile and final fields extensively to avoid expensive locking. - * There are also many other optimizations, to keep the cost of owner tracing to minimum. - */ -/** - * Trace of object ancestors for easier debugging and tracing. - */ -@NoTests -@StubDocs -@DraftApi("should be in a separate library") -public class OwnerTrace { - /* - * We don't want to force every class to carry OwnerTrace as a member. - * Many classes cannot even expose any member, for example when they are exposed only through some interface. - * We will instead associate tracing information to any object via a map with weak keys. - * - * WeakHashMap would be the simplest solution, but it uses hashCode/equals, - * which results in surprising behavior when owner trace information is added to collections. - * We will instead use Guava's CacheBuilder that automatically uses System.identityHashCode() - * and reference equality when weakKeys() is specified. It is also automatically synchronized. - * The downside is that Guava cache is likely quite inefficient for the simple use case we need. - * - * Use of weak map has the unpleasant consequence that we cannot hold reference back to the target object, - * because the map holds hard reference to its values even if the key is no longer referenced by anything. - * If we kept a hard reference to the target object, which is used as a key in the map, - * we would prevent collection of our own map entry and create a memory leak. - * Fortunately, reference to the target object is only needed in our builder API. - * So we reserve OwnerTrace itself as our builder API and use private object as a value in the weak map. - * This will cost us an extra object allocation every time we access OwnerTrace. - */ - private static LoadingCache all = CacheBuilder.newBuilder() - .weakKeys() - .build(CacheLoader.from(OwnerTraceData::new)); - public static OwnerTrace of(T target) { - return new OwnerTrace(target, all.getUnchecked(target)); - } - private final T target; - public T target() { - return target; - } - private final OwnerTraceData data; - private OwnerTrace(T target, OwnerTraceData data) { - Objects.requireNonNull(target); - this.target = target; - this.data = data; - } - private static class OwnerTraceData { - /* - * All fields are actually stored here since the outer class is a short-lived builder. - * We will use volatile fields to avoid expensive locking when the data is accessed. - */ - volatile String alias; - volatile OwnerTag tags; - volatile OwnerTraceData parent; - /* - * We could store classname (the default for alias) in separate field, - * but it's much simpler to write it directly into the alias field as the default value. - */ - OwnerTraceData(Object target) { - /* - * Static objects are likely to have class as their parent, but we don't want just "Class" as an alias. - * Class name is taken instead as an alias in that case. Since class name is used as default alias for instances too, - * classes used in ownership chain as both instances and static classes will have to explicitly specify aliases. - */ - if (target instanceof Class) - alias = ((Class)target).getSimpleName(); - else - alias = target.getClass().getSimpleName(); - } - } - /* - * Aliasing allows using neat short namespaces for ancestor tags instead of the lengthy class name. - */ - public OwnerTrace alias(String alias) { - data.alias = alias; - return this; - } - /* - * All tags are explicitly set. On-demand computed tags are rarely useful and they are dangerous and inefficient. - * - * Linked list of (mostly) immutable tag structures seems to be the fastest solution for short tag lists. - * Its downside is that it complicates updating tag value, but then we mostly care about read speed. - */ - private static class OwnerTag { - final String key; - /* - * This will require type-testing and casting when tagging the tracing span. - * Maybe there is a way to optimize that casting by having multiple typed fields here, - * but I cannot know without profiling. - */ - volatile Object value; - final OwnerTag next; - OwnerTag(String key, Object value, OwnerTag next) { - this.key = key; - this.value = value; - this.next = next; - } - } - public OwnerTrace tag(String key, Object value) { - Objects.requireNonNull(key); - /* - * Silently ignore null tag values. This simplifies code that would otherwise have to perform null check. - */ - if (value != null) { - OwnerTag head = data.tags; - for (OwnerTag tag = head; tag != null; tag = tag.next) { - if (tag.key.equals(key)) { - tag.value = value; - return this; - } - } - data.tags = new OwnerTag(key, value, head); - } - return this; - } - /* - * Sometimes we cannot provide any good tags that would identify the object, - * but we still want to have a way to tell one instance from the other. - * We provide convenience ID generator here to allow for that. - * 64-bit counter will never overflow. - */ - private static final AtomicLong counter = new AtomicLong(); - public OwnerTrace generateId() { - return tag("id", counter.incrementAndGet()); - } - /* - * This is the key. We establish ownership hierarchy to make child scopes meaningful - * even if there's no parent scope in the trace. - */ - public OwnerTrace parent(Object parent) { - if (parent instanceof OwnerTrace) - data.parent = ((OwnerTrace)parent).data; - else if (parent == null) - data.parent = null; - else - data.parent = OwnerTrace.of(parent).data; - return this; - } - /* - * When using OwnerTrace in tracing spans and toString(), we have to namespace tags of different ancestors. - * By default, tag's namespace is identical to object's alias, but it can be numbered in case there are alias conflicts. - */ - private static class Namespace { - OwnerTraceData data; - /* - * May differ from from alias if there are name conflicts among ancestors. - */ - String name; - } - /* - * The linked list of ancestors we actually store only allows backward iteration of ancestor chain. - * We have to materialize the ancestor chain and reverse it in order to iterate it in forward direction. - * The materialized ancestor list is also necessary to create uniquely named namespaces. - * - * All this is relatively computationally expensive and unfortunately it has to run often. - * Unless we can find a way to execute it less often (e.g. by having a way to detect whether tracing is active), - * we will probably have to optimize this code in the future. - */ - private List namespaces() { - List namespaces = new ArrayList<>(); - for (OwnerTraceData ancestor = data; ancestor != null; ancestor = ancestor.parent) { - Namespace ns = new Namespace(); - ns.data = ancestor; - namespaces.add(ns); - } - Collections.reverse(namespaces); - Object2IntMap numbering = new Object2IntOpenHashMap<>(namespaces.size()); - for (Namespace ns : namespaces) { - /* - * Make a copy since the alias could be changed in another thread (unlikely but possible). - */ - String alias = ns.data.alias; - if (!numbering.containsKey(alias)) { - ns.name = alias; - numbering.put(alias, 2); - } else { - int number = numbering.getInt(alias); - ns.name = alias + number; - numbering.put(alias, number + 1); - } - } - return namespaces; - } - /* - * We can now use the constructed ownership hierarchy and tags to create informative tracing span. - * - * This is quite expensive operation. Ideally, we would like to skip it if the trace is going to be sampled out. - * There is unfortunately no way to detect disabled tracing, perhaps because sampling is done when trace is complete. - * We will have to be careful where we inject tracing. The operation burdened with tracing better be infrequent enough. - * In the future, we might wish to be able to disable tracing in all or part of hookless code for performance reasons. - */ - public Span fill(Span span) { - Objects.requireNonNull(span); - List namespaces = namespaces(); - span.setTag("owner", namespaces.stream().map(ns -> ns.name).collect(joining("."))); - for (Namespace ns : namespaces) { - for (OwnerTag tag = ns.data.tags; tag != null; tag = tag.next) { - String key = ns.name + "." + tag.key; - Object value = tag.value; - /* - * Opentracing API only takes certain types of variables, so cast appropriately. - * We will convert arbitrary objects via toString(). This is a bit dangerous, - * but callers are expected to be careful what are they setting as the tag value. - */ - if (value instanceof String) - span.setTag(key, (String)value); - else if (value instanceof Number) - span.setTag(key, (Number)value); - else if (value instanceof Boolean) - span.setTag(key, (boolean)value); - else - span.setTag(key, value.toString()); - } - } - return span; - } - /* - * This is used to implement toString() on the tagged object. - * The logic is the same as in fill() method. - */ - @Override - public String toString() { - /* - * Just materializing the complete tag map is the simplest implementation. - * This is not performance-critical code, so we aim for code simplicity rather than performance. - * We are constructing a TreeMap in order to force display in sorted order. - */ - Map sorted = new TreeMap<>(); - List namespaces = namespaces(); - for (Namespace ns : namespaces) - for (OwnerTag tag = ns.data.tags; tag != null; tag = tag.next) - sorted.put(ns.name + "." + tag.key, tag.value); - return namespaces.stream().map(ns -> ns.name).collect(joining(".")) + sorted; - } -} diff --git a/src/main/java/com/machinezoo/hookless/util/ReactiveCollections.java b/src/main/java/com/machinezoo/hookless/util/ReactiveCollections.java deleted file mode 100644 index 0495448..0000000 --- a/src/main/java/com/machinezoo/hookless/util/ReactiveCollections.java +++ /dev/null @@ -1,1100 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.util; - -import java.util.*; -import java.util.concurrent.*; -import com.machinezoo.hookless.*; -import com.machinezoo.stagean.*; - -/* - * We don't want to expose an ocean of new classes wrapping every kind of collection. - * We instead define wrapping methods, one for every type of collection, and let them take various options. - * This makes for a simple, concise API. - * - * There is no perfect way to define reactive collections. We try to offer good defaults - * and we allow configuration to cover cases that the defaults cannot. - * There will always be cases that cannot be covered and apps sometimes have to do explicit invalidation. - * - * We are not placing invalidation into finally block, because it is assumed - * that throwing write operations made no change to the collection. - * This might not be entirely true, but it should be true of any correctly implemented collection. - * - * Reactivity is not particularly compatible with read-write operations that collections expose, - * for example when set's add() returns true if the set was modified. - * Read-write operations create new dependency and subsequently invalidate that same dependency. - * That causes infinite re-running of the reactive computation that triggers such operation. - * We try to fight this phenomenon in several ways: - * - * 1. Invalidate only if progress is made. - * If exception is thrown, we can usually skip invalidation. If exception is not thrown, - * the return value can be often used to detect whether the collection was changed. - * If it was not changed, we can skip invalidation. - * Disallowing null values makes this check possible in more cases. - * Optionally, reactive collections can check for full equality instead of reference equality - * to better detect cases where no real progress is being made and invalidation can be skipped. - * - * 2. Avoid leaking collection state via return value and exceptions. - * Write methods often return status that makes them read-write even though callers usually ignore the status. - * Callers can disable (silence) the return value or instruct reactive collection to ignore it - * to avoid collecting unnecessary dependency. The same can be done for state-based exceptions. - * This makes most methods either read-only or write-only, avoiding the troubling read-write case. - * - * 3. Expect callers to indicate their intent. - * If callers wrap collection calls in ReactiveScope.ignore(), no dependency will be collected. - * Callers can do this to indicate that they are only interested in write part of the operation, not read. - * - * It seems attractive to add reactiveObject(T) and reactiveObjectTree(T) methods - * that would turn any POJO object or object tree into reactive data structure. - * That however involves a lot of complexity related to dynamically generating wrapper classes. - * We could use standard JRE Proxy class, restricting the feature to interfaces, - * but Proxy-based interceptors are unacceptably slow for an in-memory data structure. - * Given that this feature is rarely needed, it is better to not provide it for now - * and instead expect callers to find another way, for example using immutable objects. - */ -/** - * Reactive wrappers for {@link java.util} collections. - */ -@StubDocs -@NoTests -public class ReactiveCollections { - /* - * There are many ways to supply options to wrapping methods: - * - configure builder object and then call collection-wrapping methods on it - * - add NIO-style ellipsis option parameters to every method - * - add an overload for every method that takes an options object - * - provide many variations and overloads of the collection-wrapping methods - * - * We will go with options object, because it gives us very low verbosity combined with very high flexibility. - * - * Since most options are boolean, we then have a choice between boolean parameter and assuming true parameter. - * We will assume true parameter, because this favors the much more common static configuration of reactive collections. - */ - /** - * Options controlling behavior of reactive collections. - */ - public static class Options { - /* - * Checking for full equality during writes may reduce the number of invalidations. - * False by default since standard Java collections don't do any equality check during item writes. - */ - private boolean compareValues; - public Options compareValues() { - compareValues = true; - return this; - } - /* - * Writes may signal collection state via return value or specific exceptions. - * By default we observe the collection as a dependency to account for it. - * We let callers disable this feature in case they know they wouldn't check status/exceptions - * and they wish to avoid collecting unnecessary dependencies. - * This is equivalent to wrapping writes with ReactiveScope.ignore(). - * This has no effect on methods that are inherently read-write. - * False by default to honor read-write semantics of standard Java collections. - */ - private boolean ignoreWriteStatus; - public Options ignoreWriteStatus() { - ignoreWriteStatus = true; - return this; - } - private boolean ignoreWriteExceptions; - public Options ignoreWriteExceptions() { - ignoreWriteExceptions = true; - return this; - } - /* - * If write status or exceptions are not observed as dependencies, - * but they are still used by callers, it can result in surprisingly non-reactive code. - * To prevent that, we can silence return status and state-related exceptions from write methods. - * Callers are then forced to be explicit about whether they want read or write operation. - * This has no effect on methods that are inherently read-write. - * False by default to honor interface semantics of Java collections. - */ - private boolean silenceWriteStatus; - public Options silenceWriteStatus() { - silenceWriteStatus = true; - return this; - } - private boolean silenceWriteExceptions; - public Options silenceWriteExceptions() { - silenceWriteExceptions = true; - return this; - } - /* - * Collections have single reactive variable by default. - * This matches document-level granularity preferred for reactive objects in hookless. - * It is however often useful to have one reactive variable per item, - * especially in large maps or maps with heavy values. - */ - private boolean perItem; - public Options perItem() { - perItem = true; - return this; - } - } - /* - * Base class for both collections and iterators. - * Its main job is to provide utility methods to access reactive variable. - */ - private static class ReactiveCollectionObject { - final ReactiveVariable version; - final Options config; - ReactiveCollectionObject(Options config) { - version = OwnerTrace.of(new ReactiveVariable()) - .parent(this) - .target(); - this.config = config; - } - ReactiveCollectionObject(ReactiveCollectionObject master) { - version = master.version; - config = master.config; - } - void observe() { - version.get(); - } - void observeStatus() { - if (!config.ignoreWriteStatus) - observe(); - } - void observeException() { - if (!config.ignoreWriteExceptions) - observe(); - } - void observeStatusAndException() { - if (!config.ignoreWriteStatus || !config.ignoreWriteExceptions) - observe(); - } - void invalidate() { - version.set(new Object()); - } - void invalidateIf(boolean changed) { - if (changed) - invalidate(); - } - void invalidateIfChanged(Object previous, Object next) { - if (!config.compareValues && previous != next || config.compareValues && !Objects.equals(previous, next)) - invalidate(); - } - RuntimeException silenceException(RuntimeException ex) { - if (config.silenceWriteExceptions) - return new SilencedCollectionException(ex); - return ex; - } - boolean silenceStatus(boolean changed) { - if (config.silenceWriteStatus) - return true; - return changed; - } - T silenceResult(T result) { - if (config.silenceWriteStatus) - return null; - return result; - } - } - private static class SilencedCollectionException extends RuntimeException { - private static final long serialVersionUID = -2919538247896857962L; - SilencedCollectionException(RuntimeException silenced) { - super(silenced); - } - } - /* - * Keyed collections (sets and maps) and their iterators and views need per-key reactivity. - * We will opt to create reactive variables also when missing keys are queried, - * which can lead to surprisingly high memory usage, but it is realistically implementable. - */ - private static class ReactiveItemObject extends ReactiveCollectionObject { - final Map> kversions; - ReactiveItemObject(Options config) { - super(config); - /* - * Synchronize on kversions, so that iterators and views share lock with the main collection. - */ - kversions = new ConcurrentHashMap<>(); - } - ReactiveItemObject(ReactiveItemObject master) { - super(master); - kversions = master.kversions; - } - void observe(Object item) { - ReactiveVariable kversion = kversions.computeIfAbsent(item, k -> OwnerTrace.of(new ReactiveVariable()) - .parent(this) - .tag("item", item) - .target()); - kversion.get(); - } - void observe(Collection items) { - for (Object item : items) - observe(item); - } - void observeStatus(Object item) { - if (!config.ignoreWriteStatus) - observe(item); - } - void observeStatus(Collection items) { - if (!config.ignoreWriteStatus) - observe(items); - } - void observeStatusAndException(Object item) { - if (!config.ignoreWriteStatus || !config.ignoreWriteExceptions) - observe(item); - } - void observeStatusAndException(Collection items) { - if (!config.ignoreWriteStatus || !config.ignoreWriteExceptions) - observe(items); - } - void invalidateItem(Object item) { - ReactiveVariable kversion = kversions.remove(item); - if (kversion != null) - kversion.set(new Object()); - } - void invalidate(Object item) { - invalidateItem(item); - invalidate(); - } - void invalidate(Collection items) { - if (!items.isEmpty()) { - for (Object item : items) - invalidateItem(item); - invalidate(); - } - } - void invalidateAll() { - for (Object item : new ArrayList<>(kversions.keySet())) - invalidateItem(item); - invalidate(); - } - void invalidateIf(Object item, boolean changed) { - if (changed) - invalidate(item); - } - void invalidateIf(Collection items, boolean changed) { - if (changed) - invalidate(items); - } - void invalidateAllIf(boolean changed) { - if (changed) - invalidateAll(); - } - void invalidateIfChanged(Object item, Object previous, Object next) { - if (!config.compareValues && previous != next || config.compareValues && !Objects.equals(previous, next)) - invalidate(item); - } - } - private static class ReactiveIterator extends ReactiveCollectionObject implements Iterator { - final Iterator inner; - ReactiveIterator(ReactiveCollectionObject master, Iterator inner) { - super(master); - this.inner = inner; - OwnerTrace.of(this) - .alias("iterator") - .parent(master); - } - @Override - public boolean hasNext() { - observe(); - return inner.hasNext(); - } - @Override - public T next() { - observe(); - return inner.next(); - } - @Override - public void remove() { - inner.remove(); - invalidate(); - } - @Override - public String toString() { - observe(); - return OwnerTrace.of(this) + ": " + inner.toString(); - } - } - public static Collection collection(Collection collection, Options options) { - Objects.requireNonNull(collection); - Objects.requireNonNull(options); - return new ReactiveCollection<>(collection, options); - } - public static Collection collection(Collection collection) { - return collection(collection, new Options()); - } - private static class ReactiveCollection extends ReactiveCollectionObject implements Collection { - final Collection inner; - ReactiveCollection(Collection inner, Options config) { - super(config); - OwnerTrace.of(this).alias("collection"); - this.inner = inner; - } - ReactiveCollection(Collection inner, ReactiveCollectionObject master) { - super(master); - OwnerTrace.of(this) - .alias("collection") - .parent(master); - this.inner = inner; - } - @Override - public boolean add(T item) { - Objects.requireNonNull(item); - observeStatusAndException(); - boolean changed; - try { - changed = inner.add(item); - } catch (IllegalStateException ex) { - throw silenceException(ex); - } - invalidateIf(changed); - return silenceStatus(changed); - } - @Override - public boolean addAll(Collection collection) { - for (T item : collection) - Objects.requireNonNull(item); - observeStatusAndException(); - boolean changed; - try { - changed = inner.addAll(collection); - } catch (IllegalStateException ex) { - /* - * We don't know whether any elements were added, so invalidate just in case. - */ - invalidate(); - throw silenceException(ex); - } - invalidateIf(changed); - return silenceStatus(changed); - } - @Override - public void clear() { - inner.clear(); - invalidate(); - } - @Override - public boolean contains(Object item) { - observe(); - return inner.contains(item); - } - @Override - public boolean containsAll(Collection collection) { - observe(); - return inner.containsAll(collection); - } - @Override - public boolean equals(Object obj) { - observe(); - return inner.equals(obj); - } - @Override - public int hashCode() { - observe(); - return inner.hashCode(); - } - @Override - public boolean isEmpty() { - observe(); - return inner.isEmpty(); - } - @Override - public Iterator iterator() { - return new ReactiveIterator<>(this, inner.iterator()); - } - @Override - public boolean remove(Object item) { - observeStatus(); - boolean changed = inner.remove(item); - invalidateIf(changed); - return silenceStatus(changed); - } - @Override - public boolean removeAll(Collection collection) { - observeStatus(); - boolean changed = inner.removeAll(collection); - invalidateIf(changed); - return silenceStatus(changed); - } - @Override - public boolean retainAll(Collection collection) { - observeStatus(); - boolean changed = inner.retainAll(collection); - invalidateIf(changed); - return silenceStatus(changed); - } - @Override - public int size() { - observe(); - return inner.size(); - } - @Override - public Object[] toArray() { - observe(); - return inner.toArray(); - } - @Override - public U[] toArray(U[] array) { - observe(); - return inner.toArray(array); - } - @Override - public String toString() { - observe(); - return OwnerTrace.of(this) + ": " + inner; - } - } - private static class ReactiveItemCollection extends ReactiveItemObject implements Collection { - final Collection inner; - ReactiveItemCollection(Collection inner, Options config) { - super(config); - OwnerTrace.of(this).alias("collection"); - this.inner = inner; - } - ReactiveItemCollection(Collection inner, ReactiveItemObject master) { - super(master); - OwnerTrace.of(this) - .alias("collection") - .parent(master); - this.inner = inner; - } - @Override - public boolean add(T item) { - Objects.requireNonNull(item); - observeStatusAndException(item); - boolean changed; - try { - changed = inner.add(item); - } catch (IllegalStateException ex) { - throw silenceException(ex); - } - invalidateIf(item, changed); - return silenceStatus(changed); - } - @Override - public boolean addAll(Collection collection) { - for (T item : collection) - Objects.requireNonNull(item); - observeStatusAndException(collection); - boolean changed; - try { - changed = inner.addAll(collection); - } catch (IllegalStateException ex) { - /* - * We don't know whether any elements were added, so invalidate just in case. - */ - invalidate(collection); - throw silenceException(ex); - } - invalidateIf(collection, changed); - return silenceStatus(changed); - } - @Override - public void clear() { - inner.clear(); - invalidateAll(); - } - @Override - public boolean contains(Object item) { - observe(item); - return inner.contains(item); - } - @Override - public boolean containsAll(Collection collection) { - observe(collection); - return inner.containsAll(collection); - } - @Override - public boolean equals(Object obj) { - observe(); - return inner.equals(obj); - } - @Override - public int hashCode() { - observe(); - return inner.hashCode(); - } - @Override - public boolean isEmpty() { - observe(); - return inner.isEmpty(); - } - @Override - public Iterator iterator() { - return new ReactiveIterator<>(this, inner.iterator()); - } - @Override - public boolean remove(Object item) { - observeStatus(item); - boolean changed = inner.remove(item); - invalidateIf(item, changed); - return silenceStatus(changed); - } - @Override - public boolean removeAll(Collection collection) { - observeStatus(collection); - boolean changed = inner.removeAll(collection); - invalidateIf(collection, changed); - return silenceStatus(changed); - } - @Override - public boolean retainAll(Collection collection) { - observeStatus(); - boolean changed = inner.retainAll(collection); - invalidateAllIf(changed); - return silenceStatus(changed); - } - @Override - public int size() { - observe(); - return inner.size(); - } - @Override - public Object[] toArray() { - observe(); - return inner.toArray(); - } - @Override - public U[] toArray(U[] array) { - observe(); - return inner.toArray(array); - } - @Override - public String toString() { - observe(); - return OwnerTrace.of(this) + ": " + inner; - } - } - private static class ReactiveListIterator extends ReactiveIterator implements ListIterator { - final ListIterator inner; - ReactiveListIterator(ReactiveCollectionObject master, ListIterator inner) { - super(master, inner); - this.inner = inner; - OwnerTrace.of(this) - .alias("iterator") - .parent(master); - } - @Override - public void add(T e) { - Objects.requireNonNull(e); - inner.add(e); - invalidate(); - } - @Override - public boolean hasPrevious() { - /* - * Result of this method is predictable for ArrayList, thus no need to observe dependency, - * but LinkedList can be modified during the lifetime of the iterator - * and callers can then use this method to probe the collection for such changes. - * Since we don't know which List implementation are we working with, - * we better observe the dependency to cover all cases. - */ - observe(); - return inner.hasPrevious(); - } - @Override - public int nextIndex() { - /* - * LinkedList may have changing and thus unpredictable element offsets due to background modifications. - */ - observe(); - return inner.nextIndex(); - } - @Override - public T previous() { - observe(); - return inner.previous(); - } - @Override - public int previousIndex() { - observe(); - return inner.previousIndex(); - } - @Override - public void remove() { - inner.remove(); - invalidate(); - } - @Override - public void set(T e) { - Objects.requireNonNull(e); - inner.set(e); - invalidate(); - } - @Override - public String toString() { - observe(); - return OwnerTrace.of(this) + ": " + inner; - } - } - public static List list(List list, Options options) { - Objects.requireNonNull(list); - Objects.requireNonNull(options); - return new ReactiveList<>(list, options); - } - public static List list(List list) { - return list(list, new Options()); - } - private static class ReactiveList extends ReactiveCollection implements List { - final List inner; - ReactiveList(List inner, Options config) { - super(inner, config); - OwnerTrace.of(this).alias("list"); - this.inner = inner; - } - ReactiveList(List inner, ReactiveCollectionObject master) { - super(inner, master); - OwnerTrace.of(this) - .alias("list") - .parent(master); - this.inner = inner; - } - @Override - public boolean add(T item) { - Objects.requireNonNull(item); - inner.add(item); - invalidate(); - return true; - } - @Override - public void add(int index, T element) { - Objects.requireNonNull(element); - observeException(); - try { - inner.add(index, element); - } catch (IndexOutOfBoundsException ex) { - throw silenceException(ex); - } - invalidate(); - } - @Override - public boolean addAll(Collection collection) { - for (T item : collection) - Objects.requireNonNull(item); - boolean changed = inner.addAll(collection); - invalidateIf(changed); - /* - * Do not observe or silence the status, because it doesn't depend on collection state. - */ - return changed; - } - @Override - public boolean addAll(int index, Collection collection) { - for (T item : collection) - Objects.requireNonNull(item); - observeException(); - boolean changed; - try { - changed = inner.addAll(index, collection); - } catch (IndexOutOfBoundsException ex) { - throw silenceException(ex); - } - invalidateIf(changed); - return changed; - } - @Override - public T get(int index) { - observe(); - return inner.get(index); - } - @Override - public int indexOf(Object o) { - observe(); - return inner.indexOf(o); - } - @Override - public int lastIndexOf(Object o) { - observe(); - return inner.lastIndexOf(o); - } - @Override - public ListIterator listIterator() { - return new ReactiveListIterator<>(this, inner.listIterator()); - } - @Override - public ListIterator listIterator(int index) { - observeException(); - try { - return new ReactiveListIterator<>(this, inner.listIterator(index)); - } catch (IndexOutOfBoundsException ex) { - throw silenceException(ex); - } - } - @Override - public T remove(int index) { - observeStatusAndException(); - T item; - try { - item = inner.remove(index); - } catch (IndexOutOfBoundsException ex) { - throw silenceException(ex); - } - invalidate(); - return silenceResult(item); - } - @Override - public T set(int index, T element) { - Objects.requireNonNull(element); - observeStatusAndException(); - T previous; - try { - previous = inner.set(index, element); - } catch (IndexOutOfBoundsException ex) { - throw silenceException(ex); - } - invalidateIfChanged(previous, element); - return silenceResult(previous); - } - @Override - public List subList(int fromIndex, int toIndex) { - observeException(); - try { - return new ReactiveList<>(inner.subList(fromIndex, toIndex), this); - } catch (IndexOutOfBoundsException ex) { - throw silenceException(ex); - } - } - @Override - public String toString() { - observe(); - return OwnerTrace.of(this) + ": " + inner; - } - } - public static Set set(Set set, Options options) { - Objects.requireNonNull(set); - Objects.requireNonNull(options); - if (options.perItem) - return new ReactiveItemSet<>(set, options); - else - return new ReactiveSet<>(set, options); - } - public static Set set(Set set) { - return set(set, new Options()); - } - private static class ReactiveSet extends ReactiveCollection implements Set { - final Set inner; - ReactiveSet(Set inner, Options config) { - super(inner, config); - OwnerTrace.of(this).alias("set"); - this.inner = inner; - } - ReactiveSet(Set inner, ReactiveCollectionObject master) { - super(inner, master); - OwnerTrace.of(this) - .alias("set") - .parent(master); - this.inner = inner; - } - @Override - public boolean add(T item) { - Objects.requireNonNull(item); - observeStatus(); - boolean changed = inner.add(item); - invalidateIf(changed); - return silenceStatus(changed); - } - @Override - public boolean addAll(Collection collection) { - for (T item : collection) - Objects.requireNonNull(item); - observeStatus(); - boolean changed = inner.addAll(collection); - invalidateIf(changed); - return silenceStatus(changed); - } - @Override - public String toString() { - observe(); - return OwnerTrace.of(this) + ": " + inner; - } - } - private static class ReactiveItemSet extends ReactiveItemCollection implements Set { - final Set inner; - ReactiveItemSet(Set inner, Options config) { - super(inner, config); - OwnerTrace.of(this).alias("set"); - this.inner = inner; - } - ReactiveItemSet(Set inner, ReactiveItemObject master) { - super(inner, master); - OwnerTrace.of(this) - .alias("set") - .parent(master); - this.inner = inner; - } - @Override - public boolean add(T item) { - Objects.requireNonNull(item); - observeStatus(item); - boolean changed = inner.add(item); - invalidateIf(item, changed); - return silenceStatus(changed); - } - @Override - public boolean addAll(Collection collection) { - for (T item : collection) - Objects.requireNonNull(item); - observeStatus(collection); - boolean changed = inner.addAll(collection); - invalidateIf(collection, changed); - return silenceStatus(changed); - } - @Override - public String toString() { - observe(); - return OwnerTrace.of(this) + ": " + inner; - } - } - public static Map map(Map map, Options options) { - Objects.requireNonNull(map); - Objects.requireNonNull(options); - if (options.perItem) - return new ReactiveItemMap<>(map, options); - else - return new ReactiveMap<>(map, options); - } - public static Map map(Map map) { - return map(map, new Options()); - } - private static class ReactiveMap extends ReactiveCollectionObject implements Map { - final Map inner; - ReactiveMap(Map inner, Options config) { - super(config); - OwnerTrace.of(this).alias("map"); - this.inner = inner; - } - @Override - public void clear() { - inner.clear(); - invalidate(); - } - @Override - public boolean containsKey(Object key) { - observe(); - return inner.containsKey(key); - } - @Override - public boolean containsValue(Object value) { - observe(); - return inner.containsValue(value); - } - @Override - public Set> entrySet() { - return new ReactiveSet<>(inner.entrySet(), this); - } - @Override - public boolean equals(Object obj) { - observe(); - return inner.equals(obj); - } - @Override - public V get(Object key) { - observe(); - return inner.get(key); - } - @Override - public int hashCode() { - observe(); - return inner.hashCode(); - } - @Override - public boolean isEmpty() { - observe(); - return inner.isEmpty(); - } - @Override - public Set keySet() { - return new ReactiveSet<>(inner.keySet(), this); - } - @Override - public V put(K key, V value) { - Objects.requireNonNull(value); - observeStatus(); - V previous = inner.put(key, value); - invalidateIfChanged(previous, value); - return silenceResult(previous); - } - @Override - public void putAll(Map m) { - if (!m.isEmpty()) { - for (V value : m.values()) - Objects.requireNonNull(value); - inner.putAll(m); - invalidate(); - } - } - @Override - public V remove(Object key) { - observeStatus(); - V value = inner.remove(key); - invalidateIf(value != null); - return silenceResult(value); - } - @Override - public int size() { - observe(); - return inner.size(); - } - @Override - public Collection values() { - return new ReactiveCollection<>(inner.values(), this); - } - @Override - public String toString() { - return OwnerTrace.of(this) + ": " + inner; - } - } - private static class ReactiveItemMap extends ReactiveItemObject implements Map { - final Map inner; - ReactiveItemMap(Map inner, Options config) { - super(config); - OwnerTrace.of(this).alias("map"); - this.inner = inner; - } - @Override - public void clear() { - inner.clear(); - invalidateAll(); - } - @Override - public boolean containsKey(Object key) { - observe(key); - return inner.containsKey(key); - } - @Override - public boolean containsValue(Object value) { - observe(); - return inner.containsValue(value); - } - @Override - public Set> entrySet() { - /* - * Don't use ReactiveItemSet here, because entry objects != key objects. Per key reactivity won't work. - */ - return new ReactiveSet<>(inner.entrySet(), this); - } - @Override - public boolean equals(Object obj) { - observe(); - return inner.equals(obj); - } - @Override - public V get(Object key) { - observe(key); - return inner.get(key); - } - @Override - public int hashCode() { - observe(); - return inner.hashCode(); - } - @Override - public boolean isEmpty() { - observe(); - return inner.isEmpty(); - } - @Override - public Set keySet() { - return new ReactiveItemSet<>(inner.keySet(), this); - } - @Override - public V put(K key, V value) { - Objects.requireNonNull(value); - observeStatus(key); - V previous = inner.put(key, value); - invalidateIfChanged(key, previous, value); - return silenceResult(previous); - } - @Override - public void putAll(Map m) { - if (!m.isEmpty()) { - for (V value : m.values()) - Objects.requireNonNull(value); - inner.putAll(m); - invalidate(m.keySet()); - } - } - @Override - public V remove(Object key) { - observeStatus(key); - V value = inner.remove(key); - invalidateIf(key, value != null); - return silenceResult(value); - } - @Override - public int size() { - observe(); - return inner.size(); - } - @Override - public Collection values() { - return new ReactiveCollection<>(inner.values(), this); - } - @Override - public String toString() { - observe(); - return OwnerTrace.of(this) + ": " + inner; - } - } - public static Queue queue(Queue queue, Options options) { - Objects.requireNonNull(queue); - Objects.requireNonNull(options); - return new ReactiveQueue<>(queue, options); - } - public static Queue queue(Queue queue) { - return queue(queue, new Options()); - } - /* - * Queue can be configured ignore/silence write status and exceptions, - * but it is usually a bad idea to do so since queue operations inherently combine reads with writes. - * It is better to rely on progress detection, which is done automatically. - */ - private static class ReactiveQueue extends ReactiveCollection implements Queue { - final Queue inner; - ReactiveQueue(Queue inner, Options config) { - super(inner, config); - OwnerTrace.of(this).alias("queue"); - this.inner = inner; - } - @Override - public T element() { - observe(); - return inner.element(); - } - @Override - public boolean offer(T item) { - Objects.requireNonNull(item); - observeStatus(); - boolean changed = inner.offer(item); - invalidateIf(changed); - return silenceStatus(changed); - } - @Override - public T peek() { - observe(); - return inner.peek(); - } - @Override - public T poll() { - observeStatus(); - T item = inner.poll(); - invalidateIf(item != null); - return item; - } - @Override - public T remove() { - observeException(); - T item; - try { - item = inner.remove(); - } catch (NoSuchElementException ex) { - throw silenceException(ex); - } - /* - * We only get here if exception is not thrown. - * In that case, progress has been made and it is okay to observe and invalidate at the same time. - */ - invalidate(); - return item; - } - @Override - public String toString() { - observe(); - return OwnerTrace.of(this) + ": " + inner; - } - } -} diff --git a/src/main/java/com/machinezoo/hookless/util/package-info.java b/src/main/java/com/machinezoo/hookless/util/package-info.java deleted file mode 100644 index c7c059a..0000000 --- a/src/main/java/com/machinezoo/hookless/util/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -/** - * Reactive versions of classes from {@link java.util}. - * This package also contains hookless-specific utilities. - */ -package com.machinezoo.hookless.util; diff --git a/src/main/java/com/machinezoo/hookless/utils/WeakRunnable.java b/src/main/java/com/machinezoo/hookless/utils/WeakRunnable.java deleted file mode 100644 index c55b521..0000000 --- a/src/main/java/com/machinezoo/hookless/utils/WeakRunnable.java +++ /dev/null @@ -1,48 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless.utils; - -import java.lang.ref.*; -import java.util.*; -import java.util.function.*; -import com.machinezoo.stagean.*; - -/* - * Hookless relies on garbage collection of weakly referenced objects, so that apps don't have to explicitly unsubscribe from sources. - * Extra garbage is not just a burden on memory. Reactive zombies might keep responding to changes and eat up processor time too. - * - * Unfortunately, some reactive objects that are otherwise weakly reachable intermittently acquire strong reachability. - * This happens chiefly in two cases: running methods and queued thread pool tasks, both triggered by callbacks from changing dependencies. - * Running methods are very hard to make weak, but we don't worry about that, because their number is limited by thread count. - * Queued tasks can however keep around thousands or millions of reactive zombies if they are allowed to hold strong references. - * - * This class makes it easy to create weak Runnable from instance method references that can be safely scheduled on executors. - * It should be used whenever reactive objects need to schedule their execution in a thread pool. - */ -/** - * Weak reference to an instance method. - * - * @param - * type of object that defines the method - */ -@StubDocs -@NoTests -public class WeakRunnable implements Runnable { - private final WeakReference weakref; - private final Consumer method; - /* - * We cannot automatically break up existing Runnable created from bound instance method reference. - * We have to ask calling code to separate instance reference and method reference explicitly. - */ - public WeakRunnable(T target, Consumer method) { - Objects.requireNonNull(target); - Objects.requireNonNull(method); - weakref = new WeakReference<>(target); - this.method = method; - } - @Override - public void run() { - T target = weakref.get(); - if (target != null) - method.accept(target); - } -} diff --git a/src/test/java/com/machinezoo/hookless/CurrentReactiveScopeTest.java b/src/test/java/com/machinezoo/hookless/CurrentReactiveScopeTest.java deleted file mode 100644 index e46a007..0000000 --- a/src/test/java/com/machinezoo/hookless/CurrentReactiveScopeTest.java +++ /dev/null @@ -1,57 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.junit.jupiter.api.Assertions.*; -import org.junit.jupiter.api.*; -import com.machinezoo.closeablescope.*; - -public class CurrentReactiveScopeTest { - @Test - public void block() { - // Propagate blocking to the current scope. - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - CurrentReactiveScope.block(); - assertTrue(s.blocked()); - } - // No effect and no exception outside of any scope. - CurrentReactiveScope.block(); - } - @Test - public void blocked() { - try (CloseableScope c = new ReactiveScope().enter()) { - // Read blocking state from the current scope. - assertFalse(CurrentReactiveScope.blocked()); - CurrentReactiveScope.block(); - assertTrue(CurrentReactiveScope.blocked()); - } - // Default to false outside of any scope. - assertFalse(CurrentReactiveScope.blocked()); - } - @Test - public void freeze() { - // Propagate freeze() call to the current scope. - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - assertEquals("value", CurrentReactiveScope.freeze("key", () -> "value")); - assertEquals("value", CurrentReactiveScope.freeze("key", () -> "other")); - assertEquals(new ReactiveValue<>("value"), s.freezes().get("key")); - } - // Re-evaluate the Supplier each time outside of any reactive scope. - assertEquals("one", CurrentReactiveScope.freeze("key", () -> "one")); - assertEquals("two", CurrentReactiveScope.freeze("key", () -> "two")); - } - @Test - public void pin() { - // Propagate pin() call to the current scope. - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - assertEquals("value", CurrentReactiveScope.pin("key", () -> "value")); - assertEquals("value", CurrentReactiveScope.pin("key", () -> "other")); - assertEquals(new ReactiveValue<>("value"), s.pins().get("key")); - } - // Re-evaluate the Supplier each time outside of any reactive scope. - assertEquals("one", CurrentReactiveScope.pin("key", () -> "one")); - assertEquals("two", CurrentReactiveScope.pin("key", () -> "two")); - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactiveBlockingExceptionTest.java b/src/test/java/com/machinezoo/hookless/ReactiveBlockingExceptionTest.java deleted file mode 100644 index 1e9f685..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactiveBlockingExceptionTest.java +++ /dev/null @@ -1,65 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; -import org.junit.jupiter.api.*; -import com.machinezoo.closeablescope.*; - -public class ReactiveBlockingExceptionTest { - @Test - public void constructors() { - ReactiveBlockingException e = new ReactiveBlockingException(); - assertNull(e.getMessage()); - assertNull(e.getCause()); - e = new ReactiveBlockingException("message"); - assertEquals("message", e.getMessage()); - assertNull(e.getCause()); - ArithmeticException ae = new ArithmeticException(); - e = new ReactiveBlockingException(ae); - assertEquals(ae.toString(), e.getMessage()); - assertThat(e.getCause(), instanceOf(ArithmeticException.class)); - e = new ReactiveBlockingException("message", new ArithmeticException()); - assertEquals("message", e.getMessage()); - assertThat(e.getCause(), instanceOf(ArithmeticException.class)); - } - @Test - public void block() { - // Merely calling the constructor does not block the current computation. - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - new ReactiveBlockingException(); - assertFalse(s.blocked()); - } - // Calling block() however does both blocking and throwing. - s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - ReactiveBlockingException e = assertThrows(ReactiveBlockingException.class, () -> ReactiveBlockingException.block()); - assertTrue(s.blocked()); - assertNull(e.getMessage()); - assertNull(e.getCause()); - } - s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - ReactiveBlockingException e = assertThrows(ReactiveBlockingException.class, () -> ReactiveBlockingException.block("message")); - assertTrue(s.blocked()); - assertEquals("message", e.getMessage()); - assertNull(e.getCause()); - } - s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - ReactiveBlockingException e = assertThrows(ReactiveBlockingException.class, () -> ReactiveBlockingException.block(new ArithmeticException())); - assertTrue(s.blocked()); - assertNull(e.getMessage()); - assertThat(e.getCause(), instanceOf(ArithmeticException.class)); - } - s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - ReactiveBlockingException e = assertThrows(ReactiveBlockingException.class, () -> ReactiveBlockingException.block("message", new ArithmeticException())); - assertTrue(s.blocked()); - assertEquals("message", e.getMessage()); - assertThat(e.getCause(), instanceOf(ArithmeticException.class)); - } - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactiveExecutorTest.java b/src/test/java/com/machinezoo/hookless/ReactiveExecutorTest.java deleted file mode 100644 index c0da2e8..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactiveExecutorTest.java +++ /dev/null @@ -1,136 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.awaitility.Awaitility.*; -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; -import java.time.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.*; -import org.junit.jupiter.api.*; -import org.junitpioneer.jupiter.*; - -public class ReactiveExecutorTest extends TestBase { - ReactiveExecutor x; - @BeforeEach - public void setup() { - x = new ReactiveExecutor(); - } - @AfterEach - public void cleanup() throws Exception { - x.shutdown(); - x.awaitTermination(1, TimeUnit.MINUTES); - } - @Test - public void submit() throws Exception { - AtomicInteger n = new AtomicInteger(); - // Task submission works as usual. - x.execute(() -> n.incrementAndGet()); - Future fr = x.submit(() -> { - n.incrementAndGet(); - }); - Future fc = x.submit(() -> { - n.incrementAndGet(); - return "done"; - }); - Future fv = x.submit(() -> { - n.incrementAndGet(); - }, "ok"); - // All tasks are executed. - await().untilAtomic(n, equalTo(4)); - // Results are propagated into futures. - fr.get(); - assertEquals("done", fc.get()); - assertEquals("ok", fv.get()); - } - // Simulation of computation. - private void waste(Duration duration) { - long nanos = duration.toNanos(); - long start = System.nanoTime(); - while (System.nanoTime() - start < nanos) - ; - } - @Test - public void countSequential() { - // Counters start at zero. - assertEquals(0, x.getTaskCount()); - assertEquals(0, x.getEventCount()); - AtomicInteger n = new AtomicInteger(); - x.execute(() -> n.incrementAndGet()); - await().untilAtomic(n, equalTo(1)); - // Task counter is incremented every time some task is executed. - assertEquals(1, x.getTaskCount()); - // Without any queuing, event counter is identical to task counter. - assertEquals(1, x.getEventCount()); - x.execute(() -> n.incrementAndGet()); - await().untilAtomic(n, equalTo(2)); - assertEquals(2, x.getTaskCount()); - assertEquals(2, x.getEventCount()); - } - @Test - public void countParallel() { - // Run 100 tasks per core, each 1ms long. - AtomicInteger n = new AtomicInteger(); - int tc = 100 * Runtime.getRuntime().availableProcessors(); - for (int i = 0; i < tc; ++i) { - x.execute(() -> { - waste(Duration.ofMillis(1)); - n.incrementAndGet(); - }); - } - await().untilAtomic(n, equalTo(tc)); - // Task count is incremented for every executed task. - assertEquals(tc, x.getTaskCount()); - // But event count is much smaller, because queued tasks are aggregated in events. - assertThat(x.getEventCount(), lessThan(tc / 20L)); - } - @RetryingTest(10) - public void parallelism() { - // Submit 300ms worth of 10ms tasks. - AtomicInteger n = new AtomicInteger(); - int tc = 30 * Runtime.getRuntime().availableProcessors(); - long t0 = System.nanoTime(); - for (int i = 0; i < tc; ++i) { - x.execute(() -> { - waste(Duration.ofMillis(10)); - n.incrementAndGet(); - }); - } - await().untilAtomic(n, equalTo(tc)); - // Expect them to complete in 150% of the minimum time, which proves they run in parallel. - assertThat(Duration.ofNanos(System.nanoTime() - t0).toMillis(), lessThan(450L)); - } - // Simulation of cascading tasks, each taking 5ms. - private void cascade(int depth, Runnable then) { - waste(Duration.ofMillis(5)); - if (depth <= 1) - then.run(); - else - x.execute(() -> cascade(depth - 1, then)); - } - @RetryingTest(10) - public void latency() throws Exception { - // Start 150ms 30-task cascade. This coincides with executor's maximum cascade depth of 30. - AtomicReference latency = new AtomicReference<>(); - long t0 = System.nanoTime(); - x.execute(() -> cascade(30, () -> latency.set(Duration.ofNanos(System.nanoTime() - t0)))); - // Make sure the cascade has started, i.e. the executor has advanced its event counter. - await().until(() -> x.getEventCount() > 0); - // The cascade has not completed yet. - assertNull(latency.get()); - // Swamp the executor with 500ms worth of work. - int tc = 50 * Runtime.getRuntime().availableProcessors(); - for (int i = 0; i < tc; ++i) - x.execute(() -> waste(Duration.ofMillis(10))); - // Latency of the cascading task remains low. - await().untilAtomic(latency, notNullValue()); - long ms = latency.get().toMillis(); - assertThat(ms, greaterThan(150L)); - assertThat(ms, lessThan(225L)); - } - @Test - public void current() throws Exception { - assertSame(x, x.submit(() -> ReactiveExecutor.current()).get()); - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactiveFreezesTest.java b/src/test/java/com/machinezoo/hookless/ReactiveFreezesTest.java deleted file mode 100644 index a87d0de..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactiveFreezesTest.java +++ /dev/null @@ -1,115 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; -import java.util.concurrent.*; -import org.junit.jupiter.api.*; -import com.machinezoo.closeablescope.*; - -public class ReactiveFreezesTest { - private final ReactiveFreezes f = new ReactiveFreezes(); - // State of ReactiveFreezes can be manipulated explicitly and fully observed. - @Test - public void explicit() { - assertThat(f.keys(), is(empty())); - assertNull(f.get("key")); - f.set("key", new ReactiveValue<>("value")); - assertThat(f.keys(), contains("key")); - assertEquals(new ReactiveValue<>("value"), f.get("key")); - f.set("key", new ReactiveValue<>(new RuntimeException())); - assertThat(f.get("key").exception(), instanceOf(RuntimeException.class)); - f.set("key", null); - assertThat(f.keys(), is(empty())); - assertNull(f.get("key")); - } - // However, the usual way to use ReactiveFreezes is to call freeze(). - @Test - public void freeze() { - assertEquals("value", f.freeze("key", () -> "value")); - assertThat(f.keys(), contains("key")); - // The Supplier is not called second time. - assertEquals("value", f.freeze("key", () -> "other")); - } - // If the Supplier throws, the exception is also frozen. - @Test - public void exception() { - // ReactiveValue wraps all exceptions in CompletionException. - CompletionException ce = assertThrows(CompletionException.class, () -> f.freeze("key", () -> { - throw new ArithmeticException(); - })); - assertThat(ce.getCause(), instanceOf(ArithmeticException.class)); - assertThat(f.keys(), contains("key")); - // If we try to throw another exception second time around, we will still get the first one. - ce = assertThrows(CompletionException.class, () -> f.freeze("key", () -> { - throw new IllegalStateException(); - })); - assertThat(ce.getCause(), instanceOf(ArithmeticException.class)); - } - // Frozen ReactiveValue of course includes the blocking flag. - @Test - public void captureBlocking() { - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - assertEquals("value", f.freeze("key", () -> { - CurrentReactiveScope.block(); - return "value"; - })); - assertTrue(s.blocked()); - assertTrue(f.get("key").blocking()); - } - } - // If the frozen value is marked as blocking for whatever reason, the blocking flag is propagated into the current computation. - // This is a contrived example using explicit manipulation API. See below for a realistic example. - @Test - public void propagateBlocking() { - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - f.set("key", new ReactiveValue<>("value", true)); - assertFalse(s.blocked()); - f.freeze("key", () -> "other"); - assertTrue(s.blocked()); - } - } - // This is a realistic example of blocking flag propagation. - @Test - public void blockingScenario() { - // Consider two nested scopes. The inner one is non-blocking. The two share one ReactiveFreezes object. - try (CloseableScope c1 = new ReactiveScope().enter()) { - try (CloseableScope c2 = ReactiveScope.nonblocking()) { - // Blocking freeze of course marks the inner scope as blocked. - assertEquals("value", CurrentReactiveScope.freeze("key", () -> { - CurrentReactiveScope.block(); - return "value"; - })); - assertTrue(CurrentReactiveScope.blocked()); - } - // The blocking flag is not propagated to the outer scope. This is how non-blocking scope works. - assertFalse(CurrentReactiveScope.blocked()); - // If we however make use of the freeze again, this time outside of the non-blocking scope, then blocking is propagated. - assertEquals("value", CurrentReactiveScope.freeze("key", () -> "other")); - assertTrue(CurrentReactiveScope.blocked()); - } - } - @Test - public void inheritance() { - ReactiveFreezes gp = new ReactiveFreezes(); - gp.set("X", new ReactiveValue<>("X in grandparent")); - gp.set("Y", new ReactiveValue<>("Y in grandparent")); - ReactiveFreezes p = new ReactiveFreezes(); - p.parent(gp); - p.set("X", new ReactiveValue<>("X in parent")); - f.parent(p); - // Freeze is taken from the nearest ancestor that has the key. - assertEquals("X in parent", f.freeze("X", () -> "random")); - assertEquals("Y in grandparent", f.freeze("Y", () -> "random")); - // Child does not store freezes that were simply returned from an ancestor. - assertThat(f.keys(), is(empty())); - // Child can override freezes defined by ancestors. - f.set("X", new ReactiveValue<>("X in child")); - assertEquals("X in child", f.freeze("X", () -> "random")); - // Override in the child has no effect on the parent. - assertEquals("X in parent", p.freeze("X", () -> "random")); - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactiveFutureTest.java b/src/test/java/com/machinezoo/hookless/ReactiveFutureTest.java deleted file mode 100644 index 0c620e4..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactiveFutureTest.java +++ /dev/null @@ -1,256 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.awaitility.Awaitility.*; -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; -import java.time.*; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.*; -import java.util.function.*; -import java.util.stream.*; -import org.junit.jupiter.api.*; -import org.junit.jupiter.params.*; -import org.junit.jupiter.params.provider.*; -import org.junitpioneer.jupiter.*; -import com.google.common.util.concurrent.*; -import com.machinezoo.closeablescope.*; - -public class ReactiveFutureTest extends TestBase { - @Test - public void wrap() { - // Any CompletableFuture can be wrapped. - CompletableFuture cf = new CompletableFuture<>(); - ReactiveFuture rf = ReactiveFuture.wrap(cf); - assertSame(cf, rf.completable()); - // Wrapping the same CompletableFuture always returns the same ReactiveFuture. - assertSame(rf, ReactiveFuture.wrap(cf)); - } - @Test - public void create() { - // ReactiveFuture can also construct its own CompletableFuture if none is provided explicitly. - ReactiveFuture rf = new ReactiveFuture<>(); - assertNotNull(rf.completable()); - // Wrapping the CompletableFuture just returns the ReactiveFuture that created it. - assertSame(rf, ReactiveFuture.wrap(rf.completable())); - } - @Test - public void waiting() { - ReactiveFuture rf = new ReactiveFuture<>(); - try (CloseableScope c = new ReactiveScope().enter()) { - // State checks are negative without any blocking. - assertFalse(rf.done()); - assertFalse(rf.failed()); - assertFalse(rf.cancelled()); - // When fallback is provided, it is returned without blocking. - assertEquals("fallback", rf.getNow("fallback")); - assertFalse(CurrentReactiveScope.blocked()); - // Without fallback, blocking exception is thrown. - assertThrows(ReactiveBlockingException.class, () -> rf.get()); - assertTrue(CurrentReactiveScope.blocked()); - } - } - @Test - public void done() { - ReactiveFuture rf = ReactiveFuture.wrap(CompletableFuture.completedFuture("hello")); - try (CloseableScope c = new ReactiveScope().enter()) { - assertTrue(rf.done()); - assertFalse(rf.failed()); - assertFalse(rf.cancelled()); - assertEquals("hello", rf.getNow("fallback")); - assertEquals("hello", rf.get()); - assertEquals("hello", rf.get(Duration.ofSeconds(1))); - assertEquals("hello", rf.get(1, TimeUnit.SECONDS)); - // No blocking in any of these. - assertFalse(CurrentReactiveScope.blocked()); - } - } - private static void assertThrowsWrapped(Class clazz, Runnable runnable) { - CompletionException ce = assertThrows(CompletionException.class, runnable::run); - assertThat(ce.getCause(), instanceOf(clazz)); - } - @Test - public void failed() { - ReactiveFuture rf = new ReactiveFuture<>(); - rf.completable().completeExceptionally(new ArithmeticException()); - try (CloseableScope c = new ReactiveScope().enter()) { - assertTrue(rf.done()); - assertTrue(rf.failed()); - assertFalse(rf.cancelled()); - assertThrowsWrapped(ArithmeticException.class, () -> rf.getNow("fallback")); - assertThrowsWrapped(ArithmeticException.class, () -> rf.get()); - assertThrowsWrapped(ArithmeticException.class, () -> rf.get(Duration.ofSeconds(1))); - assertThrowsWrapped(ArithmeticException.class, () -> rf.get(1, TimeUnit.SECONDS)); - // No blocking in any of these. - assertFalse(CurrentReactiveScope.blocked()); - } - } - @Test - public void cancelled() { - ReactiveFuture rf = new ReactiveFuture<>(); - rf.completable().cancel(false); - try (CloseableScope c = new ReactiveScope().enter()) { - assertTrue(rf.done()); - assertTrue(rf.failed()); - assertTrue(rf.cancelled()); - assertThrows(CancellationException.class, () -> rf.getNow("fallback")); - assertThrows(CancellationException.class, () -> rf.get()); - assertThrows(CancellationException.class, () -> rf.get(Duration.ofSeconds(1))); - assertThrows(CancellationException.class, () -> rf.get(1, TimeUnit.SECONDS)); - // No blocking in any of these. - assertFalse(CurrentReactiveScope.blocked()); - } - } - @RepeatedTest(3) - public void timeout() { - ReactiveFuture rf = new ReactiveFuture<>(); - // Timeout overloads initially reactively block. - try (CloseableScope c = new ReactiveScope().enter()) { - assertThrows(ReactiveBlockingException.class, () -> rf.get(Duration.ofMillis(50))); - assertTrue(CurrentReactiveScope.blocked()); - } - try (CloseableScope c = new ReactiveScope().enter()) { - assertThrows(ReactiveBlockingException.class, () -> rf.get(50, TimeUnit.MILLISECONDS)); - assertTrue(CurrentReactiveScope.blocked()); - } - // When the timeout expires, the same methods throw non-blocking timeout exception instead. - sleep(100); - try (CloseableScope c = new ReactiveScope().enter()) { - assertThrows(UncheckedTimeoutException.class, () -> rf.get(Duration.ofMillis(50))); - assertThrows(UncheckedTimeoutException.class, () -> rf.get(50, TimeUnit.MILLISECONDS)); - assertFalse(CurrentReactiveScope.blocked()); - } - // When the future is completed, the timeout exception is replaced with the actual result. - rf.completable().complete("hello"); - try (CloseableScope c = new ReactiveScope().enter()) { - assertEquals("hello", rf.get(Duration.ofMillis(50))); - assertEquals("hello", rf.get(50, TimeUnit.MILLISECONDS)); - assertFalse(CurrentReactiveScope.blocked()); - } - } - public static Stream completers() { - return Stream.of( - Arguments.of("done", (Consumer>)(f -> f.complete("hello"))), - Arguments.of("failed", (Consumer>)(f -> f.completeExceptionally(new ArithmeticException()))), - Arguments.of("cancelled", (Consumer>)(f -> f.cancel(false)))); - } - @ParameterizedTest - @MethodSource("completers") - public void reactive(String name, Consumer> completer) { - ReactiveFuture rf = new ReactiveFuture<>(); - // Watch all methods of the reactive future. - List> sms = new ArrayList<>(); - sms.add(ReactiveStateMachine.supply(() -> rf.done())); - sms.add(ReactiveStateMachine.supply(() -> rf.failed())); - sms.add(ReactiveStateMachine.supply(() -> rf.cancelled())); - sms.add(ReactiveStateMachine.supply(() -> rf.get())); - sms.add(ReactiveStateMachine.supply(() -> rf.getNow("fallback"))); - sms.add(ReactiveStateMachine.supply(() -> rf.get(Duration.ofSeconds(1)))); - sms.add(ReactiveStateMachine.supply(() -> rf.get(1, TimeUnit.SECONDS))); - for (ReactiveStateMachine sm : sms) { - sm.advance(); - assertTrue(sm.valid()); - } - // When the reactive future is completed (in any way), all methods signal change. - completer.accept(rf.completable()); - for (ReactiveStateMachine sm : sms) { - assertFalse(sm.valid()); - sm.advance(); - assertTrue(sm.valid()); - } - // Redundant second completion has no effect and no change is signaled. - completer.accept(rf.completable()); - for (ReactiveStateMachine sm : sms) - assertTrue(sm.valid()); - } - @RetryingTest(10) - public void reactiveTimeout() { - Function, String> m1 = f -> f.get(Duration.ofMillis(50)); - Function, String> m2 = f -> f.get(50, TimeUnit.MILLISECONDS); - for (Function, String> m : Arrays.asList(m1, m2)) { - ReactiveFuture rf = new ReactiveFuture<>(); - // Watch the timeouting method. - ReactiveStateMachine sm = ReactiveStateMachine.supply(() -> m.apply(rf)); - assertThrows(ReactiveBlockingException.class, () -> m.apply(rf)); - sm.advance(); - assertTrue(sm.valid()); - // When timeout expires, the method signals change since the type of exception has changed. - sleep(100); - assertFalse(sm.valid()); - assertThrows(UncheckedTimeoutException.class, () -> m.apply(rf)); - sm.advance(); - assertTrue(sm.valid()); - // When the reactive future is completed, the method signals another change since the result is now available. - rf.completable().complete("done"); - assertFalse(sm.valid()); - } - } - @Test - public void supplyReactive() { - ReactiveVariable v = new ReactiveVariable<>(new ReactiveValue<>("pending", true)); - CompletableFuture f = ReactiveFuture.supplyReactive(v::get); - // The future is not completed when the supplier is blocking. - settle(); - assertFalse(f.isDone()); - // Non-blocking result will be stored in the future. - v.set("done"); - await().until(f::isDone); - assertEquals("done", f.join()); - // Further changes have no effect on the future. - v.set("extra"); - settle(); - assertEquals("done", f.join()); - // It works the same way with exceptions. - v.value(new ReactiveValue<>(new ReactiveBlockingException(), true)); - f = ReactiveFuture.supplyReactive(v::get); - settle(); - assertFalse(f.isDone()); - v.value(new ReactiveValue<>(new ArithmeticException())); - await().until(f::isDone); - assertTrue(f.isCompletedExceptionally()); - ExecutionException ex = assertThrows(ExecutionException.class, f::get); - assertThat(ex.getCause(), instanceOf(ArithmeticException.class)); - } - @Test - public void runReactive() { - AtomicInteger n = new AtomicInteger(); - ReactiveVariable v = new ReactiveVariable<>(new ReactiveValue<>("pending", true)); - CompletableFuture f = ReactiveFuture.runReactive(() -> { - v.get(); - n.incrementAndGet(); - }); - // Runnable runs, but the future is not completed, because the Runnable is blocking. - await().untilAtomic(n, equalTo(1)); - settle(); - assertFalse(f.isDone()); - // The first non-blocking run completes the future. - v.set("done"); - await().untilAtomic(n, equalTo(2)); - await().until(f::isDone); - // Further changes in dependencies do not cause the Runnable to run again. - v.set("extra"); - settle(); - assertEquals(2, n.get()); - } - @Test - public void supplyReactiveExecutor() { - ReactiveExecutor x = new ReactiveExecutor(); - // Custom executor can be specified. - CompletableFuture f = ReactiveFuture.supplyReactive(() -> ReactiveExecutor.current(), x); - // Supplier runs on the executor. - assertSame(x, f.join()); - x.shutdown(); - } - @Test - public void runReactiveExecutor() { - AtomicReference cx = new AtomicReference<>(); - ReactiveExecutor x = new ReactiveExecutor(); - // Custom executor can be specified. - ReactiveFuture.runReactive(() -> cx.set(ReactiveExecutor.current()), x).join(); - // Runnable runs on the executor. - assertSame(x, cx.get()); - x.shutdown(); - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactiveLazyTest.java b/src/test/java/com/machinezoo/hookless/ReactiveLazyTest.java deleted file mode 100644 index 31151df..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactiveLazyTest.java +++ /dev/null @@ -1,69 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.junit.jupiter.api.Assertions.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.*; -import org.junit.jupiter.api.*; -import com.machinezoo.closeablescope.*; - -public class ReactiveLazyTest { - @Test - public void fresh() { - ReactiveVariable a = new ReactiveVariable<>("hello"); - ReactiveVariable b = new ReactiveVariable<>("guys"); - ReactiveLazy l = new ReactiveLazy<>(() -> a.get() + " " + b.get()); - // Initialized upon first access. - assertEquals("hello guys", l.get()); - // Reflects changes immediately. - a.set("hi"); - assertEquals("hi guys", l.get()); - b.set("gals"); - assertEquals("hi gals", l.get()); - } - @Test - public void lazy() { - AtomicInteger n = new AtomicInteger(); - ReactiveVariable v = new ReactiveVariable<>("hello"); - // Supplier is not evaluated until first get(). - ReactiveLazy l = new ReactiveLazy<>(() -> { - n.incrementAndGet(); - return v.get(); - }); - assertEquals(0, n.get()); - // Value obtained during evaluation is cached. - assertEquals("hello", l.get()); - assertEquals("hello", l.get()); - assertEquals(1, n.get()); - // Exceptions are cached too. - v.value(new ReactiveValue<>(new ArithmeticException())); - assertThrows(CompletionException.class, l::get); - assertThrows(CompletionException.class, l::get); - assertEquals(2, n.get()); - // Blocking values are cached too and blocking is repeatedly propagated. - v.value(new ReactiveValue<>("hi", true)); - for (int i = 0; i < 2; ++i) { - try (CloseableScope c = new ReactiveScope().enter()) { - assertEquals("hi", l.get()); - assertTrue(CurrentReactiveScope.blocked()); - } - } - assertEquals(3, n.get()); - } - @Test - public void reactive() { - ReactiveVariable v = new ReactiveVariable<>("hello"); - ReactiveLazy l = new ReactiveLazy<>(() -> v.get()); - try (ReactiveTrigger t = new ReactiveTrigger()) { - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - assertEquals("hello", l.get()); - t.arm(s.versions()); - } - assertFalse(t.fired()); - // Dependency invalidation is immediately propagated to dependent computations. - v.set("hi"); - assertTrue(t.fired()); - } - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactivePinsTest.java b/src/test/java/com/machinezoo/hookless/ReactivePinsTest.java deleted file mode 100644 index 83d4a1b..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactivePinsTest.java +++ /dev/null @@ -1,119 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; -import java.util.concurrent.*; -import org.junit.jupiter.api.*; -import com.machinezoo.closeablescope.*; - -public class ReactivePinsTest { - // This test is very similar to the one for ReactiveFreezes, but it's not the same as the two classes behave differently. - private final ReactivePins p = new ReactivePins(); - // State of ReactivePins can be manipulated explicitly and fully observed. - @Test - public void explicit() { - assertThat(p.keys(), is(empty())); - assertNull(p.get("key")); - p.set("key", new ReactiveValue<>("value")); - assertThat(p.keys(), contains("key")); - assertEquals(new ReactiveValue<>("value"), p.get("key")); - p.set("key", new ReactiveValue<>(new RuntimeException())); - assertThat(p.get("key").exception(), instanceOf(RuntimeException.class)); - p.set("key", null); - assertThat(p.keys(), is(empty())); - assertNull(p.get("key")); - } - // However, the usual way to use ReactivePins is to call pin(). - @Test - public void pin() { - assertEquals("value", p.pin("key", () -> "value")); - assertThat(p.keys(), contains("key")); - // The Supplier is not called second time. - assertEquals("value", p.pin("key", () -> "other")); - } - // If the Supplier throws, the exception is also pinned. - @Test - public void exception() { - // ReactiveValue wraps all exceptions in CompletionException. - CompletionException ce = assertThrows(CompletionException.class, () -> p.pin("key", () -> { - throw new ArithmeticException(); - })); - assertThat(ce.getCause(), instanceOf(ArithmeticException.class)); - assertThat(p.keys(), contains("key")); - // If we try to throw another exception second time around, we will still get the first one. - ce = assertThrows(CompletionException.class, () -> p.pin("key", () -> { - throw new IllegalStateException(); - })); - assertThat(ce.getCause(), instanceOf(ArithmeticException.class)); - } - @Test - public void blocking() { - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - // Contrary to freezing, pinning does not capture blocking. Pinning is instead disabled when blocking. - assertEquals("value", p.pin("key", () -> { - CurrentReactiveScope.block(); - return "value"; - })); - assertTrue(s.blocked()); - assertThat(p.keys(), is(empty())); - // The same applies to pre-existing blocking condition. - assertEquals("other", p.pin("key", () -> "other")); - assertThat(p.keys(), is(empty())); - // Explicitly setting blocking value is not allowed. - assertThrows(IllegalArgumentException.class, () -> p.set("key", new ReactiveValue<>("blocking", true))); - } - } - @Test - public void inheritance() { - ReactivePins gp = new ReactivePins(); - gp.set("X", new ReactiveValue<>("X in grandparent")); - gp.set("Y", new ReactiveValue<>("Y in grandparent")); - ReactivePins pp = new ReactivePins(); - pp.parent(gp); - pp.set("X", new ReactiveValue<>("X in parent")); - p.parent(pp); - // Pin is taken from the nearest ancestor that has the key. - assertEquals("X in parent", p.pin("X", () -> "random")); - assertEquals("Y in grandparent", p.pin("Y", () -> "random")); - // Child does not store pins that were simply returned from an ancestor. - assertThat(p.keys(), is(empty())); - // Child can override pins defined by ancestors. - p.set("X", new ReactiveValue<>("X in child")); - assertEquals("X in child", p.pin("X", () -> "random")); - // Override in the child has no effect on the parent. - assertEquals("X in parent", pp.pin("X", () -> "random")); - } - @Test - public void invalidation() { - // Invalidation applies to pins, not the pin container, so ensure it is non-empty. - p.pin("key", () -> "value"); - // Pins are initially valid. - assertTrue(p.valid()); - // Invalidation simply marks them as invalid. - p.invalidate(); - assertFalse(p.valid()); - // Invalidation is inherited. - ReactivePins c = new ReactivePins(); - c.parent(p); - assertFalse(c.valid()); - ReactivePins gc = new ReactivePins(); - gc.parent(c); - assertFalse(gc.valid()); - // Pins are valid if the whole hierarchy is valid. - ReactivePins vp = new ReactivePins(); - p.pin("key", () -> "value"); - c.parent(vp); - assertTrue(c.valid()); - assertTrue(gc.valid()); - // Empty pin collection is always valid since there are no pins to invalidate. - ReactivePins ep = new ReactivePins(); - ep.invalidate(); - c.parent(ep); - assertTrue(ep.valid()); - assertTrue(c.valid()); - assertTrue(gc.valid()); - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactiveScopeTest.java b/src/test/java/com/machinezoo/hookless/ReactiveScopeTest.java deleted file mode 100644 index 875ad2e..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactiveScopeTest.java +++ /dev/null @@ -1,165 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static java.util.stream.Collectors.*; -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; -import org.junit.jupiter.api.*; -import com.machinezoo.closeablescope.*; - -public class ReactiveScopeTest { - @Test - public void observeVariableAccess() { - ReactiveVariable v = new ReactiveVariable<>("hello"); - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - assertEquals("hello", v.get()); - } - assertThat(s.versions().stream().map(x -> x.variable()).collect(toList()), contains(v)); - } - @Test - public void nest() { - ReactiveVariable v1 = new ReactiveVariable<>("hello"); - ReactiveVariable v2 = new ReactiveVariable<>("world"); - ReactiveScope s1 = new ReactiveScope(); - ReactiveScope s2 = new ReactiveScope(); - try (CloseableScope c1 = s1.enter()) { - assertEquals("hello", v1.get()); - try (CloseableScope c2 = s2.enter()) { - assertEquals("world", v2.get()); - } - } - // The outer scope only records accesses that were not shadowed by inner scope. - assertThat(s1.versions().stream().map(v -> v.variable()).collect(toList()), contains(v1)); - assertThat(s2.versions().stream().map(v -> v.variable()).collect(toList()), contains(v2)); - } - @Test - public void current() { - assertNull(ReactiveScope.current()); - ReactiveScope s1 = new ReactiveScope(); - ReactiveScope s2 = new ReactiveScope(); - // Merely creating the scope doesn't cause it to be current. - assertNull(ReactiveScope.current()); - try (CloseableScope c1 = s1.enter()) { - // Computation's scope can be always retrieved from thread-local storage. - assertSame(s1, ReactiveScope.current()); - try (CloseableScope c2 = s2.enter()) { - // Inner scope shadows outer scope. - assertSame(s2, ReactiveScope.current()); - } - // Inner scope restored outer scope when it ends. - assertSame(s1, ReactiveScope.current()); - } - // Scope is current only until its computation is closed. - assertNull(ReactiveScope.current()); - } - @Test - public void keepFirstVersion() { - ReactiveVariable v = new ReactiveVariable<>("hello"); - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - assertEquals("hello", v.get()); - v.set("world"); - assertEquals("world", v.get()); - // Of the two accesses, the first one is recorded while the second one is ignored. - assertEquals(v.version() - 1, s.versions().stream().findFirst().get().number()); - } - } - @Test - public void pickEarlierVersion() { - // Create a variable with lots of versions. - ReactiveVariable v = new ReactiveVariable<>("hello"); - for (int i = 0; i < 10; ++i) - v.set("world " + i); - // Simulate collection of unordered versions, e.g. from multiple threads. - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - s.watch(v, 3); - s.watch(v, 2); - s.watch(v, 4); - } - // Earliest version is kept regardless of insertion order. - assertEquals(2, s.versions().stream().findFirst().get().number()); - } - @Test - public void ignore() { - ReactiveVariable v = new ReactiveVariable<>("hello"); - ReactiveScope s1 = new ReactiveScope(); - try (CloseableScope c1 = s1.enter()) { - assertEquals("hello", v.get()); - // Ignore variable accesses in the inner scope. - try (CloseableScope c2 = ReactiveScope.ignore()) { - assertEquals("world", new ReactiveVariable<>("world").get()); - } - // Variable from the ignoring scope was not recorded. - assertThat(s1.versions().stream().map(x -> x.variable()).collect(toList()), contains(v)); - } - } - @Test - public void block() { - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - // Scope is not blocked by default. - assertFalse(s.blocked()); - // First block() call marks it as blocked. - s.block(); - assertTrue(s.blocked()); - } - } - @Test - public void nonblocking() { - ReactiveScope s1 = new ReactiveScope(); - try (CloseableScope c1 = s1.enter()) { - // Attempting to block in a non-blocking inner scope has no effect on the outer scope. - try (CloseableScope c2 = ReactiveScope.nonblocking()) { - ReactiveScope.current().block(); - } - assertFalse(s1.blocked()); - } - } - @Test - public void freeze() { - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - // Evaluate the Supplier the first time around. - assertEquals("value", s.freeze("key", () -> "value")); - // Keep returning the same value for the key. - assertEquals("value", s.freeze("key", () -> "other")); - // Freezes can be retrieved. - assertEquals(new ReactiveValue<>("value"), s.freezes().get("key")); - // Pre-existing freezes can be configured for the scope. - ReactiveFreezes f = new ReactiveFreezes(); - f.set("key", new ReactiveValue<>("hi")); - s.freezes(f); - assertSame(f, s.freezes()); - assertEquals("hi", s.freeze("key", () -> "other")); - } - } - @Test - public void pin() { - ReactiveScope s1 = new ReactiveScope(); - try (CloseableScope c = s1.enter()) { - // Within single scope, pins behave like freezes. - assertEquals("value", CurrentReactiveScope.pin("key", () -> "value")); - assertEquals("value", CurrentReactiveScope.pin("key", () -> "other")); - } - // After pinning in one scope, we can retrieve the pin in another scope sharing the same pins. - ReactiveScope s2 = new ReactiveScope(); - s2.pins(s1.pins()); - try (CloseableScope c = s2.enter()) { - assertEquals("value", CurrentReactiveScope.pin("key", () -> "other")); - // Pins remain valid until invalidated by blocking. - assertTrue(s2.pins().valid()); - s2.block(); - assertFalse(s2.pins().valid()); - // Once the computation is blocked, pins are not stored, but previously created pins are unaffected. - assertEquals("value", CurrentReactiveScope.pin("key", () -> "other")); - assertEquals("hello", CurrentReactiveScope.pin("alt", () -> "hello")); - assertThat(s2.pins().keys(), contains("key")); - // The pins are however downgraded to freezes and thus stable throughout the current computation. - assertThat(s2.freezes().keys(), containsInAnyOrder("key", "alt")); - assertEquals("hello", CurrentReactiveScope.pin("alt", () -> "hi")); - } - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactiveStateMachineTest.java b/src/test/java/com/machinezoo/hookless/ReactiveStateMachineTest.java deleted file mode 100644 index 0cf0b1b..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactiveStateMachineTest.java +++ /dev/null @@ -1,215 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.*; -import java.util.function.*; -import org.junit.jupiter.api.*; -import com.machinezoo.closeablescope.*; - -public class ReactiveStateMachineTest { - @Test - public void initial() { - // When initial value is provided, it is returned regardless of the supplier output. - ReactiveVariable v = new ReactiveVariable<>("hello"); - ReactiveStateMachine sm = ReactiveStateMachine.supply(new ReactiveValue<>("hi", true), v::get); - assertEquals(new ReactiveValue<>("hi", true), sm.output()); - // By default, this initial value is a blocking exception. - ReactiveValue dv = ReactiveStateMachine.supply(v::get).output(); - assertTrue(dv.blocking()); - assertThat(dv.exception(), instanceOf(ReactiveBlockingException.class)); - } - @Test - public void advance() { - // After advancement, state machine returns output of the supplier. - ReactiveVariable v = new ReactiveVariable<>("hello"); - ReactiveStateMachine sm = ReactiveStateMachine.supply(v::get); - sm.advance(); - assertEquals(new ReactiveValue<>("hello"), sm.output()); - // Changes to reactive variables behind the supplier have no effect without advancement. - v.set("hi"); - assertEquals(new ReactiveValue<>("hello"), sm.output()); - // Further advancement makes changes visible. - sm.advance(); - assertEquals(new ReactiveValue<>("hi"), sm.output()); - // Redundant advancement has no effect. - sm.advance(); - assertEquals(new ReactiveValue<>("hi"), sm.output()); - } - @Test - public void output() { - // Blocking is captured in the inner reactive computation. There doesn't have to be any outer reactive computation at all. - ReactiveVariable v = new ReactiveVariable<>(new ReactiveValue<>("hello", true)); - ReactiveStateMachine sm = ReactiveStateMachine.supply(v::get); - sm.advance(); - assertEquals(new ReactiveValue<>("hello", true), sm.output()); - // Exceptions are captured as well. - v.value(new ReactiveValue<>(new ArithmeticException())); - sm.advance(); - assertThat(sm.output().exception(), instanceOf(CompletionException.class)); - assertThat(sm.output().exception().getCause(), instanceOf(ArithmeticException.class)); - // Wrapping in CompletionException is done by the reactive variable. - // Directly throwing from the supplier gives us unwrapped exception. - sm = ReactiveStateMachine.supply(() -> { - throw new ArithmeticException(); - }); - sm.advance(); - assertThat(sm.output().exception(), instanceOf(ArithmeticException.class)); - } - @Test - public void invalidation() { - // State machine starts in an invalid state. - ReactiveVariable v = new ReactiveVariable<>(new ReactiveValue<>("hello", true)); - ReactiveStateMachine sm = ReactiveStateMachine.supply(v::get); - assertFalse(sm.valid()); - // Advancement moves it to a valid state. - sm.advance(); - assertTrue(sm.valid()); - // Variable changes invalidate it again. - v.set("hi"); - assertFalse(sm.valid()); - // And further advancement makes it valid again. - sm.advance(); - assertTrue(sm.valid()); - } - @Test - public void pinning() { - // As an example, we will pin value of AtomicInteger. - AtomicInteger n = new AtomicInteger(); - ReactiveVariable v = new ReactiveVariable<>(new ReactiveValue<>("hello", true)); - ReactiveStateMachine sm = ReactiveStateMachine.supply(() -> { - // Pin before accessing the variable, because the variable contains blocking value. Blocking would disable pinning. - int p = CurrentReactiveScope.pin("pk", n::incrementAndGet); - return v.get() + " " + p; - }); - // First (blocking) computation executes the pin supplier. - sm.advance(); - assertEquals(new ReactiveValue<>("hello 1", true), sm.output()); - // Second (blocking) computation keeps the pinned value from last blocking computation. - v.value(new ReactiveValue<>("hi", true)); - sm.advance(); - assertEquals(new ReactiveValue<>("hi 1", true), sm.output()); - // Third (non-blocking) computation still uses the pin. - v.set("bye"); - sm.advance(); - assertEquals(new ReactiveValue<>("bye 1"), sm.output()); - // Pin supplier was called only once during the three computations. - assertEquals(1, n.get()); - // Pins are not kept after non-blocking computation completes. - v.set("hello"); - sm.advance(); - assertEquals(new ReactiveValue<>("hello 2"), sm.output()); - } - @Test - public void immediate() { - // Create computation that also invalidates its own dependencies. This often happens in practice. - ReactiveVariable v = new ReactiveVariable<>("hello"); - ReactiveStateMachine sm = ReactiveStateMachine.supply(() -> { - String s = v.get(); - v.set("hi"); - return s; - }); - // We can observe computation output, but we also immediately see the output is already invalidated. - sm.advance(); - assertEquals(new ReactiveValue<>("hello"), sm.output()); - assertFalse(sm.valid()); - // As soon as there is no real change to the variable, we get output that remains valid. - sm.advance(); - assertEquals(new ReactiveValue<>("hi"), sm.output()); - assertTrue(sm.valid()); - } - private static class Triggers { - ReactiveTrigger v = new ReactiveTrigger(); - ReactiveTrigger o = new ReactiveTrigger(); - } - @Test - public void reactivity() { - ReactiveVariable v = new ReactiveVariable<>("hello"); - ReactiveStateMachine sm = ReactiveStateMachine.supply(new ReactiveValue<>("initial"), v::get); - // Initially, valid() and output() have their starting values and no reactive invalidation is signaled. - BiFunction check = (vs, o) -> { - Triggers t = new Triggers(); - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - assertEquals(vs, sm.valid()); - // Accessing valid() never blocks even if the inner computation blocks. - assertFalse(CurrentReactiveScope.blocked()); - t.v.arm(s.versions()); - } - s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - assertEquals(new ReactiveValue<>(o), sm.output()); - // Accessing output() never blocks even if the inner computation blocks. - assertFalse(CurrentReactiveScope.blocked()); - t.o.arm(s.versions()); - } - // There are no reactive invalidations without cause. - assertFalse(t.v.fired()); - assertFalse(t.o.fired()); - return t; - }; - Triggers t = check.apply(false, "initial"); - Runnable advance = () -> { - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - sm.advance(); - // Advancing the state machine never blocks even if the inner computation blocks. - assertFalse(CurrentReactiveScope.blocked()); - try (ReactiveTrigger at = new ReactiveTrigger()) { - at.arm(s.versions()); - // Advancing the state machine never invalidates the current computation. - assertFalse(at.fired()); - } - } - }; - // Advancement both changes the output and marks the current state as valid. - advance.run(); - assertTrue(t.v.fired()); - assertTrue(t.o.fired()); - t = check.apply(true, "hello"); - // Variable change will invalidate the state machine, but it has no effect on output. - v.set("hi"); - assertTrue(t.v.fired()); - assertFalse(t.o.fired()); - t = check.apply(false, "hello"); - // Series of changes have the same effect as a single change. - v.set("bye"); - assertFalse(t.v.fired()); - assertFalse(t.o.fired()); - t = check.apply(false, "hello"); - // Advancement again changes both the output and state validity. - advance.run(); - assertTrue(t.v.fired()); - assertTrue(t.o.fired()); - t = check.apply(true, "bye"); - // Redundant advancement has no effect. - advance.run(); - assertFalse(t.v.fired()); - assertFalse(t.o.fired()); - } - @Test - public void runnable() { - // We can also construct the state machine from Runnable. - ReactiveVariable v = new ReactiveVariable<>("hello"); - ReactiveStateMachine sm = ReactiveStateMachine.run(new ReactiveValue<>(null, true), () -> { - v.get(); - }); - assertEquals(new ReactiveValue<>(null, true), sm.output()); - assertFalse(sm.valid()); - // Advancement does not change the value of course, but it may have side effects, throw exceptions, and signal blocking. - sm.advance(); - assertEquals(new ReactiveValue<>(null), sm.output()); - assertTrue(sm.valid()); - v.value(new ReactiveValue<>(new ArithmeticException())); - assertFalse(sm.valid()); - sm.advance(); - assertThat(sm.output().exception().getCause(), instanceOf(ArithmeticException.class)); - // Initial value is an exception as with Supplier. - ReactiveValue dv = ReactiveStateMachine.run(() -> {}).output(); - assertTrue(dv.blocking()); - assertThat(dv.exception(), instanceOf(ReactiveBlockingException.class)); - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactiveThreadTest.java b/src/test/java/com/machinezoo/hookless/ReactiveThreadTest.java deleted file mode 100644 index 194ffb7..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactiveThreadTest.java +++ /dev/null @@ -1,218 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.awaitility.Awaitility.*; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; -import java.lang.ref.*; -import java.util.*; -import java.util.concurrent.atomic.*; -import org.junit.jupiter.api.*; - -public class ReactiveThreadTest extends TestBase { - ReactiveThread t = new ReactiveThread(); - @AfterEach - public void stop() { - t.stop(); - } - @Test - public void reactive() { - AtomicInteger n = new AtomicInteger(); - ReactiveVariable v = new ReactiveVariable<>("hello"); - t = new ReactiveThread(() -> { - v.get(); - n.incrementAndGet(); - }); - // No action is taken before the thread is explicitly started. - assertEquals(0, n.get()); - // When started, the thread runs once. - t.start(); - await().untilAtomic(n, equalTo(1)); - // It then runs whenever its reactive dependencies change. - v.set("hi"); - await().untilAtomic(n, equalTo(2)); - v.set("bye"); - await().untilAtomic(n, equalTo(3)); - } - @Test - public void runnable() { - AtomicInteger n = new AtomicInteger(); - // Thread's Runnable can be also supplied after constructor is called. - t.runnable(n::incrementAndGet); - assertEquals(0, n.get()); - t.start(); - await().untilAtomic(n, equalTo(1)); - // It however cannot be changed after the thread is started. - assertThrows(IllegalStateException.class, () -> t.runnable(n::decrementAndGet)); - // Runnable must be non-null. - assertThrows(NullPointerException.class, () -> new ReactiveThread(null)); - assertThrows(NullPointerException.class, () -> new ReactiveThread().runnable(null)); - } - @Test - public void overridable() { - AtomicInteger n = new AtomicInteger(); - // If no Runnable is provided, thread's run() method is called. - t = new ReactiveThread() { - @Override - protected void run() { - n.incrementAndGet(); - } - }; - t.start(); - await().untilAtomic(n, equalTo(1)); - } - @Test - public void current() { - assertNull(ReactiveThread.current()); - AtomicInteger n = new AtomicInteger(); - t.runnable(() -> { - assertEquals(t, ReactiveThread.current()); - // Increment to signal that the above assertion passed. - n.incrementAndGet(); - }); - t.start(); - await().untilAtomic(n, equalTo(1)); - assertNull(ReactiveThread.current()); - } - @Test - public void stoppable() { - AtomicInteger n = new AtomicInteger(); - ReactiveVariable v = new ReactiveVariable<>("hello"); - t = new ReactiveThread(() -> { - v.get(); - n.incrementAndGet(); - // Thread can be stopped from within its Runnable. - ReactiveThread.current().stop(); - }); - t.start(); - await().untilAtomic(n, equalTo(1)); - // Once the thread is stopped, it will not run in response to dependency changes. - v.set("hi"); - settle(); - assertEquals(1, n.get()); - } - @Test - public void states() { - AtomicInteger n = new AtomicInteger(); - t = new ReactiveThread(n::incrementAndGet); - // It is safe to start the thread twice. - t.start(); - t.start(); - await().untilAtomic(n, equalTo(1)); - // It is safe to stop the thread twice. - t.stop(); - t.stop(); - // Thread cannot be restarted, but since it is allowed to stop the thread before it is started, restart attempts are silently ignored. - t.start(); - settle(); - assertEquals(1, n.get()); - // It is allowed to stop the thread before it is started. In that case, starting the thread has no effect. - t = new ReactiveThread(n::incrementAndGet); - t.stop(); - t.start(); - settle(); - assertEquals(1, n.get()); - } - @Test - public void pinning() { - AtomicInteger n = new AtomicInteger(); - ReactiveVariable o = new ReactiveVariable<>(); - Map> m = new HashMap<>(); - m.put("a", new ReactiveVariable<>("b")); - ReactiveValue bv = new ReactiveValue<>(new ReactiveBlockingException(), true); - m.put("b", new ReactiveVariable<>(bv)); - t.runnable(() -> { - try { - ReactiveVariable a = m.get("a"); - String p = CurrentReactiveScope.pin("pinkey", () -> a.get()); - ReactiveVariable b = m.get(p); - o.set(b.get()); - } finally { - n.incrementAndGet(); - } - }); - t.start(); - // Initial run will not write the result due to blocking. - await().untilAtomic(n, equalTo(1)); - assertNull(o.get()); - // Thread will run again due to dependency change, but its behavior will not change since the value was already pinned. - m.put("c", new ReactiveVariable<>(bv)); - m.get("a").set("c"); - await().untilAtomic(n, equalTo(2)); - assertNull(o.get()); - // Final result is then based on the pinned value. There will be two iteration due to the way blocking interacts with pinning. - m.get("b").set("hello"); - await().untilAtomic(n, equalTo(4)); - assertEquals("hello", o.get()); - // Pinned value is then discarded and pinning is done anew. - m.put("d", new ReactiveVariable<>(bv)); - m.get("a").set("d"); - await().untilAtomic(n, equalTo(5)); - assertEquals("hello", o.get()); - m.get("c").set("bye"); - await().untilAtomic(n, equalTo(7)); - assertEquals("bye", o.get()); - } - @Test - public void blockingException() { - ReactiveVariable v = new ReactiveVariable<>(new ReactiveValue<>(new ReactiveBlockingException(), true)); - ReactiveVariable o = new ReactiveVariable<>(); - t.runnable(() -> o.set(v.get())); - t.handler((rt, ex) -> o.set("exception")); - // Blocking exception is silently ignored. - t.start(); - settle(); - assertNull(o.get()); - // Reactive thread continues to run when dependencies change. - v.set("hello"); - await().until(o::get, equalTo("hello")); - } - @Test - public void nonblockingException() { - ReactiveVariable v = new ReactiveVariable<>("initial"); - ReactiveVariable o = new ReactiveVariable<>(); - t.runnable(() -> o.set(v.get())); - t.handler((rt, ex) -> o.set("handled")); - t.start(); - await().until(o::get, equalTo("initial")); - // Non-blocking exception causes the uncaught exception handler to be called. - v.value(new ReactiveValue<>(new NumberFormatException())); - await().until(o::get, equalTo("handled")); - // Thread is stopped. Dependency changes don't cause the thread to run again. - v.set("bye"); - settle(); - assertEquals("handled", o.get()); - } - volatile Object pressure; - @Test - public void daemon() { - ReactiveVariable v = new ReactiveVariable<>("hello"); - ReactiveVariable o = new ReactiveVariable<>(); - // Daemon threads run normally like non-daemon threads as long as they are referenced. - ReactiveThread dt = new ReactiveThread(() -> o.set(v.get())) - .daemon(true) - .start(); - await().until(o::get, equalTo("hello")); - // Once the last strong reference is gone, daemon threads may be collected. - WeakReference w = new WeakReference<>(dt); - dt = null; - // Increase pressure on GC until the thread is collected. - while (w.get() != null) - pressure = Arrays.asList(pressure); - } - @Test - public void executor() { - AtomicReference cx = new AtomicReference<>(); - ReactiveThread t = new ReactiveThread(() -> cx.set(ReactiveExecutor.current())); - // Common reactive executor is used by default. - assertSame(ReactiveExecutor.common(), t.executor()); - ReactiveExecutor x = new ReactiveExecutor(); - // Custom executor can be set. - t.executor(x); - assertSame(x, t.executor()); - t.start(); - // Thread runs on the executor. - await().untilAtomic(cx, sameInstance(x)); - x.shutdown(); - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactiveTriggerTest.java b/src/test/java/com/machinezoo/hookless/ReactiveTriggerTest.java deleted file mode 100644 index 67cca20..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactiveTriggerTest.java +++ /dev/null @@ -1,136 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.junit.jupiter.api.Assertions.*; -import java.util.*; -import java.util.concurrent.atomic.*; -import org.junit.jupiter.api.*; - -public class ReactiveTriggerTest { - @Test - public void states() { - try (ReactiveTrigger t = new ReactiveTrigger()) { - // Initial state. - assertFalse(t.armed()); - assertFalse(t.fired()); - assertFalse(t.closed()); - // Armed state. - t.arm(Collections.emptyList()); - assertTrue(t.armed()); - assertFalse(t.fired()); - assertFalse(t.closed()); - // Fired state. - t.fire(); - assertTrue(t.armed()); - assertTrue(t.fired()); - assertFalse(t.closed()); - // Closed state. - t.close(); - assertTrue(t.armed()); - assertTrue(t.fired()); - assertTrue(t.closed()); - } - } - @Test - public void callback() { - try (ReactiveTrigger t = new ReactiveTrigger()) { - AtomicInteger n = new AtomicInteger(0); - t.callback(n::incrementAndGet); - t.arm(Collections.emptyList()); - // No change has been signaled so far. - assertEquals(0, n.get()); - // Firing the trigger causes the callback to be invoked immediately. - t.fire(); - assertEquals(1, n.get()); - // Firing second time has no effect. - t.fire(); - assertEquals(1, n.get()); - } - } - @Test - public void fireOnVariableChange() { - ReactiveVariable v = new ReactiveVariable<>("hello"); - AtomicInteger n = new AtomicInteger(0); - try (ReactiveTrigger t = new ReactiveTrigger()) { - t.callback(n::incrementAndGet); - t.arm(Arrays.asList(new ReactiveVariable.Version(v))); - // No change has been signaled so far. - assertFalse(t.fired()); - assertEquals(0, n.get()); - // Variable write fires the trigger. - v.set("hi"); - assertTrue(t.fired()); - assertEquals(1, n.get()); - } - } - @Test - public void closeAtAnyTime() { - AtomicInteger n = new AtomicInteger(0); - // Closing before arming disables arming and causes firing to be ignored. - try (ReactiveTrigger t = new ReactiveTrigger()) { - t.callback(n::incrementAndGet); - t.close(); - assertFalse(t.armed()); - assertFalse(t.fired()); - assertTrue(t.closed()); - assertThrows(Throwable.class, () -> t.arm(Collections.emptyList())); - t.fire(); - assertFalse(t.fired()); - assertEquals(0, n.get()); - } - // Closing before firing causes firing to be ignored. - try (ReactiveTrigger t = new ReactiveTrigger()) { - t.callback(n::incrementAndGet); - t.arm(Collections.emptyList()); - t.close(); - assertTrue(t.armed()); - assertFalse(t.fired()); - assertTrue(t.closed()); - t.fire(); - assertFalse(t.fired()); - assertEquals(0, n.get()); - } - } - @Test - public void watchManyVariables() { - ReactiveVariable v1 = new ReactiveVariable<>("a"); - ReactiveVariable v2 = new ReactiveVariable<>("b"); - ReactiveVariable v3 = new ReactiveVariable<>("c"); - AtomicInteger n = new AtomicInteger(0); - try (ReactiveTrigger t = new ReactiveTrigger()) { - t.callback(n::incrementAndGet); - t.arm(Arrays.asList(new ReactiveVariable.Version(v1), new ReactiveVariable.Version(v2), new ReactiveVariable.Version(v3))); - assertFalse(t.fired()); - // First variable to change fires the trigger. - v2.set("hi"); - assertTrue(t.fired()); - assertEquals(1, n.get()); - // Subsequent variable changes have no effect. - v3.set("hello"); - assertEquals(1, n.get()); - } - } - @Test - public void fireImmediately() { - ReactiveVariable v = new ReactiveVariable<>("hello"); - AtomicInteger n = new AtomicInteger(0); - try (ReactiveTrigger t = new ReactiveTrigger()) { - t.callback(n::incrementAndGet); - // Arming with an old version causes the trigger to fire immediately. - t.arm(Arrays.asList(new ReactiveVariable.Version(v, v.version() - 1))); - assertTrue(t.fired()); - assertEquals(1, n.get()); - // Fire, if called anyway, then has no effect. - t.fire(); - assertEquals(1, n.get()); - } - } - @Test - public void fireUnarmed() { - try (ReactiveTrigger t = new ReactiveTrigger()) { - // It is possible to fire an unarmed trigger, but it probably isn't useful. - t.fire(); - assertTrue(t.fired()); - } - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactiveValueTest.java b/src/test/java/com/machinezoo/hookless/ReactiveValueTest.java deleted file mode 100644 index 0bd1cf5..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactiveValueTest.java +++ /dev/null @@ -1,114 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.junit.jupiter.api.Assertions.*; -import java.util.concurrent.*; -import org.junit.jupiter.api.*; -import com.machinezoo.closeablescope.*; - -public class ReactiveValueTest { - @Test - public void constructors() { - // Main constructor. - ReactiveValue v = new ReactiveValue<>("hello", null, false); - assertEquals("hello", v.result()); - RuntimeException ex = new RuntimeException(); - v = new ReactiveValue<>(null, ex, true); - assertSame(ex, v.exception()); - assertTrue(v.blocking()); - // ReactiveValue cannot carry both a value and an exception. - assertThrows(IllegalArgumentException.class, () -> new ReactiveValue<>("hello", new RuntimeException(), false)); - // Convenience constructors. - v = new ReactiveValue<>(); - assertNull(v.result()); - assertFalse(v.blocking()); - v = new ReactiveValue<>("hello"); - assertEquals("hello", v.result()); - v = new ReactiveValue<>("hello", true); - assertTrue(v.blocking()); - v = new ReactiveValue<>(ex); - assertSame(ex, v.exception()); - v = new ReactiveValue<>(ex, true); - assertTrue(v.blocking()); - } - @Test - public void equals() { - // Value equality, not reference equality. - String s1 = "hello"; - String s2 = new String(s1); - assertEquals(new ReactiveValue<>(s1), new ReactiveValue<>(s2)); - assertNotEquals(new ReactiveValue<>("some"), new ReactiveValue<>("other")); - // Tolerate nulls. - assertEquals(new ReactiveValue<>(), new ReactiveValue<>()); - // Compare blocking flag. - assertNotEquals(new ReactiveValue<>(s1, true), new ReactiveValue<>(s2, false)); - } - @Test - public void equalsForExceptions() { - // This has to be done in a loop, because two separate exception initializations would differ in line numbers. - Throwable[] same = new Throwable[2]; - for (int i = 0; i < 2; ++i) - same[i] = new RuntimeException("Test exception"); - assertEquals(new ReactiveValue<>(same[0]), new ReactiveValue<>(same[1])); - // Compare exception type. - assertNotEquals(new ReactiveValue<>(new IllegalStateException()), new ReactiveValue<>(new RuntimeException())); - // Compare line number. - assertNotEquals(new ReactiveValue<>(same[0]), new ReactiveValue<>(new RuntimeException("Test exception"))); - // Compare blocking flag for exceptions. - assertNotEquals(new ReactiveValue<>(same[0], true), new ReactiveValue<>(same[1], false)); - // Compare exception message. - Throwable[] named = new Throwable[2]; - for (int i = 0; i < 2; ++i) - named[i] = new RuntimeException("Test exception " + (i + 1)); - assertNotEquals(new ReactiveValue<>(named[0]), new ReactiveValue<>(named[1])); - } - @Test - public void same() { - // Reference equality, not value equality. - String s = "hello"; - assertFalse(new ReactiveValue<>(s).same(new ReactiveValue<>(new String(s)))); - assertTrue(new ReactiveValue<>(s).same(new ReactiveValue<>(s))); - // Compare blocking flag. - assertFalse(new ReactiveValue<>(s, true).same(new ReactiveValue<>(s, false))); - // Compare exception by reference. This has to be done in a loop like in equals() test. - Throwable[] ex = new Throwable[2]; - for (int i = 0; i < 2; ++i) - ex[i] = new RuntimeException("Test exception"); - assertTrue(new ReactiveValue<>(ex[0]).same(new ReactiveValue<>(ex[0]))); - assertFalse(new ReactiveValue<>(ex[0]).same(new ReactiveValue<>(ex[1]))); - } - @Test - public void unpack() { - assertEquals("hello", new ReactiveValue<>("hello").get()); - // Exceptions are wrapped. - RuntimeException ex = new RuntimeException(); - CompletionException ce = assertThrows(CompletionException.class, () -> new ReactiveValue<>(ex).get()); - assertSame(ex, ce.getCause()); - // Blocking flag is propagated. - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - new ReactiveValue<>("hello", true).get(); - assertTrue(s.blocked()); - } - } - @Test - public void pack() { - // Capture simple value. - assertEquals(new ReactiveValue<>("hello"), ReactiveValue.capture(() -> "hello")); - // Capture exception. - RuntimeException ex = new RuntimeException(); - assertEquals(new ReactiveValue<>(ex), ReactiveValue.capture(() -> { - throw ex; - })); - // Capture blocking. - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - assertEquals(new ReactiveValue<>("hello", true), ReactiveValue.capture(() -> { - CurrentReactiveScope.block(); - return "hello"; - })); - // Allow the blocking to be observed by current computation. - assertTrue(s.blocked()); - } - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactiveVariableTest.java b/src/test/java/com/machinezoo/hookless/ReactiveVariableTest.java deleted file mode 100644 index 5ce9b08..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactiveVariableTest.java +++ /dev/null @@ -1,128 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.junit.jupiter.api.Assertions.*; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.*; -import org.junit.jupiter.api.*; -import com.machinezoo.closeablescope.*; - -public class ReactiveVariableTest { - @Test - public void crud() { - // Construct default. - ReactiveVariable v = new ReactiveVariable<>(); - assertNull(v.get()); - // Write and read. - v.set("hello"); - assertEquals("hello", v.get()); - v.set("world"); - assertEquals("world", v.get()); - // Construct non-default. - v = new ReactiveVariable<>("hi"); - assertEquals("hi", v.get()); - // Allow nulls. - v.set(null); - assertNull(v.get()); - } - @Test - public void versions() { - // Versions are incremented by one after every write. First version is 1. - ReactiveVariable v = new ReactiveVariable<>(); - assertEquals(1, v.version()); - v.set("hello"); - assertEquals(2, v.version()); - v.set("world"); - assertEquals(3, v.version()); - } - @Test - public void fireOnChange() { - // The only way to listen to changes in the variable is to setup a trigger. - ReactiveVariable v = new ReactiveVariable<>("hello"); - AtomicInteger c = new AtomicInteger(); - try (ReactiveTrigger t = new ReactiveTrigger()) { - // Trigger can then invoke any callback. - t.callback(c::incrementAndGet); - t.arm(Arrays.asList(new ReactiveVariable.Version(v))); - // The trigger didn't fire yet. - assertEquals(0, c.get()); - // First change fires the trigger. - v.set("hi"); - assertEquals(1, c.get()); - // Further changes have no effect on the same trigger. - v.set("stop"); - assertEquals(1, c.get()); - } - } - @Test - public void trackAccess() { - ReactiveVariable v = new ReactiveVariable<>("hello"); - // Use ReactiveScope to detect variable access. - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - assertEquals("hello", v.get()); - // Variable access has been observed. - assertSame(v, s.versions().stream().findFirst().get().variable()); - } - } - @Test - public void storeReactiveValue() { - // Write and read the value. - assertEquals(new ReactiveValue<>("value"), new ReactiveVariable<>(new ReactiveValue<>("value")).value()); - // Store ordinary value via ReactiveValue. - assertEquals("value", new ReactiveVariable<>(new ReactiveValue<>("value")).get()); - // Store exception. - RuntimeException ex = new RuntimeException(); - CompletionException ce = assertThrows(CompletionException.class, () -> new ReactiveVariable<>(new ReactiveValue<>(ex)).get()); - assertSame(ex, ce.getCause()); - // Store and propagate blocking flag. - ReactiveScope s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - assertEquals("value", new ReactiveVariable<>(new ReactiveValue<>("value", true)).get()); - assertTrue(s.blocked()); - } - // Fire trigger when assigning ReactiveValue. - try (ReactiveTrigger t = new ReactiveTrigger()) { - AtomicInteger c = new AtomicInteger(); - t.callback(c::incrementAndGet); - ReactiveVariable v = new ReactiveVariable<>(); - t.arm(Arrays.asList(new ReactiveVariable.Version(v))); - v.value(new ReactiveValue<>("hi")); - assertEquals(1, c.get()); - } - // Track dependency when reading the variable as a ReactiveValue. - s = new ReactiveScope(); - try (CloseableScope c = s.enter()) { - ReactiveVariable v = new ReactiveVariable<>("value"); - assertEquals(new ReactiveValue<>("value"), v.value()); - assertSame(v, s.versions().stream().findFirst().get().variable()); - } - } - @Test - public void deduplicateWrites() { - String s = "hello"; - ReactiveVariable v = new ReactiveVariable<>(s); - assertEquals(1, v.version()); - // Overwrite with value that is equal but not the same. - v.set(new String(s)); - // Variable behaves as if the second write never happened. - assertEquals(1, v.version()); - assertSame(s, v.get()); - } - @Test - public void disableEquality() { - String s1 = "hello"; - String s2 = new String(s1); - ReactiveVariable v = new ReactiveVariable<>(s1); - v.equality(false); - assertEquals(1, v.version()); - // When value equality checking is disabled, the variable can be overwritten with object that is equal but not the same. - v.set(s2); - assertEquals(2, v.version()); - assertSame(s2, v.get()); - // But writing the same (reference-equal) object doesn't change anything, because reference equality is still checked. - v.set(s2); - assertEquals(2, v.version()); - } -} diff --git a/src/test/java/com/machinezoo/hookless/ReactiveWorkerTest.java b/src/test/java/com/machinezoo/hookless/ReactiveWorkerTest.java deleted file mode 100644 index 4b78d0c..0000000 --- a/src/test/java/com/machinezoo/hookless/ReactiveWorkerTest.java +++ /dev/null @@ -1,187 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.awaitility.Awaitility.*; -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; -import java.util.concurrent.*; -import java.util.function.*; -import org.junit.jupiter.api.*; -import org.junitpioneer.jupiter.*; - -public class ReactiveWorkerTest extends TestBase { - @Test - public void reactive() { - ReactiveVariable v = new ReactiveVariable<>("hello"); - // Output of reactive worker reflects state of underlying dependencies. - ReactiveWorker w = new ReactiveWorker<>(v::get); - await().ignoreExceptions().until(w::get, equalTo("hello")); - // Changes in dependencies are reflected in worker state with a little delay. - v.set("bye"); - await().until(w::get, equalTo("bye")); - } - @Test - public void supplier() { - ReactiveVariable v = new ReactiveVariable<>("hello"); - // Supplier can be provided after the constructor is called. - ReactiveWorker w = new ReactiveWorker<>(); - Supplier s = v::get; - w.supplier(s); - await().ignoreExceptions().until(w::get, equalTo("hello")); - // Supplier can be retrieved. - assertSame(s, w.supplier()); - // It cannot be changed after the worker starts. - assertThrows(IllegalStateException.class, () -> w.supplier(() -> "other")); - // Supplier cannot be null. - assertThrows(NullPointerException.class, () -> new ReactiveWorker<>(null)); - assertThrows(NullPointerException.class, () -> new ReactiveWorker<>().supplier(null)); - // Default supplier just returns null. - ReactiveWorker nw = new ReactiveWorker<>(); - await().ignoreExceptions().until(nw::get, nullValue()); - // If no supplier is set, the supply() method is called by default. - ReactiveWorker ow = new ReactiveWorker() { - @Override - protected String supply() { - // The supply() method can wrap configured supplier. - return supplier().get() + "/bye"; - } - }; - ow.supplier(v::get); - await().ignoreExceptions().until(ow::get, equalTo("hello/bye")); - } - @Test - public void exceptions() { - ReactiveWorker w = new ReactiveWorker<>(() -> { - throw new ArithmeticException(); - }); - // Exceptions are propagated. - await().untilAsserted(() -> { - CompletionException ex = assertThrows(CompletionException.class, w::get); - assertThat(ex.getCause(), instanceOf(ArithmeticException.class)); - }); - } - @Test - public void blocking() { - // Blocking value is propagated as long as initial value is blocking (it is by default). - ReactiveVariable v = new ReactiveVariable<>(new ReactiveValue<>("start", true)); - ReactiveWorker w = new ReactiveWorker<>(v::get); - await().until(() -> ReactiveValue.capture(w::get), equalTo(new ReactiveValue<>("start", true))); - // Further blocking values are also propagated. - v.value(new ReactiveValue<>("blocking", true)); - await().until(() -> ReactiveValue.capture(w::get), equalTo(new ReactiveValue<>("blocking", true))); - // Blocking flag is cleared after first non-blocking computation. - v.set("nonblocking"); - await().until(() -> ReactiveValue.capture(w::get), equalTo(new ReactiveValue<>("nonblocking"))); - // After the first non-blocking result, all future blocking results are ignored. - v.value(new ReactiveValue<>("blocking again", true)); - settle(); - assertEquals(new ReactiveValue<>("nonblocking"), ReactiveValue.capture(w::get)); - } - @RetryingTest(10) - public void initial() { - Supplier s = () -> { - sleep(30); - return "ready"; - }; - // By default, ReactiveBlockingException is thrown before first value is available. - ReactiveWorker w = new ReactiveWorker<>(s); - ReactiveValue iv = ReactiveValue.capture(w::get); - assertTrue(iv.blocking()); - assertThat(iv.exception(), instanceOf(CompletionException.class)); - assertThat(iv.exception().getCause(), instanceOf(ReactiveBlockingException.class)); - // Initial value can be changed. - w = new ReactiveWorker<>(s).initial(new ReactiveValue<>("initial", true)); - assertEquals(new ReactiveValue<>("initial", true), ReactiveValue.capture(w::get)); - // Initial value is blocking by default. - w = new ReactiveWorker<>(s).initial("initial"); - assertEquals(new ReactiveValue<>("initial", true), ReactiveValue.capture(w::get)); - // Initial value cannot be changed once the worker is started. - ReactiveWorker tw = new ReactiveWorker<>(s); - tw.initial("one"); - tw.get(); - assertThrows(IllegalStateException.class, () -> tw.initial("two")); - // Non-blocking initial value will not be replaced by blocking computed value. - ReactiveVariable v = new ReactiveVariable<>(new ReactiveValue<>("blocking", true)); - ReactiveWorker nw = new ReactiveWorker<>(() -> { - sleep(30); - return v.get(); - }); - nw.initial(new ReactiveValue<>("initial")); - assertEquals(new ReactiveValue<>("initial"), ReactiveValue.capture(nw::get)); - settle(); - assertEquals("initial", nw.get()); - // Non-blocking computed value will however replace any initial value. - v.set("ready"); - await().until(() -> nw.get(), equalTo("ready")); - } - @Test - public void equality() { - // Equality checking is enabled by default. - assertTrue(new ReactiveWorker().equality()); - // Create equal but non-identical strings. - String s1 = "hello"; - String s2 = new String(s1); - // Disable equality on reactive variable to make sure changes are propagated to the worker. - ReactiveVariable v = new ReactiveVariable<>(s1); - v.equality(false); - // Worker will initially return the first string. - ReactiveWorker w = new ReactiveWorker<>(v::get); - await().ignoreExceptions().until(() -> w.get(), sameInstance(s1)); - // When second string is provided, reactive variable returns it, but reactive worker sticks to the first string. - v.set(s2); - assertSame(s2, v.get()); - settle(); - assertSame(s1, w.get()); - // When equality checking is disabled, worker always returns the latest value. - v.set(s1); - ReactiveWorker iw = new ReactiveWorker<>(v::get); - iw.equality(false); - await().ignoreExceptions().until(() -> iw.get(), sameInstance(s1)); - v.set(s2); - await().forever().until(() -> iw.get(), sameInstance(s2)); - // Equality cannot be changed once the worker is started. - assertThrows(IllegalStateException.class, () -> iw.equality(true)); - } - @Test - public void pause() { - // Consider an expensive busy-looping worker. - ReactiveVariable v = new ReactiveVariable<>(0L); - ReactiveWorker w = new ReactiveWorker<>(() -> { - v.set(v.get() + 1); - return v.get(); - }); - w.initial(0L); - // Keep it alive by referencing it from a reactive thread. - ReactiveThread t = new ReactiveThread(() -> w.get()); - t.start(); - try { - // Worker keeps running. - await().until(v::get, greaterThan(100L)); - } finally { - t.stop(); - } - // When the thread is stopped and the worker is no longer queried, it ceases to execute. - settle(); - long n = v.get(); - settle(); - assertEquals(n, v.get()); - // The next value read is marked as blocking, because the worker stopped updating it. - assertEquals(new ReactiveValue<>(n, true), ReactiveValue.capture(w::get)); - // Worker starts again and generates non-blocking output. - await().until(() -> !ReactiveValue.capture(w::get).blocking()); - } - @Test - public void executor() { - ReactiveWorker w = new ReactiveWorker<>(() -> ReactiveExecutor.current()); - // Common reactive executor is used by default. - assertSame(ReactiveExecutor.common(), w.executor()); - ReactiveExecutor x = new ReactiveExecutor(); - // Custom executor can be set. - w.executor(x); - assertSame(x, w.executor()); - // Worker runs on the executor. - await().ignoreExceptions().until(w::get, sameInstance(x)); - x.shutdown(); - } -} diff --git a/src/test/java/com/machinezoo/hookless/TestBase.java b/src/test/java/com/machinezoo/hookless/TestBase.java deleted file mode 100644 index 7096b1f..0000000 --- a/src/test/java/com/machinezoo/hookless/TestBase.java +++ /dev/null @@ -1,20 +0,0 @@ -// Part of Hookless: https://hookless.machinezoo.com -package com.machinezoo.hookless; - -import static org.awaitility.Awaitility.*; -import org.awaitility.pollinterval.*; -import org.junit.jupiter.api.*; -import com.machinezoo.noexception.*; - -public abstract class TestBase { - @BeforeAll - public static void awaitility() { - setDefaultPollInterval(new FibonacciPollInterval()); - } - public static void sleep(int millis) { - Exceptions.sneak().run(() -> Thread.sleep(millis)); - } - public static void settle() { - sleep(100); - } -}