summaryrefslogtreecommitdiff
path: root/opendc-simulator/opendc-simulator-core
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2022-11-13 18:16:19 +0000
committerGitHub <noreply@github.com>2022-11-13 18:16:19 +0000
commit52eed48441693149993db79b63431b99e0973027 (patch)
treeba267db531bc3d81409ddfe9caeb6d3b5a65e8c8 /opendc-simulator/opendc-simulator-core
parent183cfa96910ebb74c668dea7ef98071966f8fcb9 (diff)
parent33d91ef30ad7bcb73365934fe536461210d1082a (diff)
merge: Increase minimum Java version to 17 (#115)
This pull request increases the minimum version of Java required by OpenDC to 17. This new version of Java introduces several new features compared to our old minimum version (11), which we attempt to apply in this conversion. ## Implementation Notes :hammer_and_pick: * Increase minimum Java version to Java 17 * Use RandomGenerator as randomness source * Add common dispatcher interface * Add compatibility with Kotlin coroutines * Use InstantSource as time source * Re-implement SimulationScheduler as Dispatcher * Replace use of CoroutineContext by Dispatcher ## External Dependencies :four_leaf_clover: * Java 17 ## Breaking API Changes :warning: * The use of `CoroutineContext` and `Clock` as parameters of classes has been replaced by the `Dispatcher` interface. * The use of `Clock` has been replaced by `InstantSource` which does not carry time zone info. * The use of `Random` and `SplittableRandom` as parameter type has been replaced by `RandomGenerator`
Diffstat (limited to 'opendc-simulator/opendc-simulator-core')
-rw-r--r--opendc-simulator/opendc-simulator-core/build.gradle.kts1
-rw-r--r--opendc-simulator/opendc-simulator-core/src/main/java/org/opendc/simulator/SimulationDispatcher.java (renamed from opendc-simulator/opendc-simulator-core/src/main/java/org/opendc/simulator/SimulationScheduler.java)134
-rw-r--r--opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationBuilders.kt74
-rw-r--r--opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationController.kt34
-rw-r--r--opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineDispatcher.kt98
-rw-r--r--opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineScope.kt36
-rw-r--r--opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/SimulationDispatcherTest.kt (renamed from opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/SimulationSchedulerTest.kt)23
-rw-r--r--opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/kotlin/SimulationBuildersTest.kt98
8 files changed, 272 insertions, 226 deletions
diff --git a/opendc-simulator/opendc-simulator-core/build.gradle.kts b/opendc-simulator/opendc-simulator-core/build.gradle.kts
index 0de96a8e..0ae95d42 100644
--- a/opendc-simulator/opendc-simulator-core/build.gradle.kts
+++ b/opendc-simulator/opendc-simulator-core/build.gradle.kts
@@ -28,5 +28,6 @@ plugins {
}
dependencies {
+ api(projects.opendc.opendcCommon)
api(libs.kotlinx.coroutines)
}
diff --git a/opendc-simulator/opendc-simulator-core/src/main/java/org/opendc/simulator/SimulationScheduler.java b/opendc-simulator/opendc-simulator-core/src/main/java/org/opendc/simulator/SimulationDispatcher.java
index 305bdf5e..8c74aacf 100644
--- a/opendc-simulator/opendc-simulator-core/src/main/java/org/opendc/simulator/SimulationScheduler.java
+++ b/opendc-simulator/opendc-simulator-core/src/main/java/org/opendc/simulator/SimulationDispatcher.java
@@ -22,17 +22,17 @@
package org.opendc.simulator;
-import java.time.Clock;
import java.time.Instant;
-import java.time.ZoneId;
-import java.util.concurrent.Executor;
+import java.time.InstantSource;
+import org.opendc.common.Dispatcher;
+import org.opendc.common.DispatcherHandle;
/**
- * A scheduler is used by simulations to manage execution of (future) tasks, providing a controllable (virtual) clock to
- * skip over delays.
+ * A {@link Dispatcher} used by simulations to manage execution of (future) tasks, providing a controllable (virtual)
+ * clock to skip over delays.
*
* <p>
- * The scheduler can be queried to advance the time (via {@link #advanceBy}), run all the scheduled tasks advancing the
+ * The dispatcher can be queried to advance the time (via {@link #advanceBy}), run all the scheduled tasks advancing the
* virtual time as needed (via {@link #advanceUntilIdle}), or run the tasks that are scheduled to run as soon as
* possible but have not yet been dispatched (via {@link #runCurrent}). These methods execute the pending tasks using
* a single thread.
@@ -40,7 +40,7 @@ import java.util.concurrent.Executor;
* <p>
* This class is not thread-safe and must not be used concurrently by multiple threads.
*/
-public final class SimulationScheduler implements Executor {
+public final class SimulationDispatcher implements Dispatcher {
/**
* The {@link TaskQueue} containing the pending tasks.
*/
@@ -57,36 +57,27 @@ public final class SimulationScheduler implements Executor {
private int count = 0;
/**
- * The {@link Clock} instance linked to this scheduler.
+ * The {@link InstantSource} instance linked to this scheduler.
*/
- private final SimulationClock clock = new SimulationClock(this, ZoneId.systemDefault());
+ private final SimulationClock timeSource = new SimulationClock(this);
/**
- * Construct a {@link SimulationScheduler} instance with the specified initial time.
+ * Construct a {@link SimulationDispatcher} instance with the specified initial time.
*
* @param initialTimeMs The initial virtual time of the scheduler in milliseconds since epoch.
*/
- public SimulationScheduler(long initialTimeMs) {
+ public SimulationDispatcher(long initialTimeMs) {
this.currentTime = initialTimeMs;
}
/**
- * Construct a {@link SimulationScheduler} instance with the initial time set to UNIX Epoch 0.
+ * Construct a {@link SimulationDispatcher} instance with the initial time set to UNIX Epoch 0.
*/
- public SimulationScheduler() {
+ public SimulationDispatcher() {
this(0);
}
/**
- * Return the virtual clock associated with this dispatcher.
- *
- * @return A {@link Clock} tracking the virtual time of the dispatcher.
- */
- public Clock getClock() {
- return clock;
- }
-
- /**
* Return the current virtual timestamp of the dispatcher (in milliseconds since epoch).
*
* @return A long value representing the virtual timestamp of the dispatcher in milliseconds since epoch.
@@ -96,37 +87,30 @@ public final class SimulationScheduler implements Executor {
}
/**
- * Schedule a <code>task</code> that executes after the specified <code>delayMs</code>.
+ * Return the virtual time source associated with this dispatcher.
*
- * @param delayMs The time from now until the execution of the task (in milliseconds).
- * @param task The task to execute after the delay.
- * @return The identifier of the task that can be used together with the timestamp of the task to cancel it.
+ * @return A {@link InstantSource} tracking the virtual time of the dispatcher.
*/
- public int schedule(long delayMs, Runnable task) {
- if (delayMs < 0) {
- throw new IllegalArgumentException(
- "Attempted scheduling an event earlier in time (delay " + delayMs + " ms)");
- }
+ @Override
+ public InstantSource getTimeSource() {
+ return timeSource;
+ }
+ @Override
+ public void schedule(long delayMs, Runnable command) {
+ internalSchedule(delayMs, command);
+ }
+
+ @Override
+ public DispatcherHandle scheduleCancellable(long delayMs, Runnable command) {
long target = currentTime + delayMs;
if (target < 0) {
target = Long.MAX_VALUE;
}
- int id = count++;
- queue.add(target, id, task);
- return id;
- }
-
- /**
- * Cancel a pending task.
- *
- * @param deadline The deadline of the task.
- * @param id The identifier of the task (returned by {@link #schedule(long, Runnable)}).
- * @return A boolean indicating whether a task was actually cancelled.
- */
- public boolean cancel(long deadline, int id) {
- return queue.remove(deadline, id);
+ long deadline = target;
+ int id = internalSchedule(delayMs, command);
+ return () -> internalCancel(deadline, id);
}
/**
@@ -198,50 +182,62 @@ public final class SimulationScheduler implements Executor {
}
/**
- * Schedule the specified command to run at this moment of virtual time.
+ * Schedule a <code>task</code> that executes after the specified <code>delayMs</code>.
*
- * @param command The command to execute.
+ * @param delayMs The time from now until the execution of the task (in milliseconds).
+ * @param task The task to execute after the delay.
+ * @return The identifier of the task that can be used together with the timestamp of the task to cancel it.
*/
- @Override
- public void execute(Runnable command) {
- schedule(0, command);
+ private int internalSchedule(long delayMs, Runnable task) {
+ if (delayMs < 0) {
+ throw new IllegalArgumentException(
+ "Attempted scheduling an event earlier in time (delay " + delayMs + " ms)");
+ }
+
+ long target = currentTime + delayMs;
+ if (target < 0) {
+ target = Long.MAX_VALUE;
+ }
+
+ int id = count++;
+ queue.add(target, id, task);
+ return id;
}
/**
- * A {@link Clock} implementation for a {@link SimulationScheduler}.
+ * Cancel a pending task.
+ *
+ * @param deadline The deadline of the task.
+ * @param id The identifier of the task (returned by {@link #internalSchedule(long, Runnable)}).
+ * @return A boolean indicating whether a task was actually cancelled.
*/
- private static class SimulationClock extends Clock {
- private final SimulationScheduler scheduler;
- private final ZoneId zone;
-
- SimulationClock(SimulationScheduler scheduler, ZoneId zone) {
- this.scheduler = scheduler;
- this.zone = zone;
- }
+ private boolean internalCancel(long deadline, int id) {
+ return queue.remove(deadline, id);
+ }
- @Override
- public ZoneId getZone() {
- return zone;
- }
+ /**
+ * A {@link InstantSource} implementation for a {@link SimulationDispatcher}.
+ */
+ private static class SimulationClock implements InstantSource {
+ private final SimulationDispatcher dispatcher;
- @Override
- public Clock withZone(ZoneId zoneId) {
- return new SimulationClock(scheduler, zone);
+ SimulationClock(SimulationDispatcher dispatcher) {
+ this.dispatcher = dispatcher;
}
@Override
public Instant instant() {
- return Instant.ofEpochMilli(scheduler.currentTime);
+ return Instant.ofEpochMilli(dispatcher.currentTime);
}
@Override
public long millis() {
- return scheduler.currentTime;
+ return dispatcher.currentTime;
}
@Override
public String toString() {
- return "SimulationClock[time=" + millis() + "ms]";
+ return "SimulationDispatcher.InstantSource[time=" + millis() + "ms]";
}
}
}
diff --git a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationBuilders.kt b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationBuilders.kt
index 882a0fc5..6e568137 100644
--- a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationBuilders.kt
+++ b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationBuilders.kt
@@ -22,11 +22,18 @@
package org.opendc.simulator.kotlin
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
+import kotlinx.coroutines.NonCancellable.children
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
-import org.opendc.simulator.SimulationScheduler
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.opendc.common.DispatcherProvider
+import org.opendc.common.asCoroutineDispatcher
+import org.opendc.simulator.SimulationDispatcher
+import java.time.InstantSource
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@@ -54,16 +61,16 @@ import kotlin.coroutines.EmptyCoroutineContext
* The simulation is run in a single thread, unless other [CoroutineDispatcher] are used for child coroutines.
* Because of this, child coroutines are not executed in parallel to [body].
* In order for the spawned-off asynchronous code to actually be executed, one must either [yield] or suspend the
- * body some other way, or use commands that control scheduling (see [SimulationScheduler]).
+ * body some other way, or use commands that control scheduling (see [SimulationDispatcher]).
*/
@OptIn(ExperimentalCoroutinesApi::class)
public fun runSimulation(
context: CoroutineContext = EmptyCoroutineContext,
- scheduler: SimulationScheduler = SimulationScheduler(),
+ scheduler: SimulationDispatcher = SimulationDispatcher(),
body: suspend SimulationCoroutineScope.() -> Unit
) {
- val (safeContext, dispatcher) = context.checkArguments(scheduler)
- val startingJobs = safeContext.activeJobs()
+ val (safeContext, job, dispatcher) = context.checkArguments(scheduler)
+ val startingJobs = job.activeJobs()
val scope = SimulationCoroutineScope(safeContext)
val deferred = scope.async {
body(scope)
@@ -72,7 +79,7 @@ public fun runSimulation(
deferred.getCompletionExceptionOrNull()?.let {
throw it
}
- val endingJobs = safeContext.activeJobs()
+ val endingJobs = job.activeJobs()
if ((endingJobs - startingJobs).isNotEmpty()) {
throw IllegalStateException("Test finished with active jobs: $endingJobs")
}
@@ -82,24 +89,51 @@ public fun runSimulation(
* Convenience method for calling [runSimulation] on an existing [SimulationCoroutineScope].
*/
public fun SimulationCoroutineScope.runSimulation(block: suspend SimulationCoroutineScope.() -> Unit): Unit =
- runSimulation(coroutineContext, scheduler, block)
+ runSimulation(coroutineContext, dispatcher, block)
+
+private fun CoroutineContext.checkArguments(scheduler: SimulationDispatcher): Triple<CoroutineContext, Job, SimulationController> {
+ val job = get(Job) ?: SupervisorJob()
+ val dispatcher = get(ContinuationInterceptor) ?: scheduler.asCoroutineDispatcher()
+ val simulationDispatcher = dispatcher.asSimulationDispatcher()
+ return Triple(this + dispatcher + job, job, simulationDispatcher.asController())
+}
+
+private fun Job.activeJobs(): Set<Job> {
+ return children.filter { it.isActive }.toSet()
+}
/**
- * Convenience method for calling [runSimulation] on an existing [SimulationCoroutineDispatcher].
+ * Convert a [ContinuationInterceptor] into a [SimulationDispatcher] if possible.
*/
-public fun SimulationCoroutineDispatcher.runSimulation(block: suspend SimulationCoroutineScope.() -> Unit): Unit =
- runSimulation(this, scheduler, block)
-
-private fun CoroutineContext.checkArguments(scheduler: SimulationScheduler): Pair<CoroutineContext, SimulationController> {
- val dispatcher = get(ContinuationInterceptor).run {
- this?.let { require(this is SimulationController) { "Dispatcher must implement SimulationController: $this" } }
- this ?: SimulationCoroutineDispatcher(scheduler)
- }
+internal fun ContinuationInterceptor.asSimulationDispatcher(): SimulationDispatcher {
+ val provider = this as? DispatcherProvider ?: throw IllegalArgumentException(
+ "DispatcherProvider such as SimulatorCoroutineDispatcher as the ContinuationInterceptor(Dispatcher) is required"
+ )
- val job = get(Job) ?: SupervisorJob()
- return Pair(this + dispatcher + job, dispatcher as SimulationController)
+ return provider.dispatcher as? SimulationDispatcher ?: throw IllegalArgumentException("Active dispatcher is not a SimulationDispatcher")
}
-private fun CoroutineContext.activeJobs(): Set<Job> {
- return checkNotNull(this[Job]).children.filter { it.isActive }.toSet()
+/**
+ * Helper method to convert a [SimulationDispatcher] into a [SimulationController].
+ */
+internal fun SimulationDispatcher.asController(): SimulationController {
+ return object : SimulationController {
+ override val dispatcher: SimulationDispatcher
+ get() = this@asController
+
+ override val timeSource: InstantSource
+ get() = this@asController.timeSource
+
+ override fun advanceUntilIdle() {
+ dispatcher.advanceUntilIdle()
+ }
+
+ override fun advanceBy(delayMs: Long) {
+ dispatcher.advanceBy(delayMs)
+ }
+
+ override fun runCurrent() {
+ dispatcher.runCurrent()
+ }
+ }
}
diff --git a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationController.kt b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationController.kt
index f96b2326..f7470ad9 100644
--- a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationController.kt
+++ b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationController.kt
@@ -23,30 +23,48 @@
package org.opendc.simulator.kotlin
import kotlinx.coroutines.CoroutineDispatcher
-import org.opendc.simulator.SimulationScheduler
-import java.time.Clock
+import org.opendc.simulator.SimulationDispatcher
+import java.time.InstantSource
/**
- * Control the virtual clock of a [CoroutineDispatcher].
+ * Interface to control the virtual clock of a [CoroutineDispatcher].
*/
public interface SimulationController {
/**
* The current virtual clock as it is known to this Dispatcher.
*/
- public val clock: Clock
+ public val timeSource: InstantSource
/**
- * The [SimulationScheduler] driving the simulation.
+ * The current virtual timestamp of the dispatcher (in milliseconds since epoch).
*/
- public val scheduler: SimulationScheduler
+ public val currentTime: Long
+ get() = timeSource.millis()
+
+ /**
+ * Return the [SimulationDispatcher] driving the simulation.
+ */
+ public val dispatcher: SimulationDispatcher
/**
* Immediately execute all pending tasks and advance the virtual clock-time to the last delay.
*
* If new tasks are scheduled due to advancing virtual time, they will be executed before `advanceUntilIdle`
* returns.
+ */
+ public fun advanceUntilIdle()
+
+ /**
+ * Move the virtual clock of this dispatcher forward by the specified amount, running the scheduled tasks in the
+ * meantime.
*
- * @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds.
+ * @param delayMs The amount of time to move the virtual clock forward (in milliseconds).
+ * @throws IllegalStateException if passed a negative <code>delay</code>.
+ */
+ public fun advanceBy(delayMs: Long)
+
+ /**
+ * Execute the tasks that are scheduled to execute at this moment of virtual time.
*/
- public fun advanceUntilIdle(): Long
+ public fun runCurrent()
}
diff --git a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineDispatcher.kt b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineDispatcher.kt
deleted file mode 100644
index cacbbbf7..00000000
--- a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineDispatcher.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (c) 2021 AtLarge Research
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package org.opendc.simulator.kotlin
-
-import kotlinx.coroutines.CancellableContinuation
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Delay
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.InternalCoroutinesApi
-import org.opendc.simulator.SimulationScheduler
-import java.lang.Runnable
-import java.time.Clock
-import kotlin.coroutines.CoroutineContext
-
-/**
- * A [CoroutineDispatcher] that performs both immediate execution of coroutines on the main thread and uses a virtual
- * clock for time management.
- *
- * @param scheduler The [SimulationScheduler] used to manage the execution of future tasks.
- */
-@OptIn(InternalCoroutinesApi::class)
-public class SimulationCoroutineDispatcher(
- override val scheduler: SimulationScheduler = SimulationScheduler()
-) : CoroutineDispatcher(), SimulationController, Delay {
- /**
- * The virtual clock of this dispatcher.
- */
- override val clock: Clock = scheduler.clock
-
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- block.run()
- }
-
- override fun dispatchYield(context: CoroutineContext, block: Runnable) {
- scheduler.execute(block)
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
- scheduler.schedule(timeMillis, CancellableContinuationRunnable(continuation) { resumeUndispatched(Unit) })
- }
-
- override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
- return object : DisposableHandle {
- private val deadline = (scheduler.currentTime + timeMillis).let { if (it >= 0) it else Long.MAX_VALUE }
- private val id = scheduler.schedule(timeMillis, block)
-
- override fun dispose() {
- scheduler.cancel(deadline, id)
- }
- }
- }
-
- override fun toString(): String {
- return "SimulationCoroutineDispatcher[time=${scheduler.currentTime}ms]"
- }
-
- override fun advanceUntilIdle(): Long {
- val scheduler = scheduler
- val oldTime = scheduler.currentTime
-
- scheduler.advanceUntilIdle()
-
- return scheduler.currentTime - oldTime
- }
-
- /**
- * This class exists to allow cleanup code to avoid throwing for cancelled continuations scheduled
- * in the future.
- */
- private class CancellableContinuationRunnable<T>(
- @JvmField val continuation: CancellableContinuation<T>,
- private val block: CancellableContinuation<T>.() -> Unit
- ) : Runnable {
- override fun run() = continuation.block()
- }
-}
diff --git a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineScope.kt b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineScope.kt
index 6be8e67a..ca49fc53 100644
--- a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineScope.kt
+++ b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineScope.kt
@@ -24,7 +24,8 @@ package org.opendc.simulator.kotlin
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
-import org.opendc.simulator.SimulationScheduler
+import org.opendc.common.asCoroutineDispatcher
+import org.opendc.simulator.SimulationDispatcher
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@@ -34,33 +35,28 @@ import kotlin.coroutines.EmptyCoroutineContext
*/
public interface SimulationCoroutineScope : CoroutineScope, SimulationController
-private class SimulationCoroutineScopeImpl(
- override val coroutineContext: CoroutineContext
-) :
- SimulationCoroutineScope,
- SimulationController by coroutineContext.simulationController
-
/**
* A scope which provides detailed control over the execution of coroutines for simulations.
*
* If the provided context does not provide a [ContinuationInterceptor] (Dispatcher) or [CoroutineExceptionHandler], the
- * scope adds [SimulationCoroutineDispatcher] automatically.
+ * scope adds a dispatcher automatically.
*/
-@Suppress("FunctionName")
public fun SimulationCoroutineScope(
context: CoroutineContext = EmptyCoroutineContext,
- scheduler: SimulationScheduler = SimulationScheduler()
+ scheduler: SimulationDispatcher = SimulationDispatcher()
): SimulationCoroutineScope {
var safeContext = context
- if (context[ContinuationInterceptor] == null) safeContext += SimulationCoroutineDispatcher(scheduler)
- return SimulationCoroutineScopeImpl(safeContext)
-}
+ val simulationDispatcher: SimulationDispatcher
+ val interceptor = context[ContinuationInterceptor]
-private inline val CoroutineContext.simulationController: SimulationController
- get() {
- val handler = this[ContinuationInterceptor]
- return handler as? SimulationController ?: throw IllegalArgumentException(
- "SimulationCoroutineScope requires a SimulationController such as SimulatorCoroutineDispatcher as " +
- "the ContinuationInterceptor (Dispatcher)"
- )
+ if (interceptor != null) {
+ simulationDispatcher = interceptor.asSimulationDispatcher()
+ } else {
+ simulationDispatcher = scheduler
+ safeContext += scheduler.asCoroutineDispatcher()
}
+
+ return object : SimulationCoroutineScope, SimulationController by simulationDispatcher.asController() {
+ override val coroutineContext: CoroutineContext = safeContext
+ }
+}
diff --git a/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/SimulationSchedulerTest.kt b/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/SimulationDispatcherTest.kt
index eca3b582..600102be 100644
--- a/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/SimulationSchedulerTest.kt
+++ b/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/SimulationDispatcherTest.kt
@@ -28,15 +28,15 @@ import org.junit.jupiter.api.assertThrows
import java.time.Instant
/**
- * Test suite for the [SimulationScheduler] class.
+ * Test suite for the [SimulationDispatcher] class.
*/
-class SimulationSchedulerTest {
+class SimulationDispatcherTest {
/**
- * Test the basic functionality of [SimulationScheduler.runCurrent].
+ * Test the basic functionality of [SimulationDispatcher.runCurrent].
*/
@Test
fun testRunCurrent() {
- val scheduler = SimulationScheduler()
+ val scheduler = SimulationDispatcher()
var count = 0
scheduler.schedule(1) { count += 1 }
@@ -58,11 +58,11 @@ class SimulationSchedulerTest {
}
/**
- * Test the clock of the [SimulationScheduler].
+ * Test the clock of the [SimulationDispatcher].
*/
@Test
fun testClock() {
- val scheduler = SimulationScheduler()
+ val scheduler = SimulationDispatcher()
var count = 0
scheduler.schedule(1) { count += 1 }
@@ -70,8 +70,8 @@ class SimulationSchedulerTest {
scheduler.advanceBy(2)
assertEquals(2, scheduler.currentTime)
- assertEquals(2, scheduler.clock.millis())
- assertEquals(Instant.ofEpochMilli(2), scheduler.clock.instant())
+ assertEquals(2, scheduler.timeSource.millis())
+ assertEquals(Instant.ofEpochMilli(2), scheduler.timeSource.instant())
}
/**
@@ -79,7 +79,7 @@ class SimulationSchedulerTest {
*/
@Test
fun testAdvanceByLargeDelays() {
- val scheduler = SimulationScheduler()
+ val scheduler = SimulationDispatcher()
var count = 0
scheduler.schedule(1) { count += 1 }
@@ -87,10 +87,11 @@ class SimulationSchedulerTest {
scheduler.advanceBy(10)
scheduler.schedule(Long.MAX_VALUE) { count += 1 }
+ scheduler.scheduleCancellable(Long.MAX_VALUE) { count += 1 }
scheduler.schedule(100_000_000) { count += 1 }
scheduler.advanceUntilIdle()
- assertEquals(3, count)
+ assertEquals(4, count)
}
/**
@@ -98,7 +99,7 @@ class SimulationSchedulerTest {
*/
@Test
fun testNegativeDelays() {
- val scheduler = SimulationScheduler()
+ val scheduler = SimulationDispatcher()
assertThrows<IllegalArgumentException> { scheduler.schedule(-100) { } }
assertThrows<IllegalArgumentException> { scheduler.advanceBy(-100) }
diff --git a/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/kotlin/SimulationBuildersTest.kt b/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/kotlin/SimulationBuildersTest.kt
new file mode 100644
index 00000000..26419a50
--- /dev/null
+++ b/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/kotlin/SimulationBuildersTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.simulator.kotlin
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+
+/**
+ * Test suite for the Kotlin simulation builders.
+ */
+class SimulationBuildersTest {
+ @Test
+ fun testDelay() = runSimulation {
+ assertEquals(0, currentTime)
+ delay(100)
+ assertEquals(100, currentTime)
+ }
+
+ @Test
+ fun testController() = runSimulation {
+ var completed = false
+
+ launch {
+ delay(20)
+ completed = true
+ }
+
+ advanceBy(10)
+ assertFalse(completed)
+ advanceBy(11)
+ assertTrue(completed)
+
+ completed = false
+ launch { completed = true }
+ runCurrent()
+ assertTrue(completed)
+ }
+
+ @Test
+ fun testFailOnActiveJobs() {
+ assertThrows<IllegalStateException> {
+ runSimulation {
+ launch { suspendCancellableCoroutine {} }
+ }
+ }
+ }
+
+ @Test
+ fun testPropagateException() {
+ assertThrows<IllegalStateException> {
+ runSimulation {
+ throw IllegalStateException("Test")
+ }
+ }
+ }
+
+ @Test
+ fun testInvalidDispatcher() {
+ assertThrows<IllegalArgumentException> {
+ runSimulation(Dispatchers.Default) { }
+ }
+ }
+
+ @Test
+ fun testExistingJob() {
+ runSimulation(Job()) {
+ delay(10)
+ }
+ }
+}