├── gradle.properties ├── settings.gradle ├── src ├── main │ └── java │ │ └── com │ │ └── ishland │ │ └── flowsched │ │ ├── package-info.java │ │ ├── executor │ │ ├── LockToken.java │ │ ├── README.md │ │ ├── Task.java │ │ ├── SimpleTask.java │ │ ├── WorkerThread.java │ │ ├── test │ │ │ └── TestThroughput.java │ │ └── ExecutorManager.java │ │ ├── scheduler │ │ ├── ExceptionHandlingAction.java │ │ ├── Cancellable.java │ │ ├── ObjectFactory.java │ │ ├── KeyStatusPair.java │ │ ├── README.md │ │ ├── CancellationSignaller.java │ │ ├── BusyRefCounter.java │ │ ├── ItemStatus.java │ │ ├── TicketSet.java │ │ ├── ItemTicket.java │ │ ├── ItemHolder.java │ │ └── StatusAdvancingScheduler.java │ │ ├── util │ │ └── Assertions.java │ │ └── structs │ │ ├── OneTaskAtATimeExecutor.java │ │ ├── SimpleObjectPool.java │ │ └── DynamicPriorityQueue.java └── test │ └── java │ └── com │ └── ishland │ └── flowsched │ ├── scheduler │ ├── support │ │ ├── TestItem.java │ │ ├── TestContext.java │ │ ├── TestSchedulerImpl.java │ │ └── TestStatus.java │ └── SchedulerTest.java │ ├── executor │ └── ExecutorManagerTest.java │ └── structs │ └── DynamicPriorityQueueTest.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── README.md ├── .gitignore ├── LICENSE ├── .github └── workflows │ └── build.yml ├── gradlew.bat └── gradlew /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.warning.mode=all 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'FlowSched' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/package-info.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched; -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RelativityMC/FlowSched/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/executor/LockToken.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.executor; 2 | 3 | public interface LockToken { 4 | } 5 | -------------------------------------------------------------------------------- /src/test/java/com/ishland/flowsched/scheduler/support/TestItem.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler.support; 2 | 3 | public class TestItem { 4 | } 5 | -------------------------------------------------------------------------------- /src/test/java/com/ishland/flowsched/scheduler/support/TestContext.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler.support; 2 | 3 | public record TestContext(long key, int rng) { 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlowSched 2 | 3 | A collection of scheduling implementations for Java. 4 | 5 | Currently implemented: 6 | - A priority task scheduler for low-overhead locking of multiple resources 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Oct 06 15:30:53 CST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-rc-3-all.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/executor/README.md: -------------------------------------------------------------------------------- 1 | # The Scheduler 2 | 3 | The scheduler takes care of locking multiple resources for tasks in a non-blocking way. 4 | There is no dedicated thread for the scheduler, instead, all worker threads attempts to schedule tasks for themselves. 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/executor/Task.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.executor; 2 | 3 | public interface Task { 4 | 5 | void run(Runnable releaseLocks); 6 | 7 | void propagateException(Throwable t); 8 | 9 | LockToken[] lockTokens(); 10 | 11 | int priority(); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/scheduler/ExceptionHandlingAction.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler; 2 | 3 | public enum ExceptionHandlingAction { 4 | 5 | /** 6 | * Ignore the exception and proceed to continue 7 | */ 8 | PROCEED, 9 | /** 10 | * Abort the transaction and clear all dependencies, marking it broken 11 | */ 12 | MARK_BROKEN, 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/scheduler/Cancellable.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler; 2 | 3 | import java.util.concurrent.atomic.AtomicBoolean; 4 | 5 | public class Cancellable { 6 | 7 | private final AtomicBoolean cancelled = new AtomicBoolean(false); 8 | 9 | public void cancel() { 10 | this.cancelled.set(true); 11 | } 12 | 13 | public boolean isCancelled() { 14 | return this.cancelled.get(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/ 9 | *.iws 10 | *.iml 11 | *.ipr 12 | out/ 13 | !**/src/main/**/out/ 14 | !**/src/test/**/out/ 15 | 16 | ### Eclipse ### 17 | .apt_generated 18 | .classpath 19 | .factorypath 20 | .project 21 | .settings 22 | .springBeans 23 | .sts4-cache 24 | bin/ 25 | !**/src/main/**/bin/ 26 | !**/src/test/**/bin/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | /nbdist/ 33 | /.nb-gradle/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | ### Mac OS ### 39 | .DS_Store -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/util/Assertions.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.util; 2 | 3 | public class Assertions { 4 | 5 | public static void assertTrue(boolean value, String message) { 6 | if (!value) { 7 | final AssertionError error = new AssertionError(message); 8 | error.printStackTrace(); 9 | throw error; 10 | } 11 | } 12 | 13 | public static void assertTrue(boolean state, String format, Object... args) { 14 | if (!state) { 15 | final AssertionError error = new AssertionError(String.format(format, args)); 16 | error.printStackTrace(); 17 | throw error; 18 | } 19 | } 20 | 21 | public static void assertTrue(boolean value) { 22 | if (!value) { 23 | final AssertionError error = new AssertionError(); 24 | error.printStackTrace(); 25 | throw error; 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/executor/SimpleTask.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.executor; 2 | 3 | import java.util.Objects; 4 | 5 | public class SimpleTask implements Task { 6 | 7 | private final Runnable wrapped; 8 | private final int priority; 9 | 10 | public SimpleTask(Runnable wrapped, int priority) { 11 | this.wrapped = Objects.requireNonNull(wrapped); 12 | this.priority = priority; 13 | } 14 | 15 | @Override 16 | public void run(Runnable releaseLocks) { 17 | try { 18 | wrapped.run(); 19 | } finally { 20 | releaseLocks.run(); 21 | } 22 | } 23 | 24 | @Override 25 | public void propagateException(Throwable t) { 26 | t.printStackTrace(); 27 | } 28 | 29 | @Override 30 | public LockToken[] lockTokens() { 31 | return new LockToken[0]; 32 | } 33 | 34 | @Override 35 | public int priority() { 36 | return this.priority; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ishland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/scheduler/ObjectFactory.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler; 2 | 3 | import java.util.Collections; 4 | import java.util.Queue; 5 | import java.util.Set; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.concurrent.ConcurrentLinkedQueue; 8 | import java.util.concurrent.ConcurrentMap; 9 | 10 | public interface ObjectFactory { 11 | 12 | ConcurrentMap createConcurrentHashMap(); 13 | 14 | Set createConcurrentSet(); 15 | 16 | Queue newMPMCQueue(); 17 | 18 | Queue newMPSCQueue(); 19 | 20 | public static class DefaultObjectFactory implements ObjectFactory { 21 | 22 | @Override 23 | public ConcurrentMap createConcurrentHashMap() { 24 | return new ConcurrentHashMap<>(); 25 | } 26 | 27 | @Override 28 | public Set createConcurrentSet() { 29 | return Collections.newSetFromMap(new ConcurrentHashMap<>()); 30 | } 31 | 32 | @Override 33 | public Queue newMPMCQueue() { 34 | return new ConcurrentLinkedQueue<>(); 35 | } 36 | 37 | @Override 38 | public Queue newMPSCQueue() { 39 | return new ConcurrentLinkedQueue<>(); 40 | } 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [pull_request, push] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: checkout repository 9 | uses: actions/checkout@v3 10 | with: 11 | fetch-depth: 0 12 | 13 | - uses: actions/cache@v3 14 | with: 15 | path: | 16 | ~/.gradle/caches 17 | ~/.gradle/wrapper 18 | key: ${{ runner.os }}-gradle0-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 19 | restore-keys: | 20 | ${{ runner.os }}-gradle0- 21 | - name: validate gradle wrapper 22 | uses: gradle/actions/wrapper-validation@v3 23 | - name: setup jdk 17 24 | uses: actions/setup-java@v3 25 | with: 26 | distribution: 'zulu' 27 | java-version: 17 28 | java-package: jdk 29 | - name: build 30 | run: ./gradlew build 31 | 32 | - name: JaCoCo Code Coverage Report 33 | id: jacoco_reporter 34 | uses: PavanMudigonda/jacoco-reporter@v4.8 35 | with: 36 | coverage_results_path: build/reports/jacoco/test/jacocoTestReport.xml 37 | coverage_report_name: Coverage 38 | coverage_report_title: JaCoCo 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | skip_check_run: false 41 | minimum_coverage: 80 42 | fail_below_threshold: false 43 | publish_only_summary: true 44 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/scheduler/KeyStatusPair.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler; 2 | 3 | import java.util.Objects; 4 | 5 | @SuppressWarnings("ClassCanBeRecord") 6 | public final class KeyStatusPair { 7 | private final K key; 8 | private final ItemStatus status; 9 | 10 | public KeyStatusPair(K key, ItemStatus status) { 11 | this.key = key; 12 | this.status = status; 13 | } 14 | 15 | public K key() { 16 | return key; 17 | } 18 | 19 | public ItemStatus status() { 20 | return status; 21 | } 22 | 23 | @Override 24 | public boolean equals(Object obj) { 25 | if (obj == this) return true; 26 | if (obj == null || obj.getClass() != this.getClass()) return false; 27 | var that = (KeyStatusPair) obj; 28 | return Objects.equals(this.key, that.key) && 29 | Objects.equals(this.status, that.status); 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | // inlined Objects.hash(key, status) 35 | int result = 1; 36 | 37 | result = 31 * result + key.hashCode(); 38 | result = 31 * result + status.hashCode(); 39 | 40 | return result; 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | return "KeyStatusPair[" + 46 | "key=" + key + ", " + 47 | "status=" + status + ']'; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/structs/OneTaskAtATimeExecutor.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.structs; 2 | 3 | import java.util.Queue; 4 | import java.util.concurrent.Executor; 5 | import java.util.concurrent.atomic.AtomicBoolean; 6 | 7 | public class OneTaskAtATimeExecutor implements Executor { 8 | 9 | private final AtomicBoolean currentlyRunning = new AtomicBoolean(false); 10 | private final Queue queue; 11 | private final Executor backingExecutor; 12 | private final Runnable task = this::run0; 13 | 14 | public OneTaskAtATimeExecutor(Queue queue, Executor backingExecutor) { 15 | this.backingExecutor = backingExecutor; 16 | this.queue = queue; 17 | } 18 | 19 | private boolean canRun() { 20 | return !this.queue.isEmpty(); 21 | } 22 | 23 | private void run0() { 24 | try { 25 | Runnable command; 26 | while ((command = this.queue.poll()) != null) { 27 | try { 28 | command.run(); 29 | } catch (Throwable t) { 30 | t.printStackTrace(); 31 | } 32 | } 33 | } finally { 34 | this.currentlyRunning.set(false); 35 | this.trySchedule(); 36 | } 37 | } 38 | 39 | private void trySchedule() { 40 | if (!this.queue.isEmpty() && this.needsWakeup()) { 41 | this.backingExecutor.execute(this.task); 42 | } 43 | } 44 | 45 | private boolean needsWakeup() { 46 | return this.currentlyRunning.compareAndSet(false, true); 47 | } 48 | 49 | @Override 50 | public void execute(Runnable command) { 51 | this.queue.add(command); 52 | this.trySchedule(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/ishland/flowsched/scheduler/support/TestSchedulerImpl.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler.support; 2 | 3 | import com.ishland.flowsched.scheduler.ItemHolder; 4 | import com.ishland.flowsched.scheduler.ItemStatus; 5 | import com.ishland.flowsched.scheduler.KeyStatusPair; 6 | import com.ishland.flowsched.scheduler.StatusAdvancingScheduler; 7 | import io.reactivex.rxjava3.core.Scheduler; 8 | import io.reactivex.rxjava3.schedulers.Schedulers; 9 | 10 | import java.util.Random; 11 | import java.util.concurrent.Executor; 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.ThreadFactory; 15 | 16 | public class TestSchedulerImpl extends StatusAdvancingScheduler { 17 | 18 | static final Random GLOBAL_RNG = new Random(); 19 | 20 | private static final ThreadFactory factory = Executors.defaultThreadFactory(); 21 | private static final ExecutorService backgroundExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2, r -> { 22 | final Thread thread = factory.newThread(r); 23 | thread.setDaemon(true); 24 | return thread; 25 | }); 26 | private static final Scheduler backgroundScheduler = Schedulers.from(backgroundExecutor); 27 | 28 | public TestSchedulerImpl() { 29 | } 30 | 31 | @Override 32 | protected Executor getBackgroundExecutor() { 33 | return backgroundExecutor; 34 | } 35 | 36 | @Override 37 | protected Scheduler getSchedulerBackedByBackgroundExecutor() { 38 | return backgroundScheduler; 39 | } 40 | 41 | @Override 42 | protected ItemStatus getUnloadedStatus() { 43 | return TestStatus.STATE_0; 44 | } 45 | 46 | @Override 47 | protected TestContext makeContext(ItemHolder holder, ItemStatus nextStatus, KeyStatusPair[] dependencies, boolean isUpgrade) { 48 | return new TestContext(holder.getKey(), GLOBAL_RNG.nextInt()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/structs/SimpleObjectPool.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.structs; 2 | 3 | import com.ishland.flowsched.util.Assertions; 4 | 5 | import java.util.Objects; 6 | import java.util.function.Consumer; 7 | import java.util.function.Function; 8 | 9 | public class SimpleObjectPool { 10 | 11 | private final Function, T> constructor; 12 | private final Consumer initializer; 13 | private final Consumer postRelease; 14 | private final int size; 15 | 16 | private final Object[] cachedObjects; 17 | private int allocatedCount = 0; 18 | 19 | public SimpleObjectPool(Function, T> constructor, Consumer initializer, Consumer postRelease, int size) { 20 | this.constructor = Objects.requireNonNull(constructor); 21 | this.initializer = Objects.requireNonNull(initializer); 22 | this.postRelease = Objects.requireNonNull(postRelease); 23 | Assertions.assertTrue(size > 0); 24 | this.cachedObjects = new Object[size]; 25 | this.size = size; 26 | 27 | for (int i = 0; i < size; i++) { 28 | final T object = constructor.apply(this); 29 | this.cachedObjects[i] = object; 30 | } 31 | } 32 | 33 | public T alloc() { 34 | final T object; 35 | synchronized (this) { 36 | if (this.allocatedCount >= this.size) { // oversized, falling back to normal alloc 37 | object = this.constructor.apply(this); 38 | return object; 39 | } 40 | 41 | // get an object from the array 42 | final int ordinal = this.allocatedCount++; 43 | object = (T) this.cachedObjects[ordinal]; 44 | this.cachedObjects[ordinal] = null; 45 | } 46 | 47 | this.initializer.accept(object); // initialize the object 48 | 49 | return object; 50 | } 51 | 52 | public void release(T object) { 53 | synchronized (this) { 54 | if (this.allocatedCount == 0) return; // pool is full 55 | this.postRelease.accept(object); 56 | this.cachedObjects[--this.allocatedCount] = object; // store the object into the pool 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/scheduler/README.md: -------------------------------------------------------------------------------- 1 | # Status advancing scheduler 2 | 3 | ## The problem 4 | A Minecraft world is made up of chunks. Each chunk has a status, which is one of the following: 5 | - NEW: The holder of the chunk is created but not loaded 6 | - EMPTY: The chunk is loaded (or created empty if not exists) 7 | - STRUCTURE_STARTS: A world generation state (skipped if already reached) 8 | - STRUCTURE_REFERENCES 9 | - BIOMES 10 | - NOISE 11 | - SURFACE 12 | - CARVERS 13 | - FEATURES 14 | - INITIALIZE_LIGHT 15 | - LIGHT 16 | - SPAWN 17 | - FULL: Hands the chunk over to the server thread 18 | - BLOCK_TICKING: The chunk is being ticked 19 | - ENTITY_TICKING: The chunk is being ticked 20 | 21 | Notes: 22 | - All statuses are ordered. A chunk can only reach a status if itself have reached the status before. 23 | - Some of the statuses depends on neighbor chunks to reach a certain status. 24 | For example, a chunk cannot reach `STRUCTURE_REFERENCES` status until all its 17x17 neighbors reach `STRUCTURE_STARTS` status. 25 | - All tasks require locking the chunk itself. 26 | - The tasks for certain statuses requires locking neighbor chunks. 27 | For example, a chunk attempting to reach `FEATURES` status requires locking all its 3x3 neighbors. 28 | - Chunks can be downgraded. 29 | 30 | ## The solution 31 | The scheduler is a thread that advances the status of chunks designed to be simple and efficient. 32 | 33 | ### The algorithm 34 | Chunks are stored in a hash map (position -> chunk holder). 35 | Every chunk holder holds tickets to maintain its status. 36 | 37 | A ticket contains: 38 | - The source of the ticket (the position of the requester) 39 | - The status to be reached 40 | - A callback to be called when the status is reached 41 | 42 | The algorithm is simple: 43 | - The scheduler maintains a queue of chunks to be updated. 44 | - The scheduler picks a chunk from the queue and schedules a task to advance or downgrade its status. 45 | - For advancing: 46 | - If the chunk requires neighbor chunks to be loaded to a certain status, it places a ticket to the neighbor chunks. 47 | - Otherwise, the task for the status is scheduled to run. 48 | - For downgrading: 49 | - If the chunk requires neighbor chunks to be loaded to a certain status, it removes the ticket to the neighbor chunks. 50 | - Then, the task for the status is scheduled to run. 51 | 52 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/scheduler/CancellationSignaller.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler; 2 | 3 | import it.unimi.dsi.fastutil.objects.ReferenceArrayList; 4 | import it.unimi.dsi.fastutil.objects.ReferenceList; 5 | 6 | import java.util.Objects; 7 | import java.util.Optional; 8 | import java.util.concurrent.atomic.AtomicBoolean; 9 | import java.util.concurrent.atomic.AtomicReference; 10 | import java.util.function.Consumer; 11 | 12 | public class CancellationSignaller { 13 | 14 | public static final CancellationSignaller COMPLETED; 15 | 16 | static { 17 | final CancellationSignaller signaller = new CancellationSignaller(unused -> { 18 | }); 19 | signaller.finished.set(Optional.empty()); 20 | signaller.cancelled.set(true); 21 | COMPLETED = signaller; 22 | } 23 | 24 | private final ReferenceList> onComplete = new ReferenceArrayList<>(); 25 | private final Consumer cancel; 26 | private final AtomicBoolean cancelled = new AtomicBoolean(false); 27 | private final AtomicReference> finished = new AtomicReference<>(null); 28 | 29 | public CancellationSignaller(Consumer cancel) { 30 | this.cancel = Objects.requireNonNull(cancel); 31 | } 32 | 33 | public void fireComplete(Throwable throwable) { 34 | if (finished.compareAndSet(null, Optional.ofNullable(throwable))) { 35 | final Consumer[] consumers; 36 | synchronized (this) { 37 | consumers = onComplete.toArray(Consumer[]::new); 38 | onComplete.clear(); 39 | } 40 | for (Consumer consumer : consumers) { 41 | try { 42 | consumer.accept(throwable); 43 | } catch (Throwable t) { 44 | t.printStackTrace(); 45 | } 46 | } 47 | } 48 | } 49 | 50 | public void addListener(Consumer callback) { 51 | if (finished.get() != null) { 52 | callback.accept(finished.get().orElse(null)); 53 | return; 54 | } 55 | synchronized (this) { 56 | if (finished.get() != null) { 57 | callback.accept(finished.get().orElse(null)); 58 | return; 59 | } 60 | onComplete.add(callback); 61 | } 62 | } 63 | 64 | public void cancel() { 65 | if (this.cancelled.compareAndSet(false, true)) { 66 | this.cancel.accept(this); 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/ishland/flowsched/executor/ExecutorManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.executor; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | import org.mockito.junit.jupiter.MockitoSettings; 9 | import org.mockito.quality.Strictness; 10 | 11 | import java.util.concurrent.CompletableFuture; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import static org.mockito.Mockito.*; 15 | 16 | @ExtendWith(MockitoExtension.class) 17 | @MockitoSettings(strictness = Strictness.STRICT_STUBS) 18 | class ExecutorManagerTest { 19 | 20 | private ExecutorManager manager; 21 | 22 | @BeforeEach 23 | void setUp() { 24 | manager = new ExecutorManager(4); 25 | } 26 | 27 | @Test 28 | void testSimpleSubmit() { 29 | Task task = mock(); 30 | 31 | when(task.priority()).thenReturn(3); 32 | when(task.lockTokens()).thenReturn(new LockToken[0]); 33 | final CompletableFuture future = new CompletableFuture<>(); 34 | doAnswer(invocation -> { 35 | future.complete(null); 36 | return null; 37 | }).when(task).run(any()); 38 | manager.schedule(task); 39 | future.orTimeout(30, TimeUnit.SECONDS).join(); 40 | verify(task, atLeastOnce()).priority(); 41 | verify(task, atLeastOnce()).lockTokens(); 42 | verify(task, times(1)).run(any()); 43 | verify(task, never()).propagateException(any()); 44 | verifyNoMoreInteractions(task); 45 | } 46 | 47 | @Test 48 | void testSimpleSubmitException() { 49 | Task task = mock(); 50 | 51 | when(task.priority()).thenReturn(3); 52 | when(task.lockTokens()).thenReturn(new LockToken[0]); 53 | final CompletableFuture future = new CompletableFuture<>(); 54 | doAnswer(invocation -> { 55 | future.complete(null); 56 | throw new RuntimeException("Test exception"); 57 | }).when(task).run(any()); 58 | manager.schedule(task); 59 | future.orTimeout(30, TimeUnit.SECONDS).join(); 60 | verify(task, atLeastOnce()).priority(); 61 | verify(task, atLeastOnce()).lockTokens(); 62 | verify(task, times(1)).run(any()); 63 | verify(task, times(1)).propagateException(any()); 64 | verifyNoMoreInteractions(task); 65 | } 66 | 67 | @AfterEach 68 | void tearDown() { 69 | manager.shutdown(); 70 | validateMockitoUsage(); 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/scheduler/BusyRefCounter.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler; 2 | 3 | import com.ishland.flowsched.util.Assertions; 4 | import it.unimi.dsi.fastutil.objects.ReferenceArrayList; 5 | import it.unimi.dsi.fastutil.objects.ReferenceList; 6 | 7 | import java.lang.invoke.VarHandle; 8 | import java.util.Objects; 9 | 10 | public class BusyRefCounter { 11 | 12 | private final ReferenceList onComplete = new ReferenceArrayList<>(); 13 | private Runnable onCompleteOnce = null; 14 | private volatile int counter = 0; 15 | 16 | public synchronized boolean isBusy() { 17 | return counter != 0; 18 | } 19 | 20 | public void addListener(Runnable runnable) { 21 | Objects.requireNonNull(runnable); 22 | boolean runNow = false; 23 | synchronized (this) { 24 | if (!isBusy()) { 25 | runNow = true; 26 | } else { 27 | onComplete.add(runnable); 28 | } 29 | } 30 | if (runNow) { 31 | runnable.run(); 32 | } 33 | } 34 | 35 | public void addListenerOnce(Runnable runnable) { 36 | Objects.requireNonNull(runnable); 37 | boolean runNow = false; 38 | synchronized (this) { 39 | if (!isBusy()) { 40 | runNow = true; 41 | } else { 42 | onCompleteOnce = runnable; 43 | } 44 | } 45 | if (runNow) { 46 | runnable.run(); 47 | } 48 | } 49 | 50 | public synchronized void incrementRefCount() { 51 | counter ++; 52 | } 53 | 54 | public void decrementRefCount() { 55 | Runnable[] onCompleteArray = null; 56 | Runnable onCompleteOnce = null; 57 | synchronized (this) { 58 | Assertions.assertTrue(counter > 0); 59 | if (--counter == 0) { 60 | onCompleteArray = onComplete.toArray(Runnable[]::new); 61 | onComplete.clear(); 62 | } 63 | onCompleteOnce = this.onCompleteOnce; 64 | this.onCompleteOnce = null; 65 | } 66 | if (onCompleteArray != null) { 67 | for (Runnable runnable : onCompleteArray) { 68 | try { 69 | runnable.run(); 70 | } catch (Throwable t) { 71 | t.printStackTrace(); 72 | } 73 | } 74 | } 75 | if (onCompleteOnce != null) { 76 | try { 77 | onCompleteOnce.run(); 78 | } catch (Throwable t) { 79 | t.printStackTrace(); 80 | } 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/executor/WorkerThread.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.executor; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.concurrent.atomic.AtomicBoolean; 7 | import java.util.concurrent.locks.LockSupport; 8 | 9 | public class WorkerThread extends Thread { 10 | 11 | private static final Logger LOGGER = LoggerFactory.getLogger("FlowSched Executor Worker Thread"); 12 | 13 | private final ExecutorManager executorManager; 14 | private volatile boolean shutdown = false; 15 | 16 | public WorkerThread(ExecutorManager executorManager) { 17 | this.executorManager = executorManager; 18 | } 19 | 20 | @Override 21 | public void run() { 22 | main_loop: 23 | while (true) { 24 | this.executorManager.waitObj.acquireUninterruptibly(); 25 | 26 | if (this.shutdown) { 27 | return; 28 | } 29 | while (!this.shutdown && !pollTasks()) { 30 | Thread.onSpinWait(); 31 | } 32 | } 33 | } 34 | 35 | private boolean pollTasks() { 36 | Task task = this.executorManager.getGlobalWorkQueue().dequeue(); 37 | if (task == null) { 38 | return false; 39 | } 40 | if (!this.executorManager.tryLock(task)) { 41 | return true; // polled 42 | } 43 | try { 44 | AtomicBoolean released = new AtomicBoolean(false); 45 | try { 46 | task.run(() -> { 47 | if (released.compareAndSet(false, true)) { 48 | executorManager.releaseLocks(task); 49 | } 50 | }); 51 | } catch (Throwable t) { 52 | try { 53 | if (released.compareAndSet(false, true)) { 54 | executorManager.releaseLocks(task); 55 | } 56 | } catch (Throwable t1) { 57 | t.addSuppressed(t1); 58 | LOGGER.error("Exception thrown while releasing locks", t); 59 | } 60 | try { 61 | task.propagateException(t); 62 | } catch (Throwable t1) { 63 | t.addSuppressed(t1); 64 | LOGGER.error("Exception thrown while propagating exception", t); 65 | } 66 | } 67 | return true; 68 | } catch (Throwable t) { 69 | LOGGER.error("Exception thrown while executing task", t); 70 | return true; 71 | } 72 | } 73 | 74 | public void shutdown() { 75 | shutdown = true; 76 | } 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/executor/test/TestThroughput.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.executor.test; 2 | 3 | import com.ishland.flowsched.executor.LockToken; 4 | import com.ishland.flowsched.executor.ExecutorManager; 5 | import com.ishland.flowsched.executor.Task; 6 | 7 | import java.util.Arrays; 8 | import java.util.concurrent.ExecutorService; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.Semaphore; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | 13 | public class TestThroughput { 14 | 15 | private static final Semaphore semaphore = new Semaphore(1 << 7); 16 | 17 | public static volatile double accumulation = 0; 18 | public static volatile long[] latency = new long[1 << 20]; 19 | public static AtomicInteger counter = new AtomicInteger(); 20 | 21 | public static void main(String[] args) { 22 | final ExecutorManager manager = new ExecutorManager(4); 23 | long last = System.nanoTime(); 24 | int schedules = 0; 25 | final ExecutorService pool = Executors.newFixedThreadPool(4); 26 | while (true) { 27 | if (schedules >= 1 << 20) { 28 | final long now = System.nanoTime(); 29 | System.out.println(String.format("Throughput: %.2f rps, latency: %.2fns, acc: %e", schedules * 1e9 / (now - last), Arrays.stream(latency).average().getAsDouble(), accumulation)); 30 | last = now; 31 | schedules = 0; 32 | } 33 | semaphore.acquireUninterruptibly(); 34 | schedules ++; 35 | // manager.schedule(new DummyTask()); 36 | pool.execute(new DummyTask()); 37 | } 38 | } 39 | 40 | static class DummyTask implements Task, Runnable { 41 | 42 | public static final Runnable EMPTY_RUNNABLE = () -> {}; 43 | 44 | private final long start = System.nanoTime(); 45 | 46 | @Override 47 | public void run() { 48 | run(EMPTY_RUNNABLE); 49 | } 50 | 51 | @Override 52 | public void run(Runnable releaseLocks) { 53 | // for (int i = 0; i < 1 << 8; i ++) accumulation += Math.random(); 54 | final long end = System.nanoTime(); 55 | latency[counter.getAndIncrement() & (latency.length - 1)] = end - start; 56 | semaphore.release(); 57 | releaseLocks.run(); 58 | } 59 | 60 | @Override 61 | public void propagateException(Throwable t) { 62 | 63 | } 64 | 65 | @Override 66 | public LockToken[] lockTokens() { 67 | return new LockToken[0]; 68 | } 69 | 70 | @Override 71 | public int priority() { 72 | return (int) (Math.random() * 128); 73 | } 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/test/java/com/ishland/flowsched/scheduler/support/TestStatus.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler.support; 2 | 3 | import com.ishland.flowsched.scheduler.Cancellable; 4 | import com.ishland.flowsched.scheduler.ItemHolder; 5 | import com.ishland.flowsched.scheduler.ItemStatus; 6 | import com.ishland.flowsched.scheduler.KeyStatusPair; 7 | import com.ishland.flowsched.util.Assertions; 8 | import io.reactivex.rxjava3.core.Completable; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.concurrent.CancellationException; 13 | 14 | public enum TestStatus implements ItemStatus, Comparable { 15 | STATE_0, 16 | STATE_1, 17 | STATE_2, 18 | STATE_3, 19 | STATE_4, 20 | STATE_5, 21 | STATE_6, 22 | STATE_7, 23 | STATE_8, 24 | ; 25 | 26 | public static final ItemStatus[] All_STATUSES = List.of(values()).toArray(ItemStatus[]::new); 27 | 28 | @Override 29 | public ItemStatus[] getAllStatuses() { 30 | return All_STATUSES; 31 | } 32 | 33 | @Override 34 | public Completable upgradeToThis(TestContext context, Cancellable cancellable) { 35 | // System.out.println(String.format("Upgrading %d to %s", context.key(), this)); 36 | if (TestSchedulerImpl.GLOBAL_RNG.nextBoolean()) { 37 | cancellable.cancel(); 38 | return Completable.error(new CancellationException()); 39 | } 40 | return Completable.complete(); 41 | } 42 | 43 | @Override 44 | public Completable postUpgradeToThis(TestContext context) { 45 | return Completable.complete(); 46 | } 47 | 48 | 49 | @Override 50 | public Completable preDowngradeFromThis(TestContext context, Cancellable cancellable) { 51 | if ((context.rng() & 1) == 0) { 52 | cancellable.cancel(); 53 | return Completable.error(new CancellationException()); 54 | } 55 | return Completable.complete(); 56 | } 57 | 58 | 59 | @Override 60 | public Completable downgradeFromThis(TestContext context, Cancellable cancellable) { 61 | if ((context.rng() & 1) == 0) { 62 | Assertions.assertTrue(false, "erroneous call to downgradeFromThis"); 63 | } 64 | // System.out.println(String.format("Downgrading %d from %s", context.key(), this)); 65 | return Completable.complete(); 66 | } 67 | 68 | @Override 69 | public KeyStatusPair[] getDependencies(ItemHolder holder) { 70 | final ItemStatus prev = this.getPrev(); 71 | if (prev == null || prev == STATE_0) return new KeyStatusPair[0]; 72 | List> deps = new ArrayList<>(); 73 | for (long i = 0; i < holder.getKey(); i ++) { 74 | deps.add(new KeyStatusPair<>(i, prev)); 75 | } 76 | // System.out.println(String.format("Dependencies of %d at %s: %s", holder.getKey(), status, deps)); 77 | return deps.toArray(KeyStatusPair[]::new); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/scheduler/ItemStatus.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler; 2 | 3 | import io.reactivex.rxjava3.core.Completable; 4 | import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; 5 | 6 | /** 7 | * Represents the status of an item. 8 | *

9 | * Implementations must also implement {@link Comparable}, and higher statuses must be greater than lower statuses. 10 | * 11 | * @param the context type 12 | */ 13 | public interface ItemStatus { 14 | 15 | @SuppressWarnings("rawtypes") 16 | static KeyStatusPair[] EMPTY_DEPENDENCIES = new KeyStatusPair[0]; 17 | 18 | default ItemStatus getPrev() { 19 | if (this.ordinal() > 0) { 20 | return getAllStatuses()[this.ordinal() - 1]; 21 | } else { 22 | return null; 23 | } 24 | } 25 | 26 | default ItemStatus getNext() { 27 | final ItemStatus[] allStatuses = getAllStatuses(); 28 | if (this.ordinal() < allStatuses.length - 1) { 29 | return allStatuses[this.ordinal() + 1]; 30 | } else { 31 | return null; 32 | } 33 | } 34 | 35 | ItemStatus[] getAllStatuses(); 36 | 37 | int ordinal(); 38 | 39 | Completable upgradeToThis(Ctx context, Cancellable cancellable); 40 | 41 | Completable postUpgradeToThis(Ctx context); 42 | 43 | Completable preDowngradeFromThis(Ctx context, Cancellable cancellable); 44 | 45 | /** 46 | * @implNote cancelling the given cancellable here is discouraged. If implementations do cancel here, postUpgrade hook will not be called after this. 47 | */ 48 | Completable downgradeFromThis(Ctx context, Cancellable cancellable); 49 | 50 | /** 51 | * Get the dependencies of the given item at the given status. 52 | *

53 | * The returned collection must not contain the given item itself. 54 | * 55 | * @param holder the item holder 56 | * @return the dependencies 57 | */ 58 | KeyStatusPair[] getDependencies(ItemHolder holder); 59 | 60 | default KeyStatusPair[] getDependenciesToRemove(ItemHolder holder) { 61 | final KeyStatusPair[] curDep = holder.getDependencies(this); 62 | final KeyStatusPair[] newDep = this.getDependencies(holder); 63 | final ObjectOpenHashSet> toRemove = new ObjectOpenHashSet<>(curDep); 64 | for (KeyStatusPair pair : newDep) { 65 | toRemove.remove(pair); 66 | } 67 | return toRemove.toArray(KeyStatusPair[]::new); 68 | } 69 | 70 | default KeyStatusPair[] getDependenciesToAdd(ItemHolder holder) { 71 | final KeyStatusPair[] curDep = holder.getDependencies(this); 72 | final KeyStatusPair[] newDep = this.getDependencies(holder); 73 | final ObjectOpenHashSet> toAdd = new ObjectOpenHashSet<>(newDep); 74 | for (KeyStatusPair pair : curDep) { 75 | toAdd.remove(pair); 76 | } 77 | return toAdd.toArray(KeyStatusPair[]::new); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/scheduler/TicketSet.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler; 2 | 3 | import com.ishland.flowsched.util.Assertions; 4 | 5 | import java.lang.invoke.MethodHandles; 6 | import java.lang.invoke.VarHandle; 7 | import java.util.Set; 8 | import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; 9 | 10 | public class TicketSet { 11 | 12 | private final ItemStatus initialStatus; 13 | private final Set>[] status2Tickets; 14 | private final int[] status2TicketsSize; 15 | private volatile int targetStatus = 0; 16 | 17 | public TicketSet(ItemStatus initialStatus, ObjectFactory objectFactory) { 18 | this.initialStatus = initialStatus; 19 | this.targetStatus = initialStatus.ordinal(); 20 | ItemStatus[] allStatuses = initialStatus.getAllStatuses(); 21 | this.status2Tickets = new Set[allStatuses.length]; 22 | for (int i = 0; i < allStatuses.length; i++) { 23 | this.status2Tickets[i] = objectFactory.createConcurrentSet(); 24 | } 25 | this.status2TicketsSize = new int[allStatuses.length]; 26 | VarHandle.fullFence(); 27 | } 28 | 29 | public boolean checkAdd(ItemTicket ticket) { 30 | ItemStatus targetStatus = ticket.getTargetStatus(); 31 | final boolean added = this.status2Tickets[targetStatus.ordinal()].add(ticket); 32 | return added; 33 | } 34 | 35 | /** 36 | * Not thread-safe 37 | */ 38 | public void addUnchecked(ItemTicket ticket) { 39 | ItemStatus targetStatus = ticket.getTargetStatus(); 40 | this.status2TicketsSize[targetStatus.ordinal()] ++; 41 | this.updateTargetStatus(); 42 | } 43 | 44 | public boolean checkRemove(ItemTicket ticket) { 45 | ItemStatus targetStatus = ticket.getTargetStatus(); 46 | final boolean removed = this.status2Tickets[targetStatus.ordinal()].remove(ticket); 47 | return removed; 48 | } 49 | 50 | /** 51 | * Not thread-safe 52 | */ 53 | public void removeUnchecked(ItemTicket ticket) { 54 | ItemStatus targetStatus = ticket.getTargetStatus(); 55 | this.status2TicketsSize[targetStatus.ordinal()] --; 56 | this.updateTargetStatus(); 57 | } 58 | 59 | private void updateTargetStatus() { 60 | this.targetStatus = this.computeTargetStatusSlow(); 61 | } 62 | 63 | /** 64 | * Not thread-safe 65 | */ 66 | public ItemStatus getTargetStatus() { 67 | return this.initialStatus.getAllStatuses()[this.targetStatus]; 68 | } 69 | 70 | public Set> getTicketsForStatus(ItemStatus status) { 71 | return this.status2Tickets[status.ordinal()]; 72 | } 73 | 74 | void clear() { 75 | for (Set> tickets : status2Tickets) { 76 | tickets.clear(); 77 | } 78 | 79 | VarHandle.fullFence(); 80 | } 81 | 82 | void assertEmpty() { 83 | for (Set> tickets : status2Tickets) { 84 | Assertions.assertTrue(tickets.isEmpty()); 85 | } 86 | } 87 | 88 | private int computeTargetStatusSlow() { 89 | for (int i = this.status2Tickets.length - 1; i > 0; i--) { 90 | if (this.status2TicketsSize[i] > 0) { 91 | return i; 92 | } 93 | } 94 | return 0; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/scheduler/ItemTicket.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler; 2 | 3 | import java.util.Objects; 4 | import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; 5 | 6 | public class ItemTicket { 7 | 8 | private static final AtomicReferenceFieldUpdater CALLBACK_UPDATER = AtomicReferenceFieldUpdater.newUpdater(ItemTicket.class, Runnable.class, "callback"); 9 | 10 | private final TicketType type; 11 | private final Object source; 12 | private final ItemStatus targetStatus; 13 | private volatile Runnable callback = null; 14 | // private int hash = 0; 15 | 16 | public ItemTicket(TicketType type, Object source, ItemStatus targetStatus, Runnable callback) { 17 | this.type = Objects.requireNonNull(type); 18 | this.source = Objects.requireNonNull(source); 19 | this.targetStatus = Objects.requireNonNull(targetStatus); 20 | this.callback = callback; 21 | } 22 | 23 | public Object getSource() { 24 | return this.source; 25 | } 26 | 27 | public ItemStatus getTargetStatus() { 28 | return this.targetStatus; 29 | } 30 | 31 | public TicketType getType() { 32 | return this.type; 33 | } 34 | 35 | public void consumeCallback() { 36 | Runnable callback = CALLBACK_UPDATER.getAndSet(this, null); 37 | if (callback != null) { 38 | try { 39 | callback.run(); 40 | } catch (Throwable t) { 41 | t.printStackTrace(); 42 | } 43 | } 44 | } 45 | 46 | @Override 47 | public boolean equals(Object o) { 48 | if (this == o) return true; 49 | if (o == null || getClass() != o.getClass()) return false; 50 | ItemTicket that = (ItemTicket) o; 51 | return type == that.type && Objects.equals(source, that.source) && Objects.equals(targetStatus, that.targetStatus); 52 | } 53 | 54 | // public boolean equalsAlternative(ItemTicket that) { 55 | // if (this == that) return true; 56 | // if (that == null) return false; 57 | // return type == that.type && Objects.equals(source, that.source); 58 | // } 59 | 60 | @Override 61 | public int hashCode() { 62 | // inlined version of Objects.hash(type, source, targetStatus) 63 | int result = 1; 64 | 65 | result = 31 * result + type.hashCode(); 66 | result = 31 * result + source.hashCode(); 67 | result = 31 * result + targetStatus.hashCode(); 68 | return result; 69 | } 70 | 71 | // public int hashCodeAlternative() { 72 | // int hc = hash; 73 | // if (hc == 0) { 74 | // // inlined version of Objects.hash(type, source, targetStatus) 75 | // int result = 1; 76 | // 77 | // result = 31 * result + type.hashCode(); 78 | // result = 31 * result + source.hashCode(); 79 | // hc = hash = result; 80 | // } 81 | // return hc; 82 | // } 83 | 84 | public static class TicketType { 85 | public static TicketType DEPENDENCY = new TicketType("flowsched:dependency"); 86 | public static TicketType EXTERNAL = new TicketType("flowsched:external"); 87 | 88 | private final String description; 89 | 90 | public TicketType(String description) { 91 | this.description = description; 92 | } 93 | 94 | public String getDescription() { 95 | return this.description; 96 | } 97 | 98 | // use default equals() and hashCode() 99 | 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/structs/DynamicPriorityQueue.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.structs; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | import java.util.concurrent.ConcurrentLinkedQueue; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | import java.util.concurrent.atomic.AtomicIntegerArray; 7 | 8 | /** 9 | * A priority queue with fixed number of priorities and allows changing priorities of elements. 10 | * 11 | * @param the type of elements held in this collection 12 | */ 13 | public class DynamicPriorityQueue { 14 | 15 | private final AtomicIntegerArray taskCount; 16 | private final ConcurrentLinkedQueue[] priorities; 17 | private final ConcurrentHashMap priorityMap = new ConcurrentHashMap<>(); 18 | 19 | public DynamicPriorityQueue(int priorityCount) { 20 | this.taskCount = new AtomicIntegerArray(priorityCount); 21 | //noinspection unchecked 22 | this.priorities = new ConcurrentLinkedQueue[priorityCount]; 23 | for (int i = 0; i < priorityCount; i++) { 24 | this.priorities[i] = new ConcurrentLinkedQueue<>(); 25 | } 26 | } 27 | 28 | public void enqueue(E element, int priority) { 29 | if (priority < 0 || priority >= priorities.length) 30 | throw new IllegalArgumentException("Priority out of range"); 31 | if (this.priorityMap.putIfAbsent(element, priority) != null) 32 | throw new IllegalArgumentException("Element already in queue"); 33 | 34 | this.priorities[priority].add(element); 35 | this.taskCount.incrementAndGet(priority); 36 | } 37 | 38 | // behavior is undefined when changing priority for one item concurrently 39 | public boolean changePriority(E element, int priority) { 40 | if (priority < 0 || priority >= priorities.length) 41 | throw new IllegalArgumentException("Priority out of range"); 42 | 43 | int currentPriority = this.priorityMap.getOrDefault(element, -1); 44 | if (currentPriority == -1 || currentPriority == priority) { 45 | return false; // a clear failure 46 | } 47 | final boolean removedFromQueue = this.priorities[currentPriority].remove(element); 48 | if (!removedFromQueue) { 49 | return false; // the element is dequeued while we are changing priority 50 | } 51 | this.taskCount.decrementAndGet(currentPriority); 52 | final Integer put = this.priorityMap.put(element, priority); 53 | final boolean changeSuccess = put != null && put == currentPriority; 54 | if (!changeSuccess) { 55 | return false; // something else may have called remove() 56 | } 57 | this.priorities[priority].add(element); 58 | this.taskCount.incrementAndGet(priority); 59 | return true; 60 | } 61 | 62 | public E dequeue() { 63 | for (int i = 0; i < this.priorities.length; i ++) { 64 | if (this.taskCount.get(i) == 0) continue; 65 | E element = priorities[i].poll(); 66 | if (element != null) { 67 | this.taskCount.decrementAndGet(i); 68 | this.priorityMap.remove(element); 69 | return element; 70 | } 71 | } 72 | return null; 73 | } 74 | 75 | public boolean contains(E element) { 76 | return priorityMap.containsKey(element); 77 | } 78 | 79 | public void remove(E element) { 80 | final Integer remove = this.priorityMap.remove(element); 81 | if (remove == null) return; 82 | boolean removed = this.priorities[remove].remove(element); // best-effort 83 | if (removed) this.taskCount.decrementAndGet(remove); 84 | } 85 | 86 | public int size() { 87 | return priorityMap.size(); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/com/ishland/flowsched/scheduler/SchedulerTest.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler; 2 | 3 | import com.ishland.flowsched.scheduler.support.TestContext; 4 | import com.ishland.flowsched.scheduler.support.TestItem; 5 | import com.ishland.flowsched.scheduler.support.TestSchedulerImpl; 6 | import com.ishland.flowsched.scheduler.support.TestStatus; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | import org.mockito.junit.jupiter.MockitoSettings; 12 | import org.mockito.quality.Strictness; 13 | 14 | import java.util.ArrayList; 15 | import java.util.Random; 16 | import java.util.concurrent.CompletableFuture; 17 | import java.util.concurrent.atomic.AtomicBoolean; 18 | import java.util.concurrent.locks.LockSupport; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | @MockitoSettings(strictness = Strictness.STRICT_STUBS) 22 | public class SchedulerTest { 23 | 24 | @Test 25 | public void testSimple() { 26 | final TestSchedulerImpl scheduler = new TestSchedulerImpl(); 27 | long startTime = System.nanoTime(); 28 | 29 | final long key = 1024L; 30 | AtomicBoolean spamLoaderRunning = new AtomicBoolean(true); 31 | ArrayList> spammedFutures = new ArrayList<>(); 32 | 33 | Thread spamLoader = new Thread(() -> { 34 | Random random = new Random(); 35 | while (spamLoaderRunning.get()) { 36 | long victim = random.nextLong(key - 1); 37 | ItemHolder holder = scheduler.addTicket(victim, TestStatus.STATE_8, StatusAdvancingScheduler.NO_OP); 38 | CompletableFuture future = holder.getFutureForStatus0(TestStatus.STATE_8); 39 | if (future.isCompletedExceptionally()) { 40 | Assertions.fail(); 41 | } 42 | spammedFutures.add(future); 43 | scheduler.removeTicket(victim, TestStatus.STATE_8); 44 | LockSupport.parkNanos(1_000L); 45 | } 46 | }); 47 | spamLoader.start(); 48 | 49 | scheduler.addTicket(key, TestStatus.STATE_7, () -> { 50 | System.out.println("reached STATE_7 after " + (System.nanoTime() - startTime) + "ns"); 51 | scheduler.removeTicket(key, TestStatus.STATE_7); 52 | 53 | new Thread(() -> { 54 | LockSupport.parkNanos(100_000_000); 55 | long start2 = System.nanoTime(); 56 | scheduler.addTicket(key, TestStatus.STATE_8, () -> { 57 | System.out.println("reached STATE_8 after " + (System.nanoTime() - startTime) + "ns"); 58 | scheduler.removeTicket(key, TestStatus.STATE_8); 59 | spamLoaderRunning.set(false); 60 | }); 61 | scheduler.getHolder(key).getFutureForStatus0(TestStatus.STATE_8).whenComplete((unused, throwable) -> { 62 | if (throwable != null) throwable.printStackTrace(); 63 | System.out.println("reached STATE_8 (future) after " + (System.nanoTime() - startTime) + "ns"); 64 | }); 65 | // scheduler.waitTickSync(); 66 | System.out.println("task 2 initial submission took " + (System.nanoTime() - start2) + "ns"); 67 | }).start(); 68 | }); 69 | scheduler.getHolder(key).getFutureForStatus0(TestStatus.STATE_7).whenComplete((unused, throwable) -> { 70 | if (throwable != null) throwable.printStackTrace(); 71 | System.out.println("reached STATE_7 (future) after " + (System.nanoTime() - startTime) + "ns"); 72 | }); 73 | 74 | // scheduler.waitTickSync(); 75 | System.out.println("task 1 initial submission took " + (System.nanoTime() - startTime) + "ns"); 76 | 77 | for (TestStatus value : TestStatus.values()) { 78 | if (value.ordinal() == 0) continue; 79 | scheduler.getHolder(key).getFutureForStatus(value).thenRun(() -> { 80 | System.out.println("reached " + value + " after " + (System.nanoTime() - startTime) + "ns"); 81 | }); 82 | } 83 | 84 | while (scheduler.itemCount() != 0) { 85 | LockSupport.parkNanos(1_000_000L); 86 | } 87 | 88 | System.out.println("All unloaded after " + (System.nanoTime() - startTime) + "ns"); 89 | 90 | for (CompletableFuture spammedFuture : spammedFutures) { 91 | if (!spammedFuture.isDone()) { 92 | Assertions.fail(); 93 | } 94 | } 95 | 96 | 97 | // scheduler.shutdown(); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/com/ishland/flowsched/structs/DynamicPriorityQueueTest.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.structs; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.junit.jupiter.api.Assertions.*; 7 | 8 | class DynamicPriorityQueueTest { 9 | 10 | private DynamicPriorityQueue queue; 11 | 12 | @BeforeEach 13 | void setUp() { 14 | queue = new DynamicPriorityQueue<>(256); 15 | queue.enqueue("a", 5); 16 | queue.enqueue("b", 10); 17 | queue.enqueue("c", 20); 18 | queue.enqueue("d", 30); 19 | queue.enqueue("e", 40); 20 | queue.enqueue("f", 50); 21 | queue.enqueue("g", 60); 22 | queue.enqueue("h", 254); 23 | } 24 | 25 | @Test 26 | void testDequeue() { 27 | assertQueue("a", "b", "c", "d", "e", "f", "g", "h"); 28 | assertNull(queue.dequeue()); 29 | } 30 | 31 | @Test 32 | void testEnqueue() { 33 | queue.enqueue("i", 0); 34 | assertQueue("i", "a", "b", "c", "d", "e", "f", "g", "h"); 35 | } 36 | 37 | @Test 38 | void testChangePriority1() { 39 | queue.changePriority("a", 255); 40 | queue.changePriority("b", 255); 41 | queue.changePriority("c", 255); 42 | queue.changePriority("d", 255); 43 | queue.changePriority("e", 255); 44 | queue.changePriority("f", 255); 45 | queue.changePriority("g", 255); 46 | queue.changePriority("h", 255); 47 | assertQueue("a", "b", "c", "d", "e", "f", "g", "h"); 48 | } 49 | 50 | @Test 51 | void testChangePriority2() { 52 | queue.changePriority("h", 0); 53 | queue.changePriority("g", 10); 54 | queue.changePriority("f", 20); 55 | queue.changePriority("e", 30); 56 | queue.changePriority("d", 40); 57 | queue.changePriority("c", 50); 58 | queue.changePriority("b", 60); 59 | queue.changePriority("a", 254); 60 | assertQueue("h", "g", "f", "e", "d", "c", "b", "a"); 61 | } 62 | 63 | @Test 64 | void testContains() { 65 | assertTrue(queue.contains("a")); 66 | assertTrue(queue.contains("b")); 67 | assertTrue(queue.contains("c")); 68 | assertTrue(queue.contains("d")); 69 | assertTrue(queue.contains("e")); 70 | assertTrue(queue.contains("f")); 71 | assertTrue(queue.contains("g")); 72 | assertTrue(queue.contains("h")); 73 | assertFalse(queue.contains("i")); 74 | } 75 | 76 | @Test 77 | void testRemove() { 78 | queue.remove("a"); 79 | queue.remove("b"); 80 | queue.remove("c"); 81 | queue.remove("d"); 82 | queue.remove("e"); 83 | queue.remove("f"); 84 | queue.remove("g"); 85 | queue.remove("h"); 86 | assertQueue(); 87 | } 88 | 89 | @Test 90 | void testSize() { 91 | assertEquals(8, queue.size()); 92 | queue.remove("a"); 93 | assertEquals(7, queue.size()); 94 | queue.remove("b"); 95 | assertEquals(6, queue.size()); 96 | queue.remove("c"); 97 | assertEquals(5, queue.size()); 98 | queue.remove("d"); 99 | assertEquals(4, queue.size()); 100 | queue.remove("e"); 101 | assertEquals(3, queue.size()); 102 | queue.remove("f"); 103 | assertEquals(2, queue.size()); 104 | queue.remove("g"); 105 | assertEquals(1, queue.size()); 106 | queue.remove("h"); 107 | assertEquals(0, queue.size()); 108 | } 109 | 110 | @Test 111 | void testAssertions() { 112 | assertThrows(IllegalArgumentException.class, () -> queue.enqueue("i", -1)); // attempt to enqueue with invalid priority 113 | assertThrows(IllegalArgumentException.class, () -> queue.enqueue("i", 256)); // attempt to enqueue with invalid priority 114 | assertThrows(IllegalArgumentException.class, () -> queue.enqueue("a", 0)); // attempt to enqueue with existing element 115 | assertThrows(IllegalArgumentException.class, () -> queue.changePriority("a", -1)); // attempt to change priority with invalid priority 116 | assertThrows(IllegalArgumentException.class, () -> queue.changePriority("a", 256)); // attempt to change priority with invalid priority 117 | assertFalse(queue.changePriority("i", 0)); // attempt to change priority with non-existing element 118 | assertFalse(queue.changePriority("a", 5)); // attempt to change priority with same priority 119 | } 120 | 121 | private void assertQueue(String... expected) { 122 | for (int i = 0; i < expected.length; i++) { 123 | assertEquals(expected[i], queue.dequeue(), "Element #" + i + " mismatched"); 124 | } 125 | if (queue.size() != 0) { 126 | fail("Queue size mismatched"); 127 | } 128 | } 129 | 130 | } -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/executor/ExecutorManager.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.executor; 2 | 3 | import com.ishland.flowsched.structs.DynamicPriorityQueue; 4 | import com.ishland.flowsched.util.Assertions; 5 | import it.unimi.dsi.fastutil.objects.ReferenceArrayList; 6 | 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.ConcurrentMap; 9 | import java.util.concurrent.Executor; 10 | import java.util.concurrent.Semaphore; 11 | import java.util.function.Consumer; 12 | 13 | public class ExecutorManager { 14 | 15 | private final DynamicPriorityQueue globalWorkQueue; 16 | private final ConcurrentMap lockListeners = new ConcurrentHashMap<>(); 17 | private final WorkerThread[] workerThreads; 18 | public final Semaphore waitObj = new Semaphore(0); 19 | 20 | /** 21 | * Creates a new executor manager. 22 | * 23 | * @param workerThreadCount the number of worker threads. 24 | */ 25 | public ExecutorManager(int workerThreadCount) { 26 | this(workerThreadCount, thread -> {}); 27 | } 28 | 29 | /** 30 | * Creates a new executor manager. 31 | * 32 | * @param workerThreadCount the number of worker threads. 33 | * @param threadInitializer the thread initializer. 34 | */ 35 | public ExecutorManager(int workerThreadCount, Consumer threadInitializer) { 36 | this(workerThreadCount, threadInitializer, 64); 37 | } 38 | 39 | /** 40 | * Creates a new executor manager. 41 | * 42 | * @param workerThreadCount the number of worker threads. 43 | * @param threadInitializer the thread initializer. 44 | * @param priorityCount the number of priorities. 45 | */ 46 | public ExecutorManager(int workerThreadCount, Consumer threadInitializer, int priorityCount) { 47 | globalWorkQueue = new DynamicPriorityQueue<>(priorityCount); 48 | workerThreads = new WorkerThread[workerThreadCount]; 49 | for (int i = 0; i < workerThreadCount; i++) { 50 | final WorkerThread thread = new WorkerThread(this); 51 | threadInitializer.accept(thread); 52 | thread.start(); 53 | workerThreads[i] = thread; 54 | } 55 | } 56 | 57 | /** 58 | * Attempt to lock the given tokens. 59 | * The caller should discard the task if this method returns false, as it reschedules the task. 60 | * 61 | * @return {@code true} if the lock is acquired, {@code false} otherwise. 62 | */ 63 | boolean tryLock(Task task) { 64 | retry: 65 | while (true) { 66 | final FreeableTaskList listenerSet = new FreeableTaskList(); 67 | LockToken[] lockTokens = task.lockTokens(); 68 | for (int i = 0; i < lockTokens.length; i++) { 69 | LockToken token = lockTokens[i]; 70 | final FreeableTaskList present = this.lockListeners.putIfAbsent(token, listenerSet); 71 | if (present != null) { 72 | for (int j = 0; j < i; j++) { 73 | this.lockListeners.remove(lockTokens[j], listenerSet); 74 | } 75 | callListeners(listenerSet); // synchronizes 76 | synchronized (present) { 77 | if (present.freed) { 78 | continue retry; 79 | } else { 80 | present.add(task); 81 | } 82 | } 83 | return false; 84 | } 85 | } 86 | return true; 87 | } 88 | } 89 | 90 | /** 91 | * Release the locks held by the given task. 92 | * @param task the task. 93 | */ 94 | void releaseLocks(Task task) { 95 | FreeableTaskList expectedListeners = null; 96 | for (LockToken token : task.lockTokens()) { 97 | final FreeableTaskList listeners = this.lockListeners.remove(token); 98 | if (listeners != null) { 99 | if (expectedListeners == null) { 100 | expectedListeners = listeners; 101 | } else { 102 | Assertions.assertTrue(expectedListeners == listeners, "Inconsistent lock listeners"); 103 | } 104 | } else { 105 | throw new IllegalStateException("Lock token " + token + " is not locked"); 106 | } 107 | } 108 | if (expectedListeners != null) { 109 | callListeners(expectedListeners); // synchronizes 110 | } 111 | } 112 | 113 | private void callListeners(FreeableTaskList listeners) { 114 | synchronized (listeners) { 115 | listeners.freed = true; 116 | if (listeners.isEmpty()) return; 117 | for (Task listener : listeners) { 118 | this.schedule0(listener); 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Polls an executable task from the global work queue. 125 | * @return the task, or {@code null} if no task is executable. 126 | */ 127 | Task pollExecutableTask() { 128 | Task task; 129 | while ((task = this.globalWorkQueue.dequeue()) != null) { 130 | if (this.tryLock(task)) { 131 | return task; 132 | } 133 | } 134 | return null; 135 | } 136 | 137 | DynamicPriorityQueue getGlobalWorkQueue() { 138 | return this.globalWorkQueue; 139 | } 140 | 141 | /** 142 | * Shuts down the executor manager. 143 | */ 144 | public void shutdown() { 145 | for (WorkerThread workerThread : workerThreads) { 146 | workerThread.shutdown(); 147 | } 148 | this.waitObj.release(workerThreads.length * 128); 149 | } 150 | 151 | /** 152 | * Schedules a task. 153 | * @param task the task. 154 | */ 155 | public void schedule(Task task) { 156 | schedule0(task); 157 | } 158 | 159 | private void schedule0(Task task) { 160 | this.globalWorkQueue.enqueue(task, task.priority()); 161 | this.waitObj.release(1); 162 | } 163 | 164 | /** 165 | * Schedules a runnable for execution with the given priority. 166 | * 167 | * @param runnable the runnable. 168 | * @param priority the priority. 169 | */ 170 | public void schedule(Runnable runnable, int priority) { 171 | this.schedule(new SimpleTask(runnable, priority)); 172 | } 173 | 174 | /** 175 | * Creates an executor that schedules runnables with the given priority. 176 | * 177 | * @param priority the priority. 178 | * @return the executor. 179 | */ 180 | public Executor executor(int priority) { 181 | return runnable -> this.schedule(runnable, priority); 182 | } 183 | 184 | /** 185 | * Notifies the executor manager that the priority of the given task has changed. 186 | * 187 | * @param task the task. 188 | */ 189 | public void notifyPriorityChange(Task task) { 190 | this.globalWorkQueue.changePriority(task, task.priority()); 191 | } 192 | 193 | private static class FreeableTaskList extends ReferenceArrayList { 194 | 195 | private boolean freed = false; 196 | 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/scheduler/ItemHolder.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler; 2 | 3 | import com.ishland.flowsched.structs.OneTaskAtATimeExecutor; 4 | import com.ishland.flowsched.util.Assertions; 5 | import io.reactivex.rxjava3.core.Completable; 6 | import io.reactivex.rxjava3.core.Scheduler; 7 | import io.reactivex.rxjava3.schedulers.Schedulers; 8 | import it.unimi.dsi.fastutil.Pair; 9 | import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap; 10 | import it.unimi.dsi.fastutil.objects.Object2ReferenceMap; 11 | import it.unimi.dsi.fastutil.objects.ObjectArrayList; 12 | import it.unimi.dsi.fastutil.objects.ObjectBidirectionalIterator; 13 | 14 | import java.lang.invoke.MethodHandles; 15 | import java.lang.invoke.VarHandle; 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | import java.util.Objects; 19 | import java.util.concurrent.CompletableFuture; 20 | import java.util.concurrent.CompletionStage; 21 | import java.util.concurrent.ConcurrentLinkedQueue; 22 | import java.util.concurrent.Executor; 23 | import java.util.concurrent.atomic.AtomicInteger; 24 | import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; 25 | import java.util.concurrent.atomic.AtomicReference; 26 | import java.util.function.Function; 27 | 28 | public class ItemHolder { 29 | 30 | private static final VarHandle FUTURES_HANDLE = MethodHandles.arrayElementVarHandle(CompletableFuture[].class); 31 | private static final AtomicIntegerFieldUpdater SCHEDULED_DIRTY_UPDATER = 32 | AtomicIntegerFieldUpdater.newUpdater(ItemHolder.class, "scheduledDirty"); 33 | 34 | public static final IllegalStateException UNLOADED_EXCEPTION = new IllegalStateException("Not loaded"); 35 | private static final CompletableFuture UNLOADED_FUTURE = CompletableFuture.failedFuture(UNLOADED_EXCEPTION); 36 | private static final CompletableFuture COMPLETED_VOID_FUTURE = CompletableFuture.completedFuture(null); 37 | 38 | @SuppressWarnings("PointlessBitwiseExpression") 39 | public static final int FLAG_REMOVED = 1 << 0; 40 | /** 41 | * Indicates the holder have been marked broken 42 | * If set, the holder: 43 | * - will not be allowed to be upgraded any further 44 | * - will still be allowed to be downgraded, but operations to it should be careful 45 | */ 46 | public static final int FLAG_BROKEN = 1 << 1; 47 | /** 48 | * Indicates the holder have at least one failed transactions and proceeded to retry 49 | */ 50 | public static final int FLAG_HAVE_RETRIED = 1 << 2; 51 | 52 | private final K key; 53 | private final ItemStatus unloadedStatus; 54 | private final AtomicReference item = new AtomicReference<>(); 55 | private final AtomicReference userData = new AtomicReference<>(); 56 | private final BusyRefCounter busyRefCounter = new BusyRefCounter(); 57 | private final AtomicReference>> runningUpgradeAction = new AtomicReference<>(); 58 | private final TicketSet tickets; 59 | private volatile ItemStatus status = null; 60 | // private final List, Long>> statusHistory = ReferenceLists.synchronize(new ReferenceArrayList<>()); 61 | private final KeyStatusPair[][] requestedDependencies; 62 | private final CompletableFuture[] futures; 63 | private final AtomicInteger flags = new AtomicInteger(0); 64 | private volatile int scheduledDirty = 0; // meant to be used as a boolean 65 | private final OneTaskAtATimeExecutor criticalSectionExecutor; 66 | private final Scheduler criticalSectionScheduler; 67 | private final Object2ReferenceLinkedOpenHashMap dependencyInfos = new Object2ReferenceLinkedOpenHashMap<>() { 68 | @Override 69 | protected void rehash(int newN) { 70 | if (n < newN) { 71 | super.rehash(newN); 72 | } 73 | } 74 | }; 75 | private boolean dependencyDirty = false; 76 | 77 | ItemHolder(ItemStatus initialStatus, K key, ObjectFactory objectFactory, Executor backgroundExecutor) { 78 | this.unloadedStatus = Objects.requireNonNull(initialStatus); 79 | this.status = this.unloadedStatus; 80 | this.key = Objects.requireNonNull(key); 81 | this.tickets = new TicketSet<>(this.unloadedStatus, objectFactory); 82 | 83 | ItemStatus[] allStatuses = initialStatus.getAllStatuses(); 84 | this.futures = new CompletableFuture[allStatuses.length]; 85 | this.requestedDependencies = new KeyStatusPair[allStatuses.length][]; 86 | for (int i = 0, allStatusesLength = allStatuses.length; i < allStatusesLength; i++) { 87 | this.futures[i] = UNLOADED_FUTURE; 88 | this.requestedDependencies[i] = null; 89 | } 90 | this.criticalSectionExecutor = new OneTaskAtATimeExecutor(new ConcurrentLinkedQueue<>(), backgroundExecutor); 91 | this.criticalSectionScheduler = Schedulers.from(this.criticalSectionExecutor); 92 | VarHandle.fullFence(); 93 | } 94 | 95 | /** 96 | * Not thread-safe, protect with statusMutex 97 | */ 98 | private void createFutures() { 99 | final ItemStatus targetStatus = this.getTargetStatus(); 100 | for (int i = this.unloadedStatus.ordinal() + 1; i <= targetStatus.ordinal(); i++) { 101 | this.futures[i] = this.futures[i] == UNLOADED_FUTURE ? new CompletableFuture<>() : this.futures[i]; 102 | } 103 | } 104 | 105 | /** 106 | * Get the target status of this item. 107 | * 108 | * @return the target status of this item, or null if no ticket is present 109 | */ 110 | public ItemStatus getTargetStatus() { 111 | synchronized (this) { 112 | return this.tickets.getTargetStatus(); 113 | } 114 | } 115 | 116 | public ItemStatus getStatus() { 117 | return this.status; 118 | } 119 | 120 | public synchronized boolean isBusy() { 121 | assertOpen(); 122 | return busyRefCounter.isBusy(); 123 | } 124 | 125 | public ItemStatus upgradingStatusTo() { 126 | assertOpen(); 127 | final Pair> pair = this.runningUpgradeAction.get(); 128 | return pair != null ? pair.right() : null; 129 | } 130 | 131 | public void addTicket(ItemTicket ticket) { 132 | assertOpen(); 133 | final boolean add = this.tickets.checkAdd(ticket); 134 | if (!add) { 135 | throw new IllegalStateException("Ticket already exists"); 136 | } 137 | 138 | boolean needConsumption; 139 | synchronized (this) { 140 | this.tickets.addUnchecked(ticket); 141 | createFutures(); 142 | needConsumption = ticket.getTargetStatus().ordinal() <= this.getStatus().ordinal(); 143 | } 144 | 145 | if (needConsumption) { 146 | ticket.consumeCallback(); 147 | } 148 | this.validateRequestedFutures(ticket.getTargetStatus()); 149 | } 150 | 151 | public void removeTicket(ItemTicket ticket) { 152 | assertOpen(); 153 | final boolean remove = this.tickets.checkRemove(ticket); 154 | if (!remove) { 155 | throw new IllegalStateException("Ticket does not exist"); 156 | } 157 | synchronized (this) { 158 | this.tickets.removeUnchecked(ticket); 159 | } 160 | } 161 | 162 | public void submitOp(CompletionStage op) { 163 | assertOpen(); 164 | // this.opFuture.set(opFuture.get().thenCombine(op, (a, b) -> null).handle((o, throwable) -> null)); 165 | // this.opFuture.getAndUpdate(future -> future.thenCombine(op, (a, b) -> null).handle((o, throwable) -> null)); 166 | this.busyRefCounter.incrementRefCount(); 167 | op.whenComplete((unused, throwable) -> this.busyRefCounter.decrementRefCount()); 168 | } 169 | 170 | public void subscribeOp(Completable op) { 171 | assertOpen(); 172 | this.busyRefCounter.incrementRefCount(); 173 | op.onErrorComplete().subscribe(this.busyRefCounter::decrementRefCount); 174 | } 175 | 176 | BusyRefCounter busyRefCounter() { 177 | return this.busyRefCounter; 178 | } 179 | 180 | public void submitUpgradeAction(CancellationSignaller signaller, ItemStatus status) { 181 | assertOpen(); 182 | final boolean success = this.runningUpgradeAction.compareAndSet(null, Pair.of(signaller, status)); 183 | Assertions.assertTrue(success, "Only one action can happen at a time"); 184 | signaller.addListener(unused -> this.runningUpgradeAction.set(null)); 185 | } 186 | 187 | public void tryCancelUpgradeAction() { 188 | assertOpen(); 189 | final Pair> signaller = this.runningUpgradeAction.get(); 190 | if (signaller != null) { 191 | signaller.left().cancel(); 192 | } 193 | } 194 | 195 | public CompletableFuture getOpFuture() { // best-effort 196 | assertOpen(); 197 | if (!this.busyRefCounter.isBusy()) { 198 | return COMPLETED_VOID_FUTURE; 199 | } 200 | CompletableFuture future = new CompletableFuture<>(); 201 | this.busyRefCounter.addListener(() -> future.complete(null)); 202 | return future; 203 | } 204 | 205 | public void submitOpListener(Runnable runnable) { 206 | assertOpen(); 207 | this.busyRefCounter.addListener(runnable); 208 | } 209 | 210 | public void consolidateMarkDirty(StatusAdvancingScheduler scheduler) { 211 | assertOpen(); 212 | this.busyRefCounter.addListenerOnce(() -> this.markDirty(scheduler)); 213 | } 214 | 215 | public Executor getCriticalSectionExecutor() { 216 | assertOpen(); 217 | return this.criticalSectionExecutor; 218 | } 219 | 220 | public Scheduler getCriticalSectionScheduler() { 221 | assertOpen(); 222 | return this.criticalSectionScheduler; 223 | } 224 | 225 | public void executeCriticalSectionAndBusy(Runnable command) { 226 | assertOpen(); 227 | this.busyRefCounter().incrementRefCount(); 228 | this.getCriticalSectionExecutor().execute(() -> { 229 | try { 230 | command.run(); 231 | } finally { 232 | this.busyRefCounter().decrementRefCount(); 233 | } 234 | }); 235 | } 236 | 237 | public void markDirty(StatusAdvancingScheduler scheduler) { 238 | assertOpen(); 239 | markDirty0(scheduler); 240 | } 241 | 242 | public boolean tryMarkDirty(StatusAdvancingScheduler scheduler) { 243 | if (!isOpen()) return false; 244 | markDirty0(scheduler); 245 | return true; 246 | } 247 | 248 | private void markDirty0(StatusAdvancingScheduler scheduler) { 249 | if (SCHEDULED_DIRTY_UPDATER.compareAndSet(this, 0, 1)) { 250 | this.criticalSectionExecutor.execute(() -> { 251 | SCHEDULED_DIRTY_UPDATER.set(this, 0); 252 | scheduler.tickHolder0(this); 253 | }); 254 | } 255 | } 256 | 257 | public boolean setStatus(ItemStatus status, boolean isCancellation) { 258 | assertOpen(); 259 | ItemTicket[] ticketsToFire = null; 260 | CompletableFuture futureToFire = null; 261 | synchronized (this) { 262 | final ItemStatus prevStatus = this.getStatus(); 263 | Assertions.assertTrue(status != prevStatus, "duplicate setStatus call"); 264 | // this.statusHistory.add(Pair.of(status, System.currentTimeMillis())); 265 | final int compare = Integer.compare(status.ordinal(), prevStatus.ordinal()); 266 | if (compare < 0) { // status downgrade 267 | Assertions.assertTrue(prevStatus.getPrev() == status, "Invalid status downgrade"); 268 | 269 | // if (this.getTargetStatus().ordinal() > status.ordinal()) { 270 | // return false; 271 | // } 272 | 273 | this.status = status; 274 | 275 | // reinit higher futures because downgraded, and deinit futures higher than target status 276 | // already protected by statusMutex 277 | final ItemStatus targetStatus = this.getTargetStatus(); 278 | for (int i = prevStatus.ordinal(); i < this.futures.length; i ++) { 279 | if (i > targetStatus.ordinal()) { 280 | this.futures[i].completeExceptionally(UNLOADED_EXCEPTION); 281 | this.futures[i] = UNLOADED_FUTURE; 282 | } else { 283 | this.futures[i] = this.futures[i].isDone() ? new CompletableFuture<>() : this.futures[i]; 284 | } 285 | } 286 | } else if (compare > 0) { // status upgrade 287 | Assertions.assertTrue(prevStatus.getNext() == status, "Invalid status upgrade"); 288 | 289 | this.status = status; 290 | 291 | // already protected by statusMutex 292 | final CompletableFuture future = this.futures[status.ordinal()]; 293 | 294 | if (!isCancellation) { 295 | Assertions.assertTrue(future != UNLOADED_FUTURE); 296 | Assertions.assertTrue(!future.isDone()); 297 | } 298 | futureToFire = future; 299 | ticketsToFire = this.tickets.getTicketsForStatus(status).toArray(ItemTicket[]::new); 300 | } 301 | } 302 | if (ticketsToFire != null) { 303 | for (ItemTicket ticket : ticketsToFire) { 304 | ticket.consumeCallback(); 305 | } 306 | } 307 | if (futureToFire != null) { 308 | futureToFire.complete(null); 309 | } 310 | return true; 311 | } 312 | 313 | void flushUnloadedStatus(ItemStatus currentStatus) { 314 | ArrayList> futuresToFire = null; 315 | if (currentStatus.getNext() == null) { 316 | return; 317 | } 318 | synchronized (this) { 319 | ItemStatus targetStatus = this.getTargetStatus(); 320 | if (targetStatus.getNext() == null) { 321 | return; 322 | } 323 | for (int i = Math.max(currentStatus.ordinal(), targetStatus.ordinal()) + 1; i < this.futures.length; i ++) { 324 | if (futuresToFire == null) futuresToFire = new ArrayList<>(); 325 | CompletableFuture oldFuture = this.futures[i]; 326 | futuresToFire.add(oldFuture); 327 | this.futures[i] = UNLOADED_FUTURE; 328 | } 329 | } 330 | if (futuresToFire != null) { 331 | for (int i = 0, finalFuturesToFireSize = futuresToFire.size(); i < finalFuturesToFireSize; i++) { 332 | CompletableFuture future = futuresToFire.get(i); 333 | future.completeExceptionally(UNLOADED_EXCEPTION); 334 | } 335 | } 336 | } 337 | 338 | void validateCompletedFutures(ItemStatus current) { 339 | synchronized (this) { 340 | for (int i = this.unloadedStatus.ordinal() + 1; i <= current.ordinal(); i++) { 341 | CompletableFuture future = this.futures[i]; 342 | Assertions.assertTrue(future != UNLOADED_FUTURE, "Future for loaded status cannot be UNLOADED_FUTURE"); 343 | Assertions.assertTrue(future.isDone(), "Future for loaded status must be completed"); 344 | } 345 | } 346 | } 347 | 348 | void validateAllFutures() { 349 | synchronized (this) { 350 | for (int i = this.unloadedStatus.ordinal() + 1; i < this.futures.length; i++) { 351 | CompletableFuture future = this.futures[i]; 352 | if (i <= this.getStatus().ordinal()) { 353 | Assertions.assertTrue(future.isDone(), "Future for loaded status must be completed"); 354 | } 355 | if (i <= this.getTargetStatus().ordinal()) { 356 | Assertions.assertTrue(future != UNLOADED_FUTURE, "Future for requested status cannot be UNLOADED_FUTURE"); 357 | } else { 358 | Assertions.assertTrue(future == UNLOADED_FUTURE, "Future for non-requested status must be UNLOADED_FUTURE"); 359 | } 360 | } 361 | } 362 | } 363 | 364 | void validateRequestedFutures(ItemStatus current) { 365 | synchronized (this) { 366 | for (int i = this.unloadedStatus.ordinal() + 1; i <= current.ordinal(); i++) { 367 | CompletableFuture future = this.futures[i]; 368 | Assertions.assertTrue(future != UNLOADED_FUTURE, "Future for requested status cannot be UNLOADED_FUTURE"); 369 | } 370 | } 371 | } 372 | 373 | public synchronized void setDependencies(ItemStatus status, KeyStatusPair[] dependencies) { 374 | assertOpen(); 375 | final int ordinal = status.ordinal(); 376 | if (dependencies != null) { 377 | Assertions.assertTrue(this.requestedDependencies[ordinal] == null, "Duplicate setDependencies call"); 378 | this.requestedDependencies[ordinal] = dependencies; 379 | } else { 380 | Assertions.assertTrue(this.requestedDependencies[ordinal] != null, "Duplicate setDependencies call"); 381 | this.requestedDependencies[ordinal] = null; 382 | } 383 | } 384 | 385 | public synchronized KeyStatusPair[] getDependencies(ItemStatus status) { 386 | assertOpen(); 387 | return this.requestedDependencies[status.ordinal()]; 388 | } 389 | 390 | public K getKey() { 391 | return this.key; 392 | } 393 | 394 | public CompletableFuture getFutureForStatus(ItemStatus status) { 395 | synchronized (this) { 396 | return this.futures[status.ordinal()].thenApply(Function.identity()); 397 | } 398 | } 399 | 400 | /** 401 | * Only for trusted methods 402 | */ 403 | public CompletableFuture getFutureForStatus0(ItemStatus status) { 404 | synchronized (this) { 405 | return this.futures[status.ordinal()]; 406 | } 407 | } 408 | 409 | public AtomicReference getItem() { 410 | return this.item; 411 | } 412 | 413 | /** 414 | * Get the user data of this item. 415 | * 416 | * @apiNote it is the caller's obligation to ensure the holder is not closed 417 | * @return the user data 418 | */ 419 | public AtomicReference getUserData() { 420 | return this.userData; 421 | } 422 | 423 | public int getFlags() { 424 | return this.flags.get(); 425 | } 426 | 427 | public void setFlag(int flag) { 428 | assertOpen(); 429 | this.flags.getAndUpdate(operand -> operand | flag); 430 | } 431 | 432 | /** 433 | * Note: do not use this unless you know what you are doing 434 | */ 435 | public void clearFlag(int flag) { 436 | assertOpen(); 437 | Assertions.assertTrue((flag & FLAG_REMOVED) == 0, "Cannot clear FLAG_REMOVED"); 438 | this.flags.getAndUpdate(operand -> operand & ~flag); 439 | } 440 | 441 | void release() { 442 | assertOpen(); 443 | synchronized (this) { 444 | this.tickets.assertEmpty(); 445 | } 446 | setFlag(FLAG_REMOVED); 447 | } 448 | 449 | public void addDependencyTicket(StatusAdvancingScheduler scheduler, K key, ItemStatus status, Runnable callback) { 450 | synchronized (this.dependencyInfos) { 451 | final DependencyInfo info = this.dependencyInfos.computeIfAbsent(key, k -> new DependencyInfo(status.getAllStatuses().length)); 452 | final int ordinal = status.ordinal(); 453 | if (info.refCnt[ordinal] == -1) { 454 | info.refCnt[ordinal] = 0; 455 | info.callbacks[ordinal] = new ObjectArrayList<>(); 456 | scheduler.addTicket(key, ItemTicket.TicketType.DEPENDENCY, this.getKey(), status, () -> { 457 | final ObjectArrayList list; 458 | synchronized (this.dependencyInfos) { 459 | list = info.callbacks[ordinal]; 460 | if (list != null) { 461 | info.callbacks[ordinal] = null; 462 | } 463 | } 464 | if (list != null) { 465 | for (Runnable runnable : list) { 466 | try { 467 | runnable.run(); 468 | } catch (Throwable t) { 469 | t.printStackTrace(); 470 | } 471 | } 472 | } 473 | }); 474 | } 475 | info.refCnt[ordinal] ++; 476 | final ObjectArrayList list = info.callbacks[ordinal]; 477 | if (list != null) { 478 | list.add(callback); 479 | } else { 480 | callback.run(); 481 | } 482 | } 483 | } 484 | 485 | public void removeDependencyTicket(K key, ItemStatus status) { 486 | synchronized (this.dependencyInfos) { 487 | final DependencyInfo info = this.dependencyInfos.get(key); 488 | Assertions.assertTrue(info != null); 489 | final int old = info.refCnt[status.ordinal()]--; 490 | Assertions.assertTrue(old > 0); 491 | if (old == 1) { 492 | dependencyDirty = true; 493 | } 494 | } 495 | } 496 | 497 | public boolean isDependencyDirty() { 498 | synchronized (this.dependencyInfos) { 499 | return this.dependencyDirty; 500 | } 501 | } 502 | 503 | public boolean holdsDependency() { 504 | synchronized (this.dependencyInfos) { 505 | for (ObjectBidirectionalIterator> iterator = this.dependencyInfos.object2ReferenceEntrySet().fastIterator(); iterator.hasNext(); ) { 506 | final Object2ReferenceMap.Entry entry = iterator.next(); 507 | final DependencyInfo info = entry.getValue(); 508 | int[] refCnt = info.refCnt; 509 | for (int i : refCnt) { 510 | if (i != -1) return true; 511 | } 512 | } 513 | return false; 514 | } 515 | } 516 | 517 | public void cleanupDependencies(StatusAdvancingScheduler scheduler) { 518 | synchronized (this.dependencyInfos) { 519 | if (!dependencyDirty) return; 520 | for (ObjectBidirectionalIterator> iterator = this.dependencyInfos.object2ReferenceEntrySet().fastIterator(); iterator.hasNext(); ) { 521 | Object2ReferenceMap.Entry entry = iterator.next(); 522 | final K key = entry.getKey(); 523 | final DependencyInfo info = entry.getValue(); 524 | int[] refCnt = info.refCnt; 525 | boolean isEmpty = true; 526 | for (int ordinal = 0, refCntLength = refCnt.length; ordinal < refCntLength; ordinal++) { 527 | if (refCnt[ordinal] == 0) { 528 | scheduler.removeTicket(key, ItemTicket.TicketType.DEPENDENCY, this.getKey(), this.unloadedStatus.getAllStatuses()[ordinal]); 529 | refCnt[ordinal] = -1; 530 | info.callbacks[ordinal] = null; 531 | } 532 | if (refCnt[ordinal] != -1) isEmpty = false; 533 | } 534 | if (isEmpty) 535 | iterator.remove(); 536 | } 537 | dependencyDirty = false; 538 | } 539 | } 540 | 541 | private void assertOpen() { 542 | Assertions.assertTrue(isOpen()); 543 | } 544 | 545 | public boolean isOpen() { 546 | return (this.getFlags() & FLAG_REMOVED) == 0; 547 | } 548 | 549 | private static class DependencyInfo { 550 | private final int[] refCnt; 551 | private final ObjectArrayList[] callbacks; 552 | 553 | private DependencyInfo(int statuses) { 554 | this.refCnt = new int[statuses]; 555 | this.callbacks = new ObjectArrayList[statuses]; 556 | Arrays.fill(this.refCnt, -1); 557 | } 558 | } 559 | } 560 | -------------------------------------------------------------------------------- /src/main/java/com/ishland/flowsched/scheduler/StatusAdvancingScheduler.java: -------------------------------------------------------------------------------- 1 | package com.ishland.flowsched.scheduler; 2 | 3 | import com.ishland.flowsched.util.Assertions; 4 | import io.reactivex.rxjava3.core.Completable; 5 | import io.reactivex.rxjava3.core.Scheduler; 6 | import io.reactivex.rxjava3.schedulers.Schedulers; 7 | import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; 8 | 9 | import java.lang.invoke.VarHandle; 10 | import java.util.Objects; 11 | import java.util.concurrent.CancellationException; 12 | import java.util.concurrent.CompletionException; 13 | import java.util.concurrent.Executor; 14 | import java.util.concurrent.atomic.AtomicBoolean; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | import java.util.concurrent.atomic.AtomicReference; 17 | import java.util.concurrent.locks.StampedLock; 18 | 19 | /** 20 | * A scheduler that advances status of items. 21 | * 22 | * @param the key type 23 | * @param the item type 24 | * @param the context type 25 | */ 26 | public abstract class StatusAdvancingScheduler { 27 | 28 | public static final Runnable NO_OP = () -> { 29 | }; 30 | 31 | private final StampedLock itemsLock = new StampedLock(); 32 | private final Object2ReferenceOpenHashMap> items = new Object2ReferenceOpenHashMap<>() { 33 | @Override 34 | protected void rehash(int newN) { 35 | if (n < newN) { 36 | super.rehash(newN); 37 | } 38 | } 39 | }; 40 | private final ObjectFactory objectFactory; 41 | 42 | protected StatusAdvancingScheduler() { 43 | this(new ObjectFactory.DefaultObjectFactory()); 44 | } 45 | 46 | protected StatusAdvancingScheduler(ObjectFactory objectFactory) { 47 | this.objectFactory = Objects.requireNonNull(objectFactory); 48 | } 49 | 50 | protected abstract Executor getBackgroundExecutor(); 51 | 52 | protected Scheduler getSchedulerBackedByBackgroundExecutor() { 53 | return Schedulers.from(getBackgroundExecutor()); 54 | } 55 | 56 | protected abstract ItemStatus getUnloadedStatus(); 57 | 58 | protected abstract Ctx makeContext(ItemHolder holder, ItemStatus nextStatus, KeyStatusPair[] dependencies, boolean isUpgrade); 59 | 60 | protected ExceptionHandlingAction handleTransactionException(ItemHolder holder, ItemStatus nextStatus, boolean isUpgrade, Throwable throwable) { 61 | throwable.printStackTrace(); 62 | return ExceptionHandlingAction.MARK_BROKEN; 63 | } 64 | 65 | protected void handleUnrecoverableException(Throwable throwable) { 66 | } 67 | 68 | /** 69 | * Called when an item is created. 70 | * 71 | * @implNote This method is called before the item is added to the internal map. Make sure to not access the item from the map. 72 | * May get called from any thread. 73 | * @param holder 74 | */ 75 | protected void onItemCreation(ItemHolder holder) { 76 | } 77 | 78 | /** 79 | * Called when an item is deleted. 80 | * 81 | * @implNote This method is called when the monitor of the holder is held. 82 | * @param holder 83 | */ 84 | protected void onItemRemoval(ItemHolder holder) { 85 | } 86 | 87 | protected void onItemUpgrade(ItemHolder holder, ItemStatus statusReached) { 88 | } 89 | 90 | protected void onItemDowngrade(ItemHolder holder, ItemStatus statusReached) { 91 | } 92 | 93 | void tickHolder0(ItemHolder holder) { 94 | final K key = holder.getKey(); 95 | if (getHolder(key) != holder) return; 96 | // holder.sanitizeSetStatus = Thread.currentThread(); 97 | if (holder.isBusy()) { 98 | tickHandleBusy0(holder); 99 | return; 100 | } 101 | 102 | final ItemStatus current; 103 | ItemStatus nextStatus; 104 | synchronized (holder) { 105 | if (holder.isBusy()) { 106 | holder.executeCriticalSectionAndBusy(() -> this.tickHandleBusy0(holder)); 107 | return; 108 | } 109 | current = holder.getStatus(); 110 | nextStatus = getNextStatus(current, holder.getTargetStatus()); 111 | Assertions.assertTrue(holder.getStatus() == current); 112 | holder.validateCompletedFutures(current); 113 | // holder.sanitizeSetStatus = null; 114 | if (nextStatus == current) { 115 | holder.flushUnloadedStatus(current); 116 | holder.validateAllFutures(); 117 | if (current.equals(getUnloadedStatus())) { 118 | if (holder.isDependencyDirty()) { 119 | holder.executeCriticalSectionAndBusy(() -> holder.cleanupDependencies(this)); 120 | holder.markDirty(this); 121 | return; 122 | } 123 | if (holder.holdsDependency()) { 124 | if (holder.isDependencyDirty()) { 125 | holder.markDirty(this); // should rarely happen 126 | return; 127 | } 128 | System.err.println(String.format("BUG: %s still holds some dependencies when ready for unloading", holder.getKey())); 129 | } 130 | // System.out.println("Unloaded: " + key); 131 | this.onItemRemoval(holder); 132 | holder.release(); 133 | final long lock = this.itemsLock.writeLock(); 134 | try { 135 | this.items.remove(key); 136 | } finally { 137 | this.itemsLock.unlockWrite(lock); 138 | } 139 | return; 140 | } 141 | holder.executeCriticalSectionAndBusy(() -> holder.cleanupDependencies(this)); 142 | return; 143 | } 144 | } 145 | 146 | Assertions.assertTrue(holder.getStatus() == current); 147 | if (current.ordinal() < nextStatus.ordinal()) { 148 | if ((holder.getFlags() & ItemHolder.FLAG_BROKEN) != 0) { 149 | return; 150 | } 151 | // holder.submitOp(CompletableFuture.runAsync(() -> advanceStatus0(holder, nextStatus, key), getBackgroundExecutor())); 152 | Assertions.assertTrue(holder.getStatus() == current); 153 | holder.busyRefCounter().incrementRefCount(); 154 | try { 155 | advanceStatus0(holder, nextStatus, key); 156 | } finally { 157 | holder.busyRefCounter().decrementRefCount(); 158 | } 159 | } else { 160 | holder.busyRefCounter().incrementRefCount(); 161 | try { 162 | downgradeStatus0(holder, current, nextStatus, key); 163 | } finally { 164 | holder.busyRefCounter().decrementRefCount(); 165 | } 166 | } 167 | } 168 | 169 | private void tickHandleBusy0(ItemHolder holder) { 170 | // this is not protected by any synchronization, data can be unstable here 171 | final ItemStatus upgradingStatusTo = holder.upgradingStatusTo(); 172 | final ItemStatus current = holder.getStatus(); 173 | ItemStatus nextStatus = getNextStatus(current, holder.getTargetStatus()); 174 | ItemStatus projectedCurrent = upgradingStatusTo != null ? upgradingStatusTo : current; 175 | if (projectedCurrent.ordinal() > nextStatus.ordinal()) { 176 | holder.tryCancelUpgradeAction(); 177 | } 178 | holder.consolidateMarkDirty(this); 179 | return; 180 | } 181 | 182 | private void downgradeStatus0(ItemHolder holder, ItemStatus current, ItemStatus nextStatus, K key) { 183 | // Downgrade 184 | final KeyStatusPair[] dependencies = holder.getDependencies(current); 185 | Assertions.assertTrue(dependencies != null, "No dependencies for downgrade"); 186 | 187 | Cancellable cancellable = new Cancellable(); 188 | 189 | AtomicReference contextRef = new AtomicReference<>(null); 190 | AtomicBoolean hasDowngraded = new AtomicBoolean(false); 191 | 192 | final Completable completable = Completable.defer(() -> { 193 | Assertions.assertTrue(holder.isBusy()); 194 | final Ctx ctx = makeContext(holder, current, dependencies, false); 195 | Assertions.assertTrue(ctx != null); 196 | contextRef.set(ctx); 197 | final Completable stage = current.preDowngradeFromThis(ctx, cancellable); 198 | return stage; 199 | }) 200 | .andThen(Completable.defer(() -> { 201 | Assertions.assertTrue(holder.isBusy()); 202 | 203 | final boolean success = holder.setStatus(nextStatus, false); 204 | Assertions.assertTrue(success, "setStatus on downgrade failed"); 205 | 206 | hasDowngraded.set(true); 207 | 208 | final Ctx ctx = contextRef.get(); 209 | Objects.requireNonNull(ctx); 210 | final Completable stage = current.downgradeFromThis(ctx, cancellable); 211 | return stage.cache(); 212 | })) 213 | .doOnEvent((throwable) -> { 214 | try { 215 | Assertions.assertTrue(holder.isBusy()); 216 | 217 | { 218 | Throwable actual = throwable; 219 | while (actual instanceof CompletionException ex) actual = ex.getCause(); 220 | if (cancellable.isCancelled() && actual instanceof CancellationException) { 221 | if (hasDowngraded.get()) { 222 | holder.setStatus(current, true); 223 | } 224 | holder.consolidateMarkDirty(this); 225 | return; 226 | } 227 | } 228 | 229 | final ExceptionHandlingAction action = this.tryHandleTransactionException(holder, nextStatus, false, throwable); 230 | switch (action) { 231 | case PROCEED -> { 232 | releaseDependencies(holder, current); 233 | } 234 | case MARK_BROKEN -> { 235 | holder.setFlag(ItemHolder.FLAG_BROKEN); 236 | clearDependencies0(holder, current); 237 | } 238 | } 239 | holder.consolidateMarkDirty(this); 240 | onItemDowngrade(holder, nextStatus); 241 | } catch (Throwable t) { 242 | t.printStackTrace(); 243 | } 244 | }); 245 | 246 | holder.subscribeOp(completable); 247 | } 248 | 249 | private void advanceStatus0(ItemHolder holder, ItemStatus nextStatus, K key) { 250 | // Advance 251 | final KeyStatusPair[] dependencies = nextStatus.getDependencies(holder); 252 | final CancellationSignaller dependencyCompletable = getDependencyFuture0(dependencies, holder, nextStatus); 253 | Cancellable cancellable = new Cancellable(); 254 | 255 | CancellationSignaller signaller = new CancellationSignaller(unused -> { 256 | cancellable.cancel(); 257 | dependencyCompletable.cancel(); 258 | }); 259 | 260 | AtomicReference contextRef = new AtomicReference<>(null); 261 | 262 | final Completable completable = Completable.create(emitter -> dependencyCompletable.addListener(throwable -> { 263 | if (throwable != null) { 264 | emitter.onError(throwable); 265 | } else { 266 | emitter.onComplete(); 267 | } 268 | })) 269 | .andThen(Completable.defer(() -> { 270 | Assertions.assertTrue(holder.isBusy()); 271 | final Ctx ctx = makeContext(holder, nextStatus, dependencies, false); 272 | Assertions.assertTrue(ctx != null); 273 | contextRef.set(ctx); 274 | final Completable stage = nextStatus.upgradeToThis(ctx, cancellable); 275 | return stage.cache(); 276 | })) 277 | .onErrorResumeNext(throwable -> { 278 | try { 279 | Assertions.assertTrue(holder.isBusy()); 280 | 281 | { 282 | Throwable actual = throwable; 283 | while (actual instanceof CompletionException ex) actual = ex.getCause(); 284 | if (cancellable.isCancelled() && actual instanceof CancellationException) { 285 | if (holder.getDependencies(nextStatus) != null) { 286 | releaseDependencies(holder, nextStatus); 287 | } 288 | try { 289 | signaller.fireComplete(actual); 290 | } catch (Throwable t) { 291 | t.printStackTrace(); 292 | } 293 | holder.consolidateMarkDirty(this); 294 | return Completable.error(throwable); 295 | } 296 | } 297 | 298 | Assertions.assertTrue(holder.getDependencies(nextStatus) != null); 299 | 300 | final ExceptionHandlingAction action = this.tryHandleTransactionException(holder, nextStatus, true, throwable); 301 | switch (action) { 302 | case PROCEED -> { 303 | return Completable.complete(); 304 | } 305 | case MARK_BROKEN -> { 306 | holder.setFlag(ItemHolder.FLAG_BROKEN); 307 | clearDependencies0(holder, nextStatus); 308 | holder.consolidateMarkDirty(this); 309 | return Completable.error(throwable); 310 | } 311 | default -> { 312 | throw new IllegalStateException("Unexpected value: " + action); 313 | } 314 | } 315 | } catch (Throwable t) { 316 | t.printStackTrace(); 317 | if (throwable != null) { 318 | throwable.addSuppressed(t); 319 | return Completable.error(throwable); 320 | } else { 321 | return Completable.error(t); 322 | } 323 | } 324 | }) 325 | .doOnEvent(throwable -> { 326 | try { 327 | if (throwable == null) { 328 | holder.setStatus(nextStatus, false); 329 | rerequestDependencies(holder, nextStatus); 330 | holder.consolidateMarkDirty(this); 331 | onItemUpgrade(holder, nextStatus); 332 | } 333 | 334 | try { 335 | signaller.fireComplete(null); 336 | } catch (Throwable t) { 337 | t.printStackTrace(); 338 | } 339 | } catch (Throwable t) { 340 | try { 341 | holder.setFlag(ItemHolder.FLAG_BROKEN); 342 | clearDependencies0(holder, nextStatus); 343 | holder.consolidateMarkDirty(this); 344 | } catch (Throwable t1) { 345 | t.addSuppressed(t1); 346 | } 347 | t.printStackTrace(); 348 | } 349 | }) 350 | .andThen( 351 | Completable 352 | .defer(() -> { 353 | Ctx ctx = contextRef.get(); 354 | Assertions.assertTrue(ctx != null); 355 | return nextStatus.postUpgradeToThis(ctx).cache(); 356 | }) 357 | .onErrorResumeNext(throwable -> { 358 | final ExceptionHandlingAction action = this.tryHandleTransactionException(holder, nextStatus, true, throwable); 359 | switch (action) { 360 | case PROCEED -> { 361 | return Completable.complete(); 362 | } 363 | case MARK_BROKEN -> { 364 | holder.setFlag(ItemHolder.FLAG_BROKEN); 365 | holder.consolidateMarkDirty(this); 366 | holder.executeCriticalSectionAndBusy(() -> { 367 | holder.busyRefCounter().incrementRefCount(); 368 | try { 369 | downgradeStatus0(holder, nextStatus, nextStatus.getPrev(), key); 370 | } finally { 371 | holder.busyRefCounter().decrementRefCount(); 372 | } 373 | }); 374 | return Completable.error(throwable); 375 | } 376 | default -> { 377 | throw new IllegalStateException("Unexpected value: " + action); 378 | } 379 | } 380 | }) 381 | ) 382 | .onErrorComplete() 383 | .cache(); 384 | 385 | holder.submitUpgradeAction(signaller, nextStatus); 386 | holder.subscribeOp(completable); 387 | completable.subscribe(() -> signaller.fireComplete(null), signaller::fireComplete); 388 | Assertions.assertTrue(holder.isBusy() || (cancellable.isCancelled() || holder.getStatus() == nextStatus)); 389 | } 390 | 391 | private void rerequestDependencies(ItemHolder holder, ItemStatus status) { // sync externally 392 | final KeyStatusPair[] curDep = holder.getDependencies(status); 393 | final KeyStatusPair[] newDep = status.getDependencies(holder); 394 | final KeyStatusPair[] toAdd = status.getDependenciesToAdd(holder); 395 | final KeyStatusPair[] toRemove = status.getDependenciesToRemove(holder); 396 | holder.setDependencies(status, null); 397 | holder.setDependencies(status, newDep); 398 | for (KeyStatusPair pair : toAdd) { 399 | holder.addDependencyTicket(this, pair.key(), pair.status(), NO_OP); 400 | } 401 | for (KeyStatusPair pair : toRemove) { 402 | holder.removeDependencyTicket(pair.key(), pair.status()); 403 | } 404 | } 405 | 406 | public ItemHolder getHolder(K key) { 407 | long stamp = this.itemsLock.tryOptimisticRead(); 408 | if (stamp != 0L) { 409 | try { 410 | ItemHolder holder = this.items.get(key); 411 | if (this.itemsLock.validate(stamp)) { 412 | return holder; 413 | } 414 | // fall through 415 | } catch (Throwable ignored) { 416 | // fall through 417 | } 418 | } 419 | 420 | stamp = this.itemsLock.readLock(); 421 | try { 422 | return this.items.get(key); 423 | } finally { 424 | this.itemsLock.unlockRead(stamp); 425 | } 426 | } 427 | 428 | private ItemHolder getOrCreateHolder(K key) { 429 | final ItemHolder holder = getHolder(key); 430 | if (holder != null) { 431 | return holder; 432 | } 433 | final long lock = this.itemsLock.writeLock(); 434 | try { 435 | return this.items.computeIfAbsent(key, this::createHolder); 436 | } finally { 437 | this.itemsLock.unlockWrite(lock); 438 | } 439 | } 440 | 441 | public int itemCount() { 442 | VarHandle.acquireFence(); 443 | return this.items.size(); 444 | } 445 | 446 | protected void wakeUp() { 447 | } 448 | 449 | private CancellationSignaller getDependencyFuture0(KeyStatusPair[] dependencies, ItemHolder holder, ItemStatus nextStatus) { 450 | AtomicInteger satisfied = new AtomicInteger(0); 451 | final int size = dependencies.length; 452 | holder.setDependencies(nextStatus, dependencies); 453 | if (size == 0) { 454 | return CancellationSignaller.COMPLETED; 455 | } 456 | 457 | AtomicBoolean finished = new AtomicBoolean(false); 458 | final CancellationSignaller signaller = new CancellationSignaller(signaller1 -> { 459 | if (satisfied.get() == 0); 460 | if (finished.compareAndSet(false, true)) { 461 | releaseDependencies(holder, nextStatus); 462 | signaller1.fireComplete(new CancellationException()); 463 | } 464 | }); 465 | try { 466 | final KeyStatusPair keyStatusPair = new KeyStatusPair<>(holder.getKey(), nextStatus); 467 | for (KeyStatusPair dependency : dependencies) { 468 | Assertions.assertTrue(!dependency.key().equals(holder.getKey())); 469 | holder.addDependencyTicket(this, dependency.key(), dependency.status(), () -> { 470 | // Assertions.assertTrue(this.getHolder(dependency.key()).getStatus().ordinal() >= dependency.status().ordinal()); 471 | final int incrementAndGet = satisfied.incrementAndGet(); 472 | Assertions.assertTrue(incrementAndGet <= size, "Satisfied more than expected"); 473 | if (incrementAndGet == size) { 474 | if (finished.compareAndSet(false, true)) { 475 | holder.getCriticalSectionExecutor().execute(() -> signaller.fireComplete(null)); 476 | } 477 | } 478 | }); 479 | } 480 | } catch (Throwable t) { 481 | signaller.fireComplete(t); 482 | } 483 | return signaller; 484 | } 485 | 486 | public ItemHolder addTicket(K key, ItemStatus targetStatus, Runnable callback) { 487 | return this.addTicket(key, key, targetStatus, callback); 488 | } 489 | 490 | public ItemHolder addTicket(K key, Object source, ItemStatus targetStatus, Runnable callback) { 491 | return this.addTicket(key, ItemTicket.TicketType.EXTERNAL, source, targetStatus, callback); 492 | } 493 | 494 | public ItemHolder addTicket(K key, ItemTicket.TicketType type, Object source, ItemStatus targetStatus, Runnable callback) { 495 | return this.addTicket0(key, new ItemTicket<>(type, source, targetStatus, callback)); 496 | } 497 | 498 | private ItemHolder addTicket0(K key, ItemTicket ticket) { 499 | if (this.getUnloadedStatus().equals(ticket.getTargetStatus())) { 500 | throw new IllegalArgumentException("Cannot add ticket to unloaded status"); 501 | } 502 | try { 503 | while (true) { 504 | ItemHolder holder = this.getOrCreateHolder(key); 505 | 506 | synchronized (holder) { 507 | if (!holder.isOpen()) { 508 | // holder got removed before we had chance to add a ticket to it, retry 509 | // System.out.println(String.format("Retrying addTicket0(%s, %s)", key, ticket)); 510 | continue; 511 | } 512 | holder.busyRefCounter().incrementRefCount(); 513 | } 514 | try { 515 | holder.addTicket(ticket); 516 | holder.consolidateMarkDirty(this); 517 | } finally { 518 | holder.busyRefCounter().decrementRefCount(); 519 | } 520 | return holder; 521 | } 522 | } catch (Throwable t) { 523 | t.printStackTrace(); 524 | throw new RuntimeException(t); 525 | } 526 | } 527 | 528 | private ItemHolder createHolder(K k) { 529 | final ItemHolder holder1 = new ItemHolder<>(this.getUnloadedStatus(), k, this.objectFactory, this.getBackgroundExecutor()); 530 | this.onItemCreation(holder1); 531 | VarHandle.fullFence(); 532 | return holder1; 533 | } 534 | 535 | public void removeTicket(K key, ItemStatus targetStatus) { 536 | this.removeTicket(key, ItemTicket.TicketType.EXTERNAL, key, targetStatus); 537 | } 538 | 539 | public void removeTicket(K key, ItemTicket.TicketType type, Object source, ItemStatus targetStatus) { 540 | ItemHolder holder = this.getHolder(key); 541 | if (holder == null) { 542 | throw new IllegalStateException("No such item"); 543 | } 544 | holder.removeTicket(new ItemTicket<>(type, source, targetStatus, null)); 545 | // holder may have been removed at this point, only mark it dirty if it still exists 546 | holder.tryMarkDirty(this); 547 | } 548 | 549 | private ItemStatus getNextStatus(ItemStatus current, ItemStatus target) { 550 | Assertions.assertTrue(target != null); 551 | final int compare = Integer.compare(current.ordinal(), target.ordinal()); 552 | if (compare < 0) { 553 | return current.getNext(); 554 | } else if (compare == 0) { 555 | return current; 556 | } else { 557 | return current.getPrev(); 558 | } 559 | } 560 | 561 | private ExceptionHandlingAction tryHandleTransactionException(ItemHolder holder, ItemStatus nextStatus, boolean isUpgrade, Throwable throwable) { 562 | if (throwable == null) { // no exception to handle 563 | return ExceptionHandlingAction.PROCEED; 564 | } 565 | try { 566 | return this.handleTransactionException(holder, nextStatus, isUpgrade, throwable); 567 | } catch (Throwable t) { 568 | t.printStackTrace(); 569 | return ExceptionHandlingAction.MARK_BROKEN; 570 | } 571 | } 572 | 573 | private void clearDependencies0(final ItemHolder holder, final ItemStatus fromStatus) { // sync externally 574 | for (int i = fromStatus.ordinal(); i > 0; i--) { 575 | final ItemStatus status = this.getUnloadedStatus().getAllStatuses()[i]; 576 | this.releaseDependencies(holder, status); 577 | holder.setDependencies(status, new KeyStatusPair[0]); 578 | } 579 | } 580 | 581 | private void releaseDependencies(ItemHolder holder, ItemStatus status) { 582 | final KeyStatusPair[] dependencies = holder.getDependencies(status); 583 | for (KeyStatusPair dependency : dependencies) { 584 | holder.removeDependencyTicket(dependency.key(), dependency.status()); 585 | } 586 | holder.setDependencies(status, null); 587 | } 588 | 589 | } 590 | --------------------------------------------------------------------------------