From 6ca0ae07669d20a5a34ef697610df90754024035 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 20 Nov 2019 17:51:58 +0100 Subject: refactor: Move build logic to buildSrc --- odcsim/README.md | 100 ++++ odcsim/build.gradle.kts | 23 + odcsim/odcsim-core/build.gradle.kts | 38 ++ .../main/kotlin/com/atlarge/odcsim/ActorContext.kt | 175 +++++++ .../main/kotlin/com/atlarge/odcsim/ActorPath.kt | 142 ++++++ .../src/main/kotlin/com/atlarge/odcsim/ActorRef.kt | 50 ++ .../main/kotlin/com/atlarge/odcsim/ActorSystem.kt | 76 +++ .../com/atlarge/odcsim/ActorSystemFactory.kt | 38 ++ .../src/main/kotlin/com/atlarge/odcsim/Behavior.kt | 193 +++++++ .../main/kotlin/com/atlarge/odcsim/Behaviors.kt | 223 ++++++++ .../src/main/kotlin/com/atlarge/odcsim/Envelope.kt | 57 +++ .../src/main/kotlin/com/atlarge/odcsim/Signals.kt | 60 +++ .../main/kotlin/com/atlarge/odcsim/StashBuffer.kt | 88 ++++ .../src/main/kotlin/com/atlarge/odcsim/Time.kt | 75 +++ .../kotlin/com/atlarge/odcsim/TimerScheduler.kt | 92 ++++ .../com/atlarge/odcsim/coroutines/Behavior.kt | 116 +++++ .../com/atlarge/odcsim/coroutines/dsl/Receive.kt | 66 +++ .../com/atlarge/odcsim/coroutines/dsl/Timeout.kt | 42 ++ .../com/atlarge/odcsim/internal/ActorContext.kt | 43 ++ .../kotlin/com/atlarge/odcsim/internal/Behavior.kt | 48 ++ .../atlarge/odcsim/internal/BehaviorInterpreter.kt | 201 ++++++++ .../com/atlarge/odcsim/internal/Coroutines.kt | 182 +++++++ .../com/atlarge/odcsim/internal/StashBufferImpl.kt | 74 +++ .../atlarge/odcsim/internal/TimerSchedulerImpl.kt | 122 +++++ .../internal/logging/LocationAwareLoggerImpl.kt | 567 +++++++++++++++++++++ .../internal/logging/LocationIgnorantLoggerImpl.kt | 440 ++++++++++++++++ .../atlarge/odcsim/internal/logging/LoggerImpl.kt | 77 +++ .../kotlin/com/atlarge/odcsim/ActorPathTest.kt | 86 ++++ .../test/kotlin/com/atlarge/odcsim/BehaviorTest.kt | 77 +++ .../kotlin/com/atlarge/odcsim/CoroutinesTest.kt | 63 +++ odcsim/odcsim-engine-omega/build.gradle.kts | 46 ++ .../odcsim/engine/omega/OmegaActorSystem.kt | 360 +++++++++++++ .../odcsim/engine/omega/OmegaActorSystemFactory.kt | 38 ++ .../services/com.atlarge.odcsim.ActorSystemFactory | 1 + .../engine/omega/OmegaActorSystemFactoryTest.kt | 37 ++ .../odcsim/engine/omega/OmegaActorSystemTest.kt | 37 ++ odcsim/odcsim-engine-tests/build.gradle.kts | 40 ++ .../odcsim/engine/tests/ActorSystemContract.kt | 403 +++++++++++++++ .../engine/tests/ActorSystemFactoryContract.kt | 73 +++ 39 files changed, 4669 insertions(+) create mode 100644 odcsim/README.md create mode 100644 odcsim/build.gradle.kts create mode 100644 odcsim/odcsim-core/build.gradle.kts create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorContext.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorPath.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorRef.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorSystem.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorSystemFactory.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behavior.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behaviors.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Envelope.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Signals.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/StashBuffer.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Time.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/TimerScheduler.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/Behavior.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/dsl/Receive.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/dsl/Timeout.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/ActorContext.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/Behavior.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/BehaviorInterpreter.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/Coroutines.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/StashBufferImpl.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/TimerSchedulerImpl.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LocationAwareLoggerImpl.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LocationIgnorantLoggerImpl.kt create mode 100644 odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LoggerImpl.kt create mode 100644 odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorPathTest.kt create mode 100644 odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/BehaviorTest.kt create mode 100644 odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/CoroutinesTest.kt create mode 100644 odcsim/odcsim-engine-omega/build.gradle.kts create mode 100644 odcsim/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystem.kt create mode 100644 odcsim/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactory.kt create mode 100644 odcsim/odcsim-engine-omega/src/main/resources/META-INF/services/com.atlarge.odcsim.ActorSystemFactory create mode 100644 odcsim/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactoryTest.kt create mode 100644 odcsim/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemTest.kt create mode 100644 odcsim/odcsim-engine-tests/build.gradle.kts create mode 100644 odcsim/odcsim-engine-tests/src/main/kotlin/com/atlarge/odcsim/engine/tests/ActorSystemContract.kt create mode 100644 odcsim/odcsim-engine-tests/src/main/kotlin/com/atlarge/odcsim/engine/tests/ActorSystemFactoryContract.kt (limited to 'odcsim') diff --git a/odcsim/README.md b/odcsim/README.md new file mode 100644 index 00000000..00d6a2fe --- /dev/null +++ b/odcsim/README.md @@ -0,0 +1,100 @@ +

+ + OpenDC + +
+ odcsim +

+ +## Introduction +**odcsim** is a framework for discrete event simulation in Kotlin and Java, used +by the [OpenDC](https://opendc.org) project. +Simulations are defined in terms of a hierarchical grouping of actors +and the interactions between these actors +([Actor model](https://en.wikipedia.org/wiki/Actor_model)), using +an API very similar to [Akka Typed](https://doc.akka.io/docs/akka/current/typed/index.html). + +## Documentation +Check out the [Getting Started](#getting-started) section for a quick +overview. +The documentation is located in the [docs/](docs/) directory and is divided as follows: +* [Main Concepts](docs/concepts.md) +* [Building a Model](docs/build.md) +* [Running a Model](docs/run.md) +* [Pre-built Models](docs/models.md) +* [API Reference](https://atlarge-research.github.io/opendc-simulator) +* [Contributing Guide](CONTRIBUTING.md) + +## Getting Started + +### Installation +Please add the required packages as dependency in your project. +Releases are available in the [Maven Central Repository](https://search.maven.org/). + +The package `odcsim-core` is required to construct a simulation model. +A `odcsim-engine-*` package is needed for running the simulation +model. + +**Gradle** +```groovy +compile 'com.atlarge.odcsim:odcsim-core:2.0.0' +compile 'com.atlarge.odcsim:odcsim-engine-omega:2.0.0' +``` + +**Maven** +```xml + + com.atlarge.odcsim + odcsim-core + 2.0.0 + + + + com.atlarge.odcsim + odcsim-engine-omega + 2.0.0 + +``` + +### Construction of Simulation Model +Let's construct a simple simulation model of a single car actor. +The car will alternately drive and park for a while. When it starts +driving (or parking), it will print the current simulation time. + + +```kotlin +import com.atlarge.odcsim.Behavior +import com.atlarge.odcsim.coroutines.suspending +import com.atlarge.odcsim.coroutines.dsl.timeout + +fun car(): Behavior = + suspending { ctx -> + while (true) { + println("Start parking at ${ctx.time}") + val parkingDuration = 5.0 + timeout(parkingDuration) + + println("Start driving at ${ctx.time}") + val tripDuration = 2.0 + timeout(tripDuration) + } + + stopped() + } +``` + +### Running Simulation +Running the constructed simulation model requires an implementation +of the `ActorSystem` interface provided by one of the `odcsim-engine-*` +packages. The [ServiceLoader](https://docs.oracle.com/javase/9/docs/api/java/util/ServiceLoader.html) +class found in the JDK can be used to locate the `ActorSystem` implementation on the classpath. +```kotlin +import com.atlarge.odcsim.ActorSystemFactory +import java.util.ServiceLoader + +val factory = ServiceLoader.load(ActorSystemFactory::class.java).first() +val system = factory(car(), name = "car") +system.run(until = 10.0) +system.terminate() +``` + diff --git a/odcsim/build.gradle.kts b/odcsim/build.gradle.kts new file mode 100644 index 00000000..cc3f3add --- /dev/null +++ b/odcsim/build.gradle.kts @@ -0,0 +1,23 @@ +/* + * MIT License + * + * Copyright (c) 2019 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. + */ diff --git a/odcsim/odcsim-core/build.gradle.kts b/odcsim/odcsim-core/build.gradle.kts new file mode 100644 index 00000000..013d1598 --- /dev/null +++ b/odcsim/odcsim-core/build.gradle.kts @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2017 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. + */ + +/* Build configuration */ +plugins { + `kotlin-library-convention` +} + +dependencies { + implementation(kotlin("stdlib")) + api("org.slf4j:slf4j-api:${Library.SLF4J}") + + testImplementation("org.junit.jupiter:junit-jupiter-api:${Library.JUNIT_JUPITER}") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${Library.JUNIT_JUPITER}") + testImplementation("org.junit.platform:junit-platform-launcher:${Library.JUNIT_PLATFORM}") + testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.0.0") +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorContext.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorContext.kt new file mode 100644 index 00000000..dc6ca7ec --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorContext.kt @@ -0,0 +1,175 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim + +import org.slf4j.Logger + +/** + * Represents the context in which the execution of an actor's behavior takes place. + * + * @param T The shape of the messages the actor accepts. + */ +interface ActorContext { + /** + * The identity of the actor, bound to the lifecycle of this actor instance. + */ + val self: ActorRef + + /** + * A view of the children of this actor. + */ + val children: List> + + /** + * The point of time within the simulation. + */ + val time: Instant + + /** + * The [ActorSystem] the actor is part of. + */ + val system: ActorSystem<*> + + /** + * An actor specific logger instance. + */ + val log: Logger + + /** + * Obtain the child of this actor with the specified name. + * + * @param name The name of the child actor to obtain. + * @return The reference to the child actor or `null` if it does not exist. + */ + fun getChild(name: String): ActorRef<*>? + + /** + * Send the specified message to the actor referenced by this [ActorRef]. + * + * Please note that callees must guarantee that messages are sent strictly in increasing time. + * If so, this method guarantees that: + * - A message will never be received earlier than specified + * - A message might arrive later than specified if the two actors are not synchronized. + * + * @param ref The actor to send the message to. + * @param msg The message to send to the referenced actor. + * @param after The delay after which the message should be received by the actor. + */ + fun send(ref: ActorRef, msg: U, after: Duration = 0.0) + + /** + * Spawn a child actor from the given [Behavior] and with the specified name. + * + * The name may not be empty or start with "$". Moreover, the name of an actor must be unique and this method + * will throw an [IllegalArgumentException] in case a child actor of the given name already exists. + * + * @param behavior The behavior of the child actor to spawn. + * @param name The name of the child actor to spawn. + * @return A reference to the child that has/will be spawned. + */ + fun spawn(behavior: Behavior, name: String): ActorRef + + /** + * Spawn an anonymous child actor from the given [Behavior]. + * + * @param behavior The behavior of the child actor to spawn. + * @return A reference to the child that has/will be spawned. + */ + fun spawnAnonymous(behavior: Behavior): ActorRef + + /** + * Force the specified child actor to terminate after it finishes processing its current message. + * Nothing will happen if the child is already stopped. + * + * Only direct children of an actor may be stopped through the actor context. Trying to stop other actors via this + * method will result in an [IllegalArgumentException]. Instead, stopping other actors has to be expressed as + * an explicit stop message that the actor accept. + * + * @param child The reference to the child actor to stop. + */ + fun stop(child: ActorRef<*>) + + /** + * Watch the specified [ActorRef] for termination of the referenced actor. On termination of the watched actor, + * a [Terminated] signal is sent to this actor. + * + * @param target The target actor to watch. + */ + fun watch(target: ActorRef<*>) + + /** + * Revoke the registration established by [watch]. + * + * In case there exists no registration for the specified [target], no action will be performed. + * + * @param target The target actor to unwatch. + */ + fun unwatch(target: ActorRef<*>) + + /** + * Synchronize the local virtual time of this target with the other referenced actor's local virtual time. + * + * By default, actors are not guaranteed to be synchronized, meaning that for some implementations, virtual time may + * drift between different actors. Synchronization between two actors ensures that virtual time remains consistent + * between at least the two actors. + * + * Be aware that this method may cause a jump in virtual time in order to get consistent with [target]. + * Furthermore, please note that synchronization might incur performance degradation and should only be used + * when necessary. + * + * @param target The reference to the target actor to synchronize with. + */ + fun sync(target: ActorRef<*>) + + /** + * Desynchronize virtual time between two actors if possible. + * + * Please note that this method only provides a hint to the [ActorSystem] that it may drop synchronization between + * the actors, but [ActorSystem] is not compelled to actually do so (i.e. in the case where synchronization is + * always guaranteed). + * + * Furthermore, if [target] is already desychronized, the method should return without error. [ActorContext.isSync] + * may be used to determine if an actor is synchronized. + * + * @param target The reference to the target actor to desynchronize with. + */ + fun unsync(target: ActorRef<*>) + + /** + * Determine whether this actor and [target] are synchronized in virtual time. + * + * @param target The target to check for synchronization. + * @return `true` if [target] is synchronized with this actor, `false` otherwise. + */ + fun isSync(target: ActorRef<*>): Boolean +} + +/** + * Unsafe helper method for widening the type accepted by this [ActorContext]. + */ +fun ActorContext.unsafeCast(): ActorContext { + @Suppress("UNCHECKED_CAST") + return this as ActorContext +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorPath.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorPath.kt new file mode 100644 index 00000000..a6c716a2 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorPath.kt @@ -0,0 +1,142 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim + +import java.io.Serializable + +/** + * An actor path represents the unique path to a specific actor instance within an [ActorSystem]. + */ +sealed class ActorPath : Comparable, Serializable { + /** + * The name of the actor that this path refers to. + */ + abstract val name: String + + /** + * The path for the parent actor. + */ + abstract val parent: ActorPath + + /** + * Walk up the tree to obtain and return the [ActorPath.Root]. + */ + abstract val root: Root + + /** + * Create a new child actor path. + */ + fun child(name: String): ActorPath = Child(this, name) + + /** + * Create a new child actor path. + */ + operator fun div(name: String): ActorPath = child(name) + + /** + * Recursively create a descendant’s path by appending all child names. + */ + fun descendant(children: Iterable): ActorPath = children.fold(this) { parent, name -> + if (name.isNotBlank()) child(name) else parent + } + + /** + * Root of the hierarchy of [ActorPath]s. There is exactly root per [ActorSystem]. + */ + data class Root(override val name: String = "/") : ActorPath() { + init { + require(name.length == 1 || name.indexOf('/', 1) == -1) { + "/ may only exist at the beginning of the root actors name" + } + require(name.indexOf('#') == -1) { "# may not exist in a path component" } + } + + override val parent: ActorPath = this + + override val root: Root = this + + /** + * Compare the [specified][other] path with this root node for order. If both paths are roots, compare their + * name, otherwise the root is ordered higher. + * + * @return a negative integer, zero, or a positive integer as this object is less than, equal to, or greater + * than the specified path. + */ + override fun compareTo(other: ActorPath): Int = if (other is Root) name.compareTo(other.name) else 1 + + /** + * Create a string representation of this root node which prints its own [name]. + * + * @return A string representation of this node. + */ + override fun toString(): String = name + } + + /** + * A child in the hierarchy of [ActorPath]s. + */ + data class Child(override val parent: ActorPath, override val name: String) : ActorPath() { + init { + require(name.indexOf('/') == -1) { "/ may not exist in a path component" } + require(name.indexOf('#') == -1) { "# may not exist in a path component" } + } + + override val root: Root by lazy { + when (parent) { + is Root -> parent + else -> parent.root + } + } + + /** + * Compare the [specified][other] path with this child node for order. + * + * @return a negative integer, zero, or a positive integer as this object is less than, equal to, or greater + * than the specified path. + */ + override fun compareTo(other: ActorPath): Int { + tailrec fun rec(left: ActorPath, right: ActorPath): Int = when { + left == right -> 0 + left is Root -> left.compareTo(right) + right is Root -> -(right.compareTo(left)) + else -> { + val x = left.name.compareTo(right.name) + if (x == 0) + rec(left.parent, right.parent) + else + x + } + } + return rec(this, other) + } + + /** + * Create a string representation of this child node which prints the name of [parent] and its own [name]. + * + * @return A string representation of this node. + */ + override fun toString(): String = "$parent/$name" + } +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorRef.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorRef.kt new file mode 100644 index 00000000..45fc756e --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorRef.kt @@ -0,0 +1,50 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim + +import java.io.Serializable + +/** + * A reference to an entity in simulation that accepts messages of type [T]. + */ +interface ActorRef : Comparable>, Serializable { + /** + * The path for this actor (from this actor up to the root actor). + */ + val path: ActorPath + + /** + * Compare this reference to another actor reference. + */ + override fun compareTo(other: ActorRef<*>): Int = path.compareTo(other.path) +} + +/** + * Unsafe helper method for widening the type accepted by this [ActorRef]. + */ +fun ActorRef.unsafeCast(): ActorRef { + @Suppress("UNCHECKED_CAST") + return this as ActorRef +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorSystem.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorSystem.kt new file mode 100644 index 00000000..d65beebd --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorSystem.kt @@ -0,0 +1,76 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim + +/** + * An actor system is a hierarchical grouping of actors that represents a discrete event simulation. + * + * An implementation of this interface should be provided by an engine. See for example *odcsim-engine-omega*, + * which is the reference implementation of the *odcsim* API. + * + * @param T The shape of the messages the root actor in the system can receive. + */ +interface ActorSystem : ActorRef { + /** + * The current point in simulation time. + */ + val time: Instant + + /** + * The name of this engine instance, used to distinguish between multiple engines running within the same JVM. + */ + val name: String + + /** + * Run the actors until the specified point in simulation time. + * + * @param until The point until which the simulation should run. + */ + fun run(until: Duration = Duration.POSITIVE_INFINITY) + + /** + * Send the specified message to the root actor of this [ActorSystem]. + * + * @param msg The message to send to the referenced actor. + * @param after The delay after which the message should be received by the actor. + */ + fun send(msg: T, after: Duration = 0.1) + + /** + * Terminates this actor system in an asynchronous fashion. + * + * This will stop the root actor and in turn will recursively stop all its child actors. + */ + fun terminate() + + /** + * Create an actor in the "/system" namespace. This actor will be shut down during `system.terminate()` only after + * all user actors have terminated. + * + * @param behavior The behavior of the system actor to spawn. + * @param name The name of the system actor to spawn. + */ + suspend fun spawnSystem(behavior: Behavior, name: String): ActorRef +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorSystemFactory.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorSystemFactory.kt new file mode 100644 index 00000000..f59bc966 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorSystemFactory.kt @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim + +/** + * A factory for [ActorSystem] instances that allows users to dynamically load engine implementations. + */ +interface ActorSystemFactory { + /** + * Create an [ActorSystem] with the given root [Behavior] and the given name. + * + * @param root The behavior of the root actor. + * @param name The name of the engine instance. + */ + operator fun invoke(root: Behavior, name: String): ActorSystem +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behavior.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behavior.kt new file mode 100644 index 00000000..9ad7f83f --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behavior.kt @@ -0,0 +1,193 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim + +/** + * The representation of the behavior of an actor. + * + * Behavior can be formulated using factory methods on the companion object or by extending either [DeferredBehavior] or + * [ReceivingBehavior]. + * + * Users are advised not to close over [ActorContext] within [Behavior], as it will causes it to become immobile, + * meaning it cannot be moved to another context and executed there, and therefore it cannot be replicated or forked + * either. + * + * @param T The shape of the messages the behavior accepts. + */ +sealed class Behavior { + /** + * Narrow the type of this behavior. + * + * This is almost always a safe operation, but might cause [ClassCastException] in case a narrowed behavior sends + * messages of a different type to itself and is chained via [Behavior.orElse]. + */ + fun narrow(): Behavior = unsafeCast() + + /** + * Widen the type of this behavior by placing a funnel in front of it. + * + * @param transform The mapping from the widened type to the original type, returning `null` in-case the message + * should not be handled. + */ + fun widen(transform: (U) -> T?): Behavior { + return wrap(this) { interpreter -> + receive { ctx, msg -> + val res = transform(msg) + @Suppress("UNCHECKED_CAST") + if (res == null || interpreter.interpretMessage(ctx as ActorContext, res)) + unhandled() + else + interpreter.behavior.unsafeCast() + }.unsafeCast() + }.unsafeCast() + } + + /** + * Compose this [Behavior] with a fallback [Behavior] which is used in case this [Behavior] does not handle the + * incoming message or signal. + * + * @param that The fallback behavior. + */ + fun orElse(that: Behavior): Behavior = + wrap(this) { left -> + wrap(that) { right -> + object : ReceivingBehavior() { + override fun receive(ctx: ActorContext, msg: T): Behavior { + if (left.interpretMessage(ctx, msg)) { + return left.behavior + } else if (right.interpretMessage(ctx, msg)) { + return right.behavior + } + + return unhandled() + } + + override fun receiveSignal(ctx: ActorContext, signal: Signal): Behavior { + if (left.interpretSignal(ctx, signal)) { + return left.behavior + } else if (right.interpretSignal(ctx, signal)) { + return right.behavior + } + + return unhandled() + } + } + } + } + + /** + * Unsafe utility method for changing the type accepted by this [Behavior]. + * Be aware that changing the type might result in [ClassCastException], when sending a message to the resulting + * behavior. + */ + fun unsafeCast(): Behavior { + @Suppress("UNCHECKED_CAST") + return this as Behavior + } +} + +/** + * A [Behavior] that defers the construction of the actual [Behavior] until the actor is started in some [ActorContext]. + * If the actor is already started, it will immediately evaluate. + * + * @param T The shape of the messages the behavior accepts. + */ +abstract class DeferredBehavior : Behavior() { + /** + * Create a [Behavior] instance in the [specified][ctx] [ActorContext]. + * + * @param ctx The [ActorContext] in which the behavior runs. + * @return The actor's next behavior. + */ + abstract operator fun invoke(ctx: ActorContext): Behavior +} + +/** + * A [Behavior] that concretely defines how an actor will react to the messages and signals it receives. + * The message may either be of the type that the actor declares and which is part of the [ActorRef] signature, + * or it may be a system [Signal] that expresses a lifecycle event of either this actor or one of its child actors. + * + * @param T The shape of the messages the behavior accepts. + */ +abstract class ReceivingBehavior : Behavior() { + /** + * Process an incoming message of type [T] and return the actor's next behavior. + * + * The returned behavior can in addition to normal behaviors be one of the canned special objects: + * - returning [stopped] will terminate this Behavior + * - returning [same] designates to reuse the current Behavior + * - returning [unhandled] keeps the same Behavior and signals that the message was not yet handled + * + * @param ctx The [ActorContext] in which the actor is currently running. + * @param msg The message that was received. + */ + open fun receive(ctx: ActorContext, msg: T): Behavior = unhandled() + + /** + * Process an incoming [Signal] and return the actor's next behavior. + * + * The returned behavior can in addition to normal behaviors be one of the canned special objects: + * - returning [stopped] will terminate this Behavior + * - returning [same] designates to reuse the current Behavior + * - returning [unhandled] keeps the same Behavior and signals that the message was not yet handled + * + * @param ctx The [ActorContext] in which the actor is currently running. + * @param signal The [Signal] that was received. + */ + open fun receiveSignal(ctx: ActorContext, signal: Signal): Behavior = unhandled() +} + +/** + * A flag to indicate whether a [Behavior] instance is still alive. + */ +val Behavior.isAlive get() = this !is StoppedBehavior + +/** + * A flag to indicate whether the last message/signal went unhandled. + */ +val Behavior.isUnhandled get() = this is UnhandledBehavior + +// The special behaviors are kept in this file as to be able to seal the Behavior class to prevent users from extending +// it. +/** + * A special [Behavior] instance that signals that the actor has stopped. + */ +internal object StoppedBehavior : Behavior() { + override fun toString() = "Stopped" +} + +/** + * A special [Behavior] object to signal that the actor wants to reuse its previous behavior. + */ +internal object SameBehavior : Behavior() { + override fun toString() = "Same" +} + +/** + * A special [Behavior] object that indicates that the last message or signal was not handled. + */ +internal object UnhandledBehavior : Behavior() { + override fun toString() = "Unhandled" +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behaviors.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behaviors.kt new file mode 100644 index 00000000..eac254ec --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behaviors.kt @@ -0,0 +1,223 @@ +/* + * MIT License + * + * Copyright (c) 2019 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. + */ +@file:JvmName("Behaviors") +package com.atlarge.odcsim + +import com.atlarge.odcsim.internal.BehaviorInterpreter +import com.atlarge.odcsim.internal.EmptyBehavior +import com.atlarge.odcsim.internal.IgnoreBehavior +import com.atlarge.odcsim.internal.TimerSchedulerImpl +import com.atlarge.odcsim.internal.sendSignal + +/** + * This [Behavior] is used to signal that this actor shall terminate voluntarily. If this actor has created child actors + * then these will be stopped as part of the shutdown procedure. + */ +fun stopped(): Behavior = StoppedBehavior.unsafeCast() + +/** + * This [Behavior] is used to signal that this actor wants to reuse its previous behavior. + */ +fun same(): Behavior = SameBehavior.unsafeCast() + +/** + * This [Behavior] is used to signal to the system that the last message or signal went unhandled. This will + * reuse the previous behavior. + */ +fun unhandled(): Behavior = UnhandledBehavior.unsafeCast() + +/** + * A factory for [Behavior]. Creation of the behavior instance is deferred until the actor is started. + */ +fun setup(block: (ActorContext) -> Behavior): Behavior { + return object : DeferredBehavior() { + override fun invoke(ctx: ActorContext): Behavior = block(ctx) + } +} + +/** + * A [Behavior] that ignores any incoming message or signal and keeps the same behavior. + */ +fun ignore(): Behavior = IgnoreBehavior.narrow() + +/** + * A [Behavior] that treats every incoming message or signal as unhandled. + */ +fun empty(): Behavior = EmptyBehavior.narrow() + +/** + * Construct a [Behavior] that reacts to incoming messages, provides access to the [ActorContext] and returns the + * actor's next behavior. + */ +fun receive(handler: (ActorContext, T) -> Behavior): Behavior { + return object : ReceivingBehavior() { + override fun receive(ctx: ActorContext, msg: T): Behavior = handler(ctx, msg) + } +} + +/** + * Construct a [Behavior] that reacts to incoming messages of type [U], provides access to the [ActorContext] and + * returns the actor's next behavior. Other messages will be unhandled. + */ +inline fun receiveOf(crossinline handler: (ActorContext, U) -> Behavior): Behavior { + return object : ReceivingBehavior() { + override fun receive(ctx: ActorContext, msg: T): Behavior { + return if (msg is U) + handler(ctx, msg) + else + unhandled() + } + } +} + +/** + * Construct a [Behavior] that reacts to incoming messages and returns the actor's next behavior. + */ +fun receiveMessage(handler: (T) -> Behavior): Behavior { + return object : ReceivingBehavior() { + override fun receive(ctx: ActorContext, msg: T): Behavior = handler(msg) + } +} + +/** + * Construct a [Behavior] that reacts to incoming signals, provides access to the [ActorContext] and returns the + * actor's next behavior. + */ +fun receiveSignal(handler: (ActorContext, Signal) -> Behavior): Behavior { + return object : ReceivingBehavior() { + override fun receiveSignal(ctx: ActorContext, signal: Signal): Behavior = handler(ctx, signal) + } +} + +/** + * Construct a [Behavior] that wraps another behavior instance and uses a [BehaviorInterpreter] to pass incoming + * messages and signals to the wrapper behavior. + */ +fun wrap(behavior: Behavior, wrap: (BehaviorInterpreter) -> Behavior): Behavior { + return setup { ctx -> wrap(BehaviorInterpreter(behavior, ctx)) } +} + +/** + * Obtain a [TimerScheduler] for building a [Behavior] instance. + */ +fun withTimers(handler: (TimerScheduler) -> Behavior): Behavior { + return setup { ctx -> + val scheduler = TimerSchedulerImpl(ctx) + receiveSignal { _, signal -> + if (signal is TimerSchedulerImpl.TimerSignal) { + val res = scheduler.interceptTimerSignal(signal) + if (res != null) { + ctx.send(ctx.self, res) + return@receiveSignal same() + } + } + unhandled() + }.join(handler(scheduler)) + } +} + +/** + * Construct a [Behavior] that waits for the specified duration before constructing the next behavior. + * + * @param after The delay before constructing the next behavior. + * @param handler The handler to construct the behavior with. + */ +fun withTimeout(after: Duration, handler: (ActorContext) -> Behavior): Behavior = + setup { ctx -> + val target = Any() + ctx.sendSignal(ctx.self, Timeout(target), after) + receiveSignal { _, signal -> + if (signal is Timeout && signal.target == target) { + handler(ctx) + } else { + unhandled() + } + } + } + +/** + * Join together both [Behavior] with another [Behavior], essentially running them side-by-side, only directly + * propagating stopped behavior. + * + * @param that The behavior to join with. + */ +fun Behavior.join(that: Behavior): Behavior = + wrap(this) { left -> + wrap(that) { right -> + object : ReceivingBehavior() { + override fun receive(ctx: ActorContext, msg: T): Behavior { + if (left.interpretMessage(ctx, msg)) { + return left.propagate(this) // Propagate stopped behavior + } else if (right.interpretMessage(ctx, msg)) { + return right.propagate(this) + } + + return unhandled() + } + + override fun receiveSignal(ctx: ActorContext, signal: Signal): Behavior { + if (left.interpretSignal(ctx, signal)) { + return left.propagate(this) + } else if (right.interpretSignal(ctx, signal)) { + return right.propagate(this) + } + + return unhandled() + } + } + } + } + +/** + * Widen the type of messages the [Behavior] by marking all other messages as unhandled. + */ +inline fun Behavior.widen(): Behavior = widen { + if (it is T) + it + else + null +} + +/** + * Keep the specified [Behavior] alive if it returns the stopped behavior. + */ +fun Behavior.keepAlive(): Behavior = + wrap(this) { interpreter -> + object : ReceivingBehavior() { + override fun receive(ctx: ActorContext, msg: T): Behavior { + if (interpreter.interpretMessage(ctx, msg)) { + return this + } + return empty() + } + + override fun receiveSignal(ctx: ActorContext, signal: Signal): Behavior { + if (interpreter.interpretSignal(ctx, signal)) { + return this + } + + return empty() + } + } + } diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Envelope.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Envelope.kt new file mode 100644 index 00000000..3b73d52d --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Envelope.kt @@ -0,0 +1,57 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim + +import java.io.Serializable + +/** + * A timestamped wrapper for messages that will be delivered to an actor. + */ +interface Envelope : Comparable>, Serializable { + /** + * The time at which this message should be delivered. + */ + val time: Instant + + /** + * The message contained in this envelope, of type [T] + */ + val message: T + + /** + * Extract the delivery time from the envelope. + */ + operator fun component1(): Instant = time + + /** + * Extract the message from this envelope. + */ + operator fun component2(): T = message + + /** + * Compare this envelope to the [other] envelope, ordered increasingly in time. + */ + override fun compareTo(other: Envelope<*>): Int = time.compareTo(other.time) +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Signals.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Signals.kt new file mode 100644 index 00000000..9b707348 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Signals.kt @@ -0,0 +1,60 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim + +/** + * System signals are notifications that are generated by the system and delivered to the actor behavior in a reliable + * fashion. + */ +interface Signal + +/** + * Lifecycle signal that is fired upon creation of the actor. This will be the first message that the actor receives. + */ +object PreStart : Signal + +/** + * Lifecycle signal that is fired after this actor and all its child actors (transitively) have terminated. + * The [Terminated] signal is only sent to registered watchers after this signal has been processed. + */ +object PostStop : Signal + +/** + * A lifecycle signal to indicate that an actor that was watched has terminated. + * + * @property ref The reference to the actor that has terminated. + * @property failure The failure that caused the termination, or `null` on graceful termination. + */ +data class Terminated(val ref: ActorRef<*>, val failure: Throwable? = null) : Signal + +/** + * A [Signal] to indicate an actor has timed out. + * + * This class contains a [target] property in order to allow nested behavior to function properly when multiple layers + * are waiting on this signal. + * + * @property target The target object that has timed out. + */ +data class Timeout(val target: Any) : Signal diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/StashBuffer.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/StashBuffer.kt new file mode 100644 index 00000000..5d73d808 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/StashBuffer.kt @@ -0,0 +1,88 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim + +import com.atlarge.odcsim.internal.StashBufferImpl + +/** + * A non thread safe mutable message buffer that can be used to buffer messages inside actors and then unstash them. + * + * @param T The shape of the messages in this buffer. + */ +interface StashBuffer { + /** + * The first element of the buffer. + * + * @throws NoSuchElementException if the buffer is empty. + */ + val head: T + + /** + * A flag to indicate whether the buffer is empty. + */ + val isEmpty: Boolean + + /** + * A flag to indicate whether the buffer is full. + */ + val isFull: Boolean + + /** + * The number of elements in the stash buffer. + */ + val size: Int + + /** + * Iterate over all elements of the buffer and apply a function to each element, without removing them. + * + * @param block The function to invoke for each element. + */ + fun forEach(block: (T) -> Unit) + + /** + * Add one element to the end of the message buffer. + * + * @param msg The message to stash. + * @throws IllegalStateException if the element cannot be added at this time due to capacity restrictions + */ + fun stash(msg: T) + + /** + * Process all stashed messages with the behavior and the returned [Behavior] from each processed message. + * + * @param ctx The actor context to process these messages in. + * @param behavior The behavior to process the messages with. + */ + fun unstashAll(ctx: ActorContext, behavior: Behavior): Behavior + + companion object { + /** + * Construct a [StashBuffer] with the specified [capacity]. + * + * @param capacity The capacity of the buffer. + */ + operator fun invoke(capacity: Int): StashBuffer = StashBufferImpl(capacity) + } +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Time.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Time.kt new file mode 100644 index 00000000..f19f6fe2 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Time.kt @@ -0,0 +1,75 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim + +/** + * An instantaneous point on the time-line, used to record message time-stamps in a simulation. + */ +typealias Instant = Double + +/** + * A time interval which represents the amount of elapsed time between two messages. + */ +typealias Duration = Double + +/** + * Convert this [Int] into an [Instant]. + */ +fun Int.toInstant(): Instant = toDouble() + +/** + * Convert this [Int] into a [Duration]. + */ +fun Int.toDuration(): Duration = toDouble() + +/** + * Convert this [Long] into an [Instant]. + */ +fun Long.toInstant(): Instant = toDouble() + +/** + * Convert this [Long] into a [Duration]. + */ +fun Long.toDuration(): Duration = toDouble() + +/** + * Convert this [Float] into an [Instant]. + */ +fun Float.toInstant(): Instant = toDouble() + +/** + * Convert this [Float] into a [Duration]. + */ +fun Float.toDuration(): Duration = toDouble() + +/** + * Convert this [Double] into an [Instant]. + */ +fun Double.toInstant(): Instant = toDouble() + +/** + * Convert this [Double] into a [Duration]. + */ +fun Double.toDuration(): Duration = toDouble() diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/TimerScheduler.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/TimerScheduler.kt new file mode 100644 index 00000000..c5c54b64 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/TimerScheduler.kt @@ -0,0 +1,92 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim + +/** + * An interface to provide support for scheduled self messages in an actor. It is used with [withTimers]. + * Timers are bound to the lifecycle of the actor that owns it, and thus are cancelled automatically when it is + * restarted or stopped. + * + * Please be aware that [TimerScheduler] is not thread-safe and must only be used within the actor that owns it. + * + * @param T The shape of the messages the owning actor of this scheduling accepts. + */ +interface TimerScheduler { + /** + * Cancel a timer with the given key. + * + * @param key The key of the timer. + */ + fun cancel(key: Any) + + /** + * Cancel all timers. + */ + fun cancelAll() + + /** + * Check if a timer with a given [key] is active. + * + * @param key The key to check if it is active. + * @return `true` if a timer with the specified key is active, `false` otherwise. + */ + fun isTimerActive(key: Any): Boolean + + /** + * Start a periodic timer that will send [msg] to the `self` actor at a fixed [interval]. + * + * @param key The key of the timer. + * @param msg The message to send to the actor. + * @param interval The interval of simulation time after which it should be sent. + */ + fun startPeriodicTimer(key: Any, msg: T, interval: Duration) + + /** + * Start a timer that will send [msg] once to the `self` actor after the given [delay]. + * + * @param key The key of the timer. + * @param msg The message to send to the actor. + * @param delay The delay in simulation time after which it should be sent. + */ + fun startSingleTimer(key: Any, msg: T, delay: Duration) + + /** + * Run [block] periodically at a fixed [interval] + * + * @param key The key of the timer. + * @param interval The delay of simulation time after which the block should run. + * @param block The block to run. + */ + fun every(key: Any, interval: Duration, block: () -> Unit) + + /** + * Run [block] after the specified [delay]. + * + * @param key The key of the timer. + * @param delay The delay in simulation time after which the block should run. + * @param block The block to run. + */ + fun after(key: Any, delay: Duration, block: () -> Unit) +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/Behavior.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/Behavior.kt new file mode 100644 index 00000000..eb26add1 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/Behavior.kt @@ -0,0 +1,116 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim.coroutines + +import com.atlarge.odcsim.ActorContext +import com.atlarge.odcsim.Behavior +import com.atlarge.odcsim.DeferredBehavior +import com.atlarge.odcsim.Signal +import com.atlarge.odcsim.internal.SuspendingActorContextImpl +import com.atlarge.odcsim.internal.SuspendingBehaviorImpl +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.suspendCoroutine + +/** + * A [Behavior] that allows method calls to suspend execution via Kotlin coroutines. + * + * @param T The shape of the messages the actor accepts. + */ +abstract class SuspendingBehavior : DeferredBehavior() { + /** + * Run the suspending logic of this behavior. + * + * @param ctx The [SuspendingActorContext] in which the behavior is executed. + * @return The next behavior for the actor. + */ + abstract suspend operator fun invoke(ctx: SuspendingActorContext): Behavior + + // Immediately transfer to implementation + override fun invoke(ctx: ActorContext): Behavior = SuspendingBehaviorImpl(ctx, this).start() +} + +/** + * An [ActorContext] that provides additional functionality for receiving messages and signals from + * the actor's mailbox. + * + * @param T The shape of the messages the actor accepts. + */ +interface SuspendingActorContext : ActorContext, CoroutineContext.Element { + /** + * Suspend execution of the active coroutine to wait for a message of type [T] to be received in the actor's + * mailbox. During suspension, incoming signals will be marked unhandled. + * + * @return The message of type [T] that has been received. + */ + suspend fun receive(): T + + /** + * Suspend execution of the active coroutine to wait for a [Signal] to be received in the actor's mailbox. + * During suspension, incoming messages will be marked unhandled. + * + * @return The [Signal] that has been received. + */ + suspend fun receiveSignal(): Signal + + /** + * A key to provide access to the untyped [SuspendingActorContext] via [CoroutineContext] for suspending methods + * running inside a [SuspendingBehavior]. + */ + companion object Key : CoroutineContext.Key> +} + +/** + * Obtains the current continuation instance inside suspend functions and suspends currently running coroutine. [block] + * should return a [Behavior] that will resume the continuation and return the next behavior which is supplied via the + * second argument of the block. + */ +suspend fun suspendWithBehavior(block: (Continuation, () -> Behavior) -> Behavior): U = + suspendCoroutine { cont -> + @Suppress("UNCHECKED_CAST") + val ctx = cont.context[SuspendingActorContext] as? SuspendingActorContextImpl + ?: throw UnsupportedOperationException("Coroutine does not run inside SuspendingBehavior") + ctx.become(block(cont) { ctx.behavior }) + } + +/** + * Obtain the current [SuspendingActorContext] instance for the active continuation. + */ +suspend fun actorContext(): SuspendingActorContext = + suspendCoroutineUninterceptedOrReturn { cont -> + @Suppress("UNCHECKED_CAST") + cont.context[SuspendingActorContext] as? SuspendingActorContext + ?: throw UnsupportedOperationException("Coroutine does not run inside SuspendingBehavior") + } + +/** + * Construct a [Behavior] that uses Kotlin coroutines functionality to handle incoming messages and signals. + */ +fun suspending(block: suspend (SuspendingActorContext) -> Behavior): Behavior { + return object : SuspendingBehavior() { + override suspend fun invoke(ctx: SuspendingActorContext) = block(ctx) + } +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/dsl/Receive.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/dsl/Receive.kt new file mode 100644 index 00000000..e995c0e3 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/dsl/Receive.kt @@ -0,0 +1,66 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim.coroutines.dsl + +import com.atlarge.odcsim.ActorRef +import com.atlarge.odcsim.Duration +import com.atlarge.odcsim.coroutines.SuspendingActorContext +import com.atlarge.odcsim.coroutines.suspendWithBehavior +import com.atlarge.odcsim.receiveMessage +import com.atlarge.odcsim.unhandled +import kotlin.coroutines.resume + +/** + * Receive only messages of type [U] and mark all other messages as unhandled. + * + * @return The received message. + */ +suspend inline fun SuspendingActorContext.receiveOf(): U = + suspendWithBehavior { cont, next -> + receiveMessage { msg -> + if (msg is U) { + cont.resume(msg) + next() + } else { + unhandled() + } + } + } + +/** + * Send the specified message to the given reference and wait for a reply. + * + * @param ref The actor to send the message to. + * @param after The delay after which the message should be received by the actor. + * @param transform The block to transform `self` to a message. + */ +suspend inline fun SuspendingActorContext.ask( + ref: ActorRef, + after: Duration = 0.0, + transform: (ActorRef) -> U +): V { + send(ref, transform(self), after) + return receiveOf() +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/dsl/Timeout.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/dsl/Timeout.kt new file mode 100644 index 00000000..16b6f534 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/dsl/Timeout.kt @@ -0,0 +1,42 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim.coroutines.dsl + +import com.atlarge.odcsim.Duration +import com.atlarge.odcsim.coroutines.suspendWithBehavior +import com.atlarge.odcsim.withTimeout +import kotlin.coroutines.resume + +/** + * Block execution for the specified duration. + * + * @param after The duration after which execution should continue. + */ +suspend fun timeout(after: Duration) = suspendWithBehavior { cont, next -> + withTimeout(after) { + cont.resume(Unit) + next() + } +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/ActorContext.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/ActorContext.kt new file mode 100644 index 00000000..f1aba25e --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/ActorContext.kt @@ -0,0 +1,43 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim.internal + +import com.atlarge.odcsim.ActorContext +import com.atlarge.odcsim.ActorRef +import com.atlarge.odcsim.Duration +import com.atlarge.odcsim.Signal + +/** + * Send the specified [Signal] to the given actor reference after the specified duration. + * + * @param ref The actor to send the signal to. + * @param signal The signal to send to the referenced actor. + * @param after The delay after which the signal should be received by the actor. + */ +fun ActorContext<*>.sendSignal(ref: ActorRef<*>, signal: Signal, after: Duration = 0.0) { + // Signals are currently processed as regular messages + @Suppress("UNCHECKED_CAST") + send(ref as ActorRef, signal, after) +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/Behavior.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/Behavior.kt new file mode 100644 index 00000000..b07cabc0 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/Behavior.kt @@ -0,0 +1,48 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim.internal + +import com.atlarge.odcsim.ActorContext +import com.atlarge.odcsim.Behavior +import com.atlarge.odcsim.ReceivingBehavior +import com.atlarge.odcsim.Signal + +/** + * A [Behavior] object that ignores all messages sent to the actor. + */ +internal object IgnoreBehavior : ReceivingBehavior() { + override fun receive(ctx: ActorContext, msg: Any): Behavior = this + + override fun receiveSignal(ctx: ActorContext, signal: Signal): Behavior = this + + override fun toString() = "Ignore" +} + +/** + * A [Behavior] object that does not handle any message it receives. + */ +internal object EmptyBehavior : ReceivingBehavior() { + override fun toString() = "Empty" +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/BehaviorInterpreter.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/BehaviorInterpreter.kt new file mode 100644 index 00000000..194c2a62 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/BehaviorInterpreter.kt @@ -0,0 +1,201 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim.internal + +import com.atlarge.odcsim.ActorContext +import com.atlarge.odcsim.Behavior +import com.atlarge.odcsim.DeferredBehavior +import com.atlarge.odcsim.ReceivingBehavior +import com.atlarge.odcsim.SameBehavior +import com.atlarge.odcsim.Signal +import com.atlarge.odcsim.StoppedBehavior +import com.atlarge.odcsim.UnhandledBehavior +import com.atlarge.odcsim.isAlive +import com.atlarge.odcsim.isUnhandled + +/** + * Helper class that interprets messages/signals, canonicalizes special objects and manages the life-cycle of + * [Behavior] instances. + * + * @param initialBehavior The initial behavior to use. + */ +class BehaviorInterpreter(initialBehavior: Behavior) { + /** + * The current [Behavior] instance. + */ + var behavior: Behavior = initialBehavior + private set + + /** + * A flag to indicate the interpreter is still alive. + */ + val isAlive: Boolean get() = behavior.isAlive + + /** + * Construct a [BehaviorInterpreter] with the specified initial behavior and immediately start it in the specified + * context. + * + * @param initialBehavior The initial behavior of the actor. + * @param ctx The [ActorContext] to run the behavior in. + */ + constructor(initialBehavior: Behavior, ctx: ActorContext) : this(initialBehavior) { + start(ctx) + } + + /** + * Start the initial behavior. + * + * @param ctx The [ActorContext] to start the behavior in. + */ + fun start(ctx: ActorContext) { + behavior = validateAsInitial(start(ctx, behavior)) + } + + /** + * Stop the current active behavior and move into the stopped state. + * + * @param ctx The [ActorContext] this takes place in. + */ + fun stop(ctx: ActorContext) { + behavior = start(ctx, StoppedBehavior.narrow()) + } + + /** + * Replace the current behavior with the specified new behavior. + * + * @param ctx The [ActorContext] to run the behavior in. + * @param next The behavior to replace the current behavior with. + */ + fun become(ctx: ActorContext, next: Behavior) { + this.behavior = canonicalize(ctx, behavior, next) + } + + /** + * Propagate special states of the wrapper [Behavior] to the specified [Behavior]. This means + * that if the behavior of this interpreter is stopped or unhandled, this will be propagated. + * + * @param behavior The [Behavior] to map. + * @return Either the specified [Behavior] or the propagated special objects. + */ + fun propagate(behavior: Behavior): Behavior = + if (this.behavior.isUnhandled || !this.behavior.isAlive) + this.behavior + else + behavior + + /** + * Interpret the given message of type [T] using the current active behavior. + * + * @return `true` if the message was handled by the active behavior, `false` otherwise. + */ + fun interpretMessage(ctx: ActorContext, msg: T): Boolean = interpret(ctx, msg, false) + + /** + * Interpret the given [Signal] using the current active behavior. + * + * @return `true` if the signal was handled by the active behavior, `false` otherwise. + */ + fun interpretSignal(ctx: ActorContext, signal: Signal): Boolean = interpret(ctx, signal, true) + + /** + * Interpret the given message or signal using the current active behavior. + * + * @return `true` if the message or signal was handled by the active behavior, `false` otherwise. + */ + private fun interpret(ctx: ActorContext, msg: Any, isSignal: Boolean): Boolean = + if (isAlive) { + val next = when (val current = behavior) { + is DeferredBehavior -> + throw IllegalStateException("Deferred [$current] should not be passed to interpreter") + is ReceivingBehavior -> + if (isSignal) + current.receiveSignal(ctx, msg as Signal) + else + @Suppress("UNCHECKED_CAST") + current.receive(ctx, msg as T) + is SameBehavior, is UnhandledBehavior -> + throw IllegalStateException("Cannot execute with [$current] as behavior") + is StoppedBehavior -> current + } + + val unhandled = next.isUnhandled + behavior = canonicalize(ctx, behavior, next) + !unhandled + } else { + false + } + + /** + * Validate whether the given [Behavior] can be used as initial behavior. Throw an [IllegalArgumentException] if + * the [Behavior] is not valid. + * + * @param behavior The behavior to validate. + */ + private fun validateAsInitial(behavior: Behavior): Behavior = + when (behavior) { + is SameBehavior, is UnhandledBehavior -> + throw IllegalArgumentException("Cannot use [$behavior] as initial behavior") + else -> behavior + } + + /** + * Helper methods to properly manage the special, canned behavior objects. It highly recommended to use the + * [BehaviorInterpreter] instead to properly manage the life-cycles of the behavior objects. + */ + companion object { + /** + * Start the initial behavior of an actor in the specified [ActorContext]. + * + * This will activate the initial behavior and canonicalize the resulting behavior. + * + * @param ctx The [ActorContext] to start the behavior in. + * @param behavior The initial behavior to start. + * @return The behavior that has been started. + */ + tailrec fun start(ctx: ActorContext, behavior: Behavior): Behavior = + when (behavior) { + is DeferredBehavior -> start(ctx, behavior(ctx)) + else -> behavior + } + + /** + * Given a possibly special behavior (same or unhandled) and a "current" behavior (which defines the meaning of + * encountering a `same` behavior) this method computes the next behavior, suitable for passing a message or + * signal. + * + * @param ctx The context in which the actor runs. + * @param current The actor's current behavior. + * @param next The actor's next behavior. + * @return The actor's canonicalized next behavior. + */ + tailrec fun canonicalize(ctx: ActorContext, current: Behavior, next: Behavior): Behavior = + when (next) { + is SameBehavior, current -> current + is UnhandledBehavior -> current + is DeferredBehavior -> canonicalize(ctx, current, next(ctx)) + else -> next + } + } +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/Coroutines.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/Coroutines.kt new file mode 100644 index 00000000..82b29715 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/Coroutines.kt @@ -0,0 +1,182 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim.internal + +import com.atlarge.odcsim.ActorContext +import com.atlarge.odcsim.ActorRef +import com.atlarge.odcsim.ActorSystem +import com.atlarge.odcsim.Behavior +import com.atlarge.odcsim.Duration +import com.atlarge.odcsim.Instant +import com.atlarge.odcsim.ReceivingBehavior +import com.atlarge.odcsim.Signal +import com.atlarge.odcsim.coroutines.SuspendingActorContext +import com.atlarge.odcsim.coroutines.SuspendingBehavior +import com.atlarge.odcsim.coroutines.suspendWithBehavior +import com.atlarge.odcsim.empty +import com.atlarge.odcsim.receiveMessage +import com.atlarge.odcsim.receiveSignal +import org.slf4j.Logger +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resume +import kotlin.coroutines.startCoroutine + +/** + * This interface exposes internal functionality provided by [SuspendingBehaviorImpl] on [SuspendingActorContext] to + * control the active behavior of the coroutine. + */ +interface SuspendingActorContextImpl : SuspendingActorContext { + /** + * The current active behavior + */ + val behavior: Behavior + + /** + * Replace the current active behavior with the specified new behavior. + * + * @param next The behavior to replace the current behavior with. + */ + fun become(next: Behavior) +} + +/** + * Implementation of [SuspendingBehavior] class that maps the suspending method calls to the [Behavior] + * interface. + * This implementation uses the fact that each actor is thread-safe (as it processes its mailbox sequentially). + */ +internal class SuspendingBehaviorImpl( + private var actorContext: ActorContext, + initialBehavior: SuspendingBehavior +) : ReceivingBehavior(), SuspendingActorContextImpl { + + /** + * The next behavior to use. + */ + private var next: Behavior = this + + /** + * The [BehaviorInterpreter] to wrap the suspending behavior. + */ + private val interpreter = BehaviorInterpreter(initialBehavior) + + override fun receive(ctx: ActorContext, msg: T): Behavior { + this.actorContext = ctx + return interpreter.also { it.interpretMessage(ctx, msg) }.propagate(next) + } + + override fun receiveSignal(ctx: ActorContext, signal: Signal): Behavior { + this.actorContext = ctx + return interpreter.also { it.interpretSignal(ctx, signal) }.propagate(next) + } + + override val self: ActorRef get() = actorContext.self + + override val time: Instant get() = actorContext.time + + override val children: List> + get() = actorContext.children + + override val system: ActorSystem<*> + get() = actorContext.system + + override val log: Logger + get() = actorContext.log + + override fun getChild(name: String): ActorRef<*>? = actorContext.getChild(name) + + override fun send(ref: ActorRef, msg: U, after: Duration) = actorContext.send(ref, msg, after) + + override fun spawn(behavior: Behavior, name: String) = actorContext.spawn(behavior, name) + + override fun spawnAnonymous(behavior: Behavior) = actorContext.spawnAnonymous(behavior) + + override fun stop(child: ActorRef<*>) = actorContext.stop(child) + + override fun watch(target: ActorRef<*>) = actorContext.watch(target) + + override fun unwatch(target: ActorRef<*>) = actorContext.unwatch(target) + + override fun sync(target: ActorRef<*>) = actorContext.sync(target) + + override fun unsync(target: ActorRef<*>) = actorContext.unsync(target) + + override fun isSync(target: ActorRef<*>): Boolean = actorContext.isSync(target) + + override suspend fun receive(): T = suspendWithBehavior { cont, next -> + receiveMessage { msg -> + cont.resume(msg) + next() + } + } + + override suspend fun receiveSignal(): Signal = suspendWithBehavior { cont, next -> + receiveSignal { _, signal -> + cont.resume(signal) + next() + } + } + + override val behavior: Behavior get() = interpreter.behavior + + override fun become(next: Behavior) { + interpreter.become(actorContext, next) + } + + override val key: CoroutineContext.Key<*> = SuspendingActorContext.Key + + /** + * Start the suspending behavior. + */ + internal fun start(): Behavior { + val behavior = interpreter.behavior as SuspendingBehavior + val block = suspend { behavior(this) } + interpreter.become(actorContext, empty()) + block.startCoroutine(SuspendingBehaviorImplContinuation()) + return next + } + + /** + * Stop the suspending behavior. + */ + private fun stop() { + this.interpreter.stop(actorContext) + } + + /** + * The continuation of suspending behavior. + */ + private inner class SuspendingBehaviorImplContinuation : Continuation> { + override val context = this@SuspendingBehaviorImpl + + override fun resumeWith(result: Result>) { + if (result.isSuccess) { + next = result.getOrNull()!! + } else if (result.isFailure) { + throw result.exceptionOrNull()!! + } + } + } +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/StashBufferImpl.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/StashBufferImpl.kt new file mode 100644 index 00000000..24c3a9d5 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/StashBufferImpl.kt @@ -0,0 +1,74 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim.internal + +import com.atlarge.odcsim.ActorContext +import com.atlarge.odcsim.Behavior +import com.atlarge.odcsim.StashBuffer +import java.util.ArrayDeque + +/** + * Internal implementation of the [StashBuffer] interface. + */ +internal class StashBufferImpl(private val capacity: Int) : StashBuffer { + /** + * The internal queue used to store the messages. + */ + private val queue = ArrayDeque(capacity) + + override val head: T + get() = queue.first + + override val isEmpty: Boolean + get() = queue.isEmpty() + + override val isFull: Boolean + get() = size > capacity + + override val size: Int + get() = queue.size + + override fun forEach(block: (T) -> Unit) { + queue.toList().forEach(block) + } + + override fun stash(msg: T) { + queue.add(msg) + } + + override fun unstashAll(ctx: ActorContext, behavior: Behavior): Behavior { + val messages = queue.toList() + queue.clear() + + val interpreter = BehaviorInterpreter(behavior) + interpreter.start(ctx) + + for (message in messages) { + interpreter.interpretMessage(ctx, message) + } + + return interpreter.behavior + } +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/TimerSchedulerImpl.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/TimerSchedulerImpl.kt new file mode 100644 index 00000000..22bec507 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/TimerSchedulerImpl.kt @@ -0,0 +1,122 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim.internal + +import com.atlarge.odcsim.ActorContext +import com.atlarge.odcsim.Duration +import com.atlarge.odcsim.Signal +import com.atlarge.odcsim.TimerScheduler + +/** + * Implementation of [TimerScheduler] that uses the actor's [ActorContext] to provide timer functionality. + * + * @property ctx The actor context to use. + */ +internal class TimerSchedulerImpl(private val ctx: ActorContext) : TimerScheduler { + private val timers = mutableMapOf>() + + override fun cancel(key: Any) { + val timer = timers[key] ?: return + ctx.log.debug("Cancel timer [{}] with generation [{}]", timer.key, timer.generation) + timers -= timer.key + } + + override fun cancelAll() { + ctx.log.debug("Cancel all timers") + timers.clear() + } + + override fun isTimerActive(key: Any): Boolean = timers.containsKey(key) + + override fun startPeriodicTimer(key: Any, msg: T, interval: Duration) { + startTimer(key, msg, interval, true) + } + + override fun startSingleTimer(key: Any, msg: T, delay: Duration) { + startTimer(key, msg, delay, false) + } + + override fun every(key: Any, interval: Duration, block: () -> Unit) { + @Suppress("UNCHECKED_CAST") + startTimer(key, Block(block) as T, interval, true) + } + + override fun after(key: Any, delay: Duration, block: () -> Unit) { + @Suppress("UNCHECKED_CAST") + startTimer(key, Block(block) as T, delay, false) + } + + private fun startTimer(key: Any, msg: T, duration: Duration, repeat: Boolean) { + val timer = timers.getOrPut(key) { Timer(key) } + timer.duration = duration + timer.generation += 1 + timer.msg = msg + timer.repeat = repeat + ctx.sendSignal(ctx.self, TimerSignal(key, timer.generation), duration) + ctx.log.debug("Start timer [{}] with generation [{}]", key, timer.generation) + } + + fun interceptTimerSignal(signal: TimerSignal): T? { + val timer = timers[signal.key] + + if (timer == null) { + // Message was from canceled timer that was already enqueued + ctx.log.debug("Received timer [{}] that has been removed, discarding", signal.key) + return null + } else if (signal.generation != timer.generation) { + // Message was from an old timer that was enqueued before canceled + ctx.log.debug("Received timer [{}] from old generation [{}], expected generation [{}], discarding", + signal.key, signal.generation, timer.generation) + } + + if (!timer.repeat) { + timers -= timer.key + } else { + ctx.sendSignal(ctx.self, signal, timer.duration) + } + + val msg = timer.msg + + if (msg is Block) { + msg() + return null + } + + return msg + } + + data class Timer(val key: Any) { + var duration: Duration = 0.0 + var repeat: Boolean = false + var generation: Int = 0 + lateinit var msg: T + } + + data class TimerSignal(val key: Any, val generation: Int) : Signal + + data class Block(val block: () -> Unit) { + operator fun invoke() = block() + } +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LocationAwareLoggerImpl.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LocationAwareLoggerImpl.kt new file mode 100644 index 00000000..bf50b5e8 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LocationAwareLoggerImpl.kt @@ -0,0 +1,567 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim.internal.logging + +import com.atlarge.odcsim.ActorContext +import org.slf4j.Logger +import org.slf4j.Marker +import org.slf4j.helpers.MessageFormatter +import org.slf4j.spi.LocationAwareLogger + +/** + * An actor-specific [Logger] implementation that is aware of the calling location. + * + * @param ctx The owning [ActorContext] of this logger. + * @param delegate The [LocationAwareLogger] to delegate the messages to. + */ +internal class LocationAwareLoggerImpl( + ctx: ActorContext<*>, + private val delegate: LocationAwareLogger +) : LoggerImpl(ctx), Logger by delegate { + /** + * The fully qualified name of this class. + */ + private val fqcn = LocationAwareLoggerImpl::class.java.name + + override fun trace(format: String?, arg: Any?) { + if (!delegate.isTraceEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg).message + delegate.log(null, fqcn, LocationAwareLogger.TRACE_INT, formattedMessage, arrayOf(arg), null) + } + } + + override fun trace(format: String?, arg1: Any?, arg2: Any?) { + if (!delegate.isTraceEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg1, arg2).message + delegate.log(null, fqcn, LocationAwareLogger.TRACE_INT, formattedMessage, arrayOf(arg1, arg2), null) + } + } + + override fun trace(format: String?, argArray: Array) { + if (!delegate.isTraceEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.arrayFormat(format, argArray).message + delegate.log(null, fqcn, LocationAwareLogger.TRACE_INT, formattedMessage, argArray, null) + } + } + + override fun trace(msg: String?, t: Throwable?) { + if (!delegate.isTraceEnabled) { + return + } + + withMdc { + delegate.log(null, fqcn, LocationAwareLogger.TRACE_INT, msg, null, t) + } + } + + override fun trace(marker: Marker?, msg: String?) { + if (!delegate.isTraceEnabled) { + return + } + + withMdc { + delegate.log(marker, fqcn, LocationAwareLogger.TRACE_INT, msg, null, null) + } + } + + override fun trace(marker: Marker?, format: String?, arg: Any?) { + if (!delegate.isTraceEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg).message + delegate.log(marker, fqcn, LocationAwareLogger.TRACE_INT, formattedMessage, arrayOf(arg), null) + } + } + + override fun trace(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) { + if (!delegate.isTraceEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg1, arg2).message + delegate.log(marker, fqcn, LocationAwareLogger.TRACE_INT, formattedMessage, arrayOf(arg1, arg2), null) + } + } + + override fun trace(marker: Marker?, format: String?, argArray: Array) { + if (!delegate.isTraceEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.arrayFormat(format, argArray).message + delegate.log(marker, fqcn, LocationAwareLogger.TRACE_INT, formattedMessage, argArray, null) + } + } + + override fun trace(marker: Marker?, msg: String?, t: Throwable?) { + if (!delegate.isTraceEnabled) { + return + } + + withMdc { + delegate.log(marker, fqcn, LocationAwareLogger.TRACE_INT, msg, null, t) + } + } + + override fun debug(msg: String?) { + if (!delegate.isDebugEnabled) { + return + } + + withMdc { + delegate.log(null, fqcn, LocationAwareLogger.DEBUG_INT, msg, null, null) + } + } + + override fun debug(format: String?, arg: Any?) { + if (!delegate.isDebugEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg).message + delegate.log(null, fqcn, LocationAwareLogger.DEBUG_INT, formattedMessage, arrayOf(arg), null) + } + } + + override fun debug(format: String?, arg1: Any?, arg2: Any?) { + if (!delegate.isDebugEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg1, arg2).message + delegate.log(null, fqcn, LocationAwareLogger.DEBUG_INT, formattedMessage, arrayOf(arg1, arg2), null) + } + } + + override fun debug(format: String?, argArray: Array) { + if (!delegate.isDebugEnabled) { + return + } + + withMdc { + val ft = MessageFormatter.arrayFormat(format, argArray) + delegate.log(null, fqcn, LocationAwareLogger.DEBUG_INT, ft.message, ft.argArray, ft.throwable) + } + } + + override fun debug(msg: String?, t: Throwable?) { + if (!delegate.isDebugEnabled) { + return + } + + withMdc { + delegate.log(null, fqcn, LocationAwareLogger.DEBUG_INT, msg, null, t) + } + } + + override fun debug(marker: Marker?, msg: String?) { + if (!delegate.isDebugEnabled) { + return + } + + withMdc { + delegate.log(marker, fqcn, LocationAwareLogger.DEBUG_INT, msg, null, null) + } + } + + override fun debug(marker: Marker?, format: String?, arg: Any?) { + if (!delegate.isDebugEnabled) { + return + } + + withMdc { + val ft = MessageFormatter.format(format, arg) + delegate.log(marker, fqcn, LocationAwareLogger.DEBUG_INT, ft.message, ft.argArray, ft.throwable) + } + } + + override fun debug(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) { + if (!delegate.isDebugEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg1, arg2).message + delegate.log(marker, fqcn, LocationAwareLogger.DEBUG_INT, formattedMessage, arrayOf(arg1, arg2), null) + } + } + + override fun debug(marker: Marker?, format: String?, argArray: Array) { + if (!delegate.isDebugEnabled) { + return + } + + withMdc { + val ft = MessageFormatter.arrayFormat(format, argArray) + delegate.log(marker, fqcn, LocationAwareLogger.DEBUG_INT, ft.message, argArray, ft.throwable) + } + } + + override fun debug(marker: Marker?, msg: String?, t: Throwable?) { + if (!delegate.isDebugEnabled) { + return + } + + withMdc { + delegate.log(marker, fqcn, LocationAwareLogger.DEBUG_INT, msg, null, t) + } + } + + override fun info(msg: String?) { + if (!delegate.isInfoEnabled) { + return + } + + withMdc { + delegate.log(null, fqcn, LocationAwareLogger.INFO_INT, msg, null, null) + } + } + + override fun info(format: String?, arg: Any?) { + if (!delegate.isInfoEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg).message + delegate.log(null, fqcn, LocationAwareLogger.INFO_INT, formattedMessage, arrayOf(arg), null) + } + } + + override fun info(format: String?, arg1: Any?, arg2: Any?) { + if (!delegate.isInfoEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg1, arg2).message + delegate.log(null, fqcn, LocationAwareLogger.INFO_INT, formattedMessage, arrayOf(arg1, arg2), null) + } + } + + override fun info(format: String?, argArray: Array) { + if (!delegate.isInfoEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.arrayFormat(format, argArray).message + delegate.log(null, fqcn, LocationAwareLogger.INFO_INT, formattedMessage, argArray, null) + } + } + + override fun info(msg: String?, t: Throwable?) { + if (!delegate.isInfoEnabled) { + return + } + + withMdc { + delegate.log(null, fqcn, LocationAwareLogger.INFO_INT, msg, null, t) + } + } + + override fun info(marker: Marker?, msg: String?) { + if (!delegate.isInfoEnabled) { + return + } + + withMdc { + delegate.log(marker, fqcn, LocationAwareLogger.INFO_INT, msg, null, null) + } + } + + override fun info(marker: Marker?, format: String?, arg: Any?) { + if (!delegate.isInfoEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg).message + delegate.log(marker, fqcn, LocationAwareLogger.INFO_INT, formattedMessage, arrayOf(arg), null) + } + } + + override fun info(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) { + if (!delegate.isInfoEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg1, arg2).message + delegate.log(marker, fqcn, LocationAwareLogger.INFO_INT, formattedMessage, arrayOf(arg1, arg2), null) + } + } + + override fun info(marker: Marker?, format: String?, argArray: Array) { + if (!delegate.isInfoEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.arrayFormat(format, argArray).message + delegate.log(marker, fqcn, LocationAwareLogger.INFO_INT, formattedMessage, argArray, null) + } + } + + override fun info(marker: Marker?, msg: String?, t: Throwable?) { + if (!delegate.isInfoEnabled) { + return + } + + withMdc { + delegate.log(marker, fqcn, LocationAwareLogger.INFO_INT, msg, null, t) + } + } + + override fun warn(msg: String?) { + if (!delegate.isWarnEnabled) { + return + } + + withMdc { + delegate.log(null, fqcn, LocationAwareLogger.WARN_INT, msg, null, null) + } + } + + override fun warn(format: String?, arg: Any?) { + if (!delegate.isWarnEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg).message + delegate.log(null, fqcn, LocationAwareLogger.WARN_INT, formattedMessage, arrayOf(arg), null) + } + } + + override fun warn(format: String?, arg1: Any?, arg2: Any?) { + if (!delegate.isWarnEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg1, arg2).message + delegate.log(null, fqcn, LocationAwareLogger.WARN_INT, formattedMessage, arrayOf(arg1, arg2), null) + } + } + + override fun warn(format: String?, argArray: Array) { + if (!delegate.isWarnEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.arrayFormat(format, argArray).message + delegate.log(null, fqcn, LocationAwareLogger.WARN_INT, formattedMessage, argArray, null) + } + } + + override fun warn(msg: String?, t: Throwable?) { + if (!delegate.isWarnEnabled) { + return + } + + withMdc { + delegate.log(null, fqcn, LocationAwareLogger.WARN_INT, msg, null, t) + } + } + + override fun warn(marker: Marker?, msg: String?) { + if (!delegate.isWarnEnabled) { + return + } + + withMdc { + delegate.log(marker, fqcn, LocationAwareLogger.WARN_INT, msg, null, null) + } + } + + override fun warn(marker: Marker?, format: String?, arg: Any?) { + if (!delegate.isWarnEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg).message + delegate.log(marker, fqcn, LocationAwareLogger.WARN_INT, formattedMessage, arrayOf(arg), null) + } + } + + override fun warn(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) { + if (!delegate.isWarnEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg1, arg2).message + delegate.log(marker, fqcn, LocationAwareLogger.WARN_INT, formattedMessage, arrayOf(arg1, arg2), null) + } + } + + override fun warn(marker: Marker?, format: String?, argArray: Array) { + if (!delegate.isWarnEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.arrayFormat(format, argArray).message + delegate.log(marker, fqcn, LocationAwareLogger.WARN_INT, formattedMessage, argArray, null) + } + } + + override fun warn(marker: Marker?, msg: String?, t: Throwable?) { + if (!delegate.isWarnEnabled) { + return + } + + withMdc { + delegate.log(marker, fqcn, LocationAwareLogger.WARN_INT, msg, null, t) + } + } + + override fun error(msg: String?) { + if (!delegate.isErrorEnabled) { + return + } + + withMdc { + delegate.log(null, fqcn, LocationAwareLogger.ERROR_INT, msg, null, null) + } + } + + override fun error(format: String?, arg: Any?) { + if (!delegate.isErrorEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg).message + delegate.log(null, fqcn, LocationAwareLogger.ERROR_INT, formattedMessage, arrayOf(arg), null) + } + } + + override fun error(format: String?, arg1: Any?, arg2: Any?) { + if (!delegate.isErrorEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg1, arg2).message + delegate.log(null, fqcn, LocationAwareLogger.ERROR_INT, formattedMessage, arrayOf(arg1, arg2), null) + } + } + + override fun error(format: String?, argArray: Array) { + if (!delegate.isErrorEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.arrayFormat(format, argArray).message + delegate.log(null, fqcn, LocationAwareLogger.ERROR_INT, formattedMessage, argArray, null) + } + } + + override fun error(msg: String?, t: Throwable?) { + if (!delegate.isErrorEnabled) { + return + } + + withMdc { + delegate.log(null, fqcn, LocationAwareLogger.ERROR_INT, msg, null, t) + } + } + + override fun error(marker: Marker?, msg: String?) { + if (!delegate.isErrorEnabled) { + return + } + + withMdc { + delegate.log(marker, fqcn, LocationAwareLogger.ERROR_INT, msg, null, null) + } + } + + override fun error(marker: Marker?, format: String?, arg: Any?) { + if (!delegate.isErrorEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg).message + delegate.log(marker, fqcn, LocationAwareLogger.ERROR_INT, formattedMessage, arrayOf(arg), null) + } + } + + override fun error(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) { + if (!delegate.isErrorEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.format(format, arg1, arg2).message + delegate.log(marker, fqcn, LocationAwareLogger.ERROR_INT, formattedMessage, arrayOf(arg1, arg2), null) + } + } + + override fun error(marker: Marker?, format: String?, argArray: Array) { + if (!delegate.isErrorEnabled) { + return + } + + withMdc { + val formattedMessage = MessageFormatter.arrayFormat(format, argArray).message + delegate.log(marker, fqcn, LocationAwareLogger.ERROR_INT, formattedMessage, argArray, null) + } + } + + override fun error(marker: Marker?, msg: String?, t: Throwable?) { + if (!delegate.isErrorEnabled) { + return + } + + withMdc { + delegate.log(marker, fqcn, LocationAwareLogger.ERROR_INT, msg, null, t) + } + } +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LocationIgnorantLoggerImpl.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LocationIgnorantLoggerImpl.kt new file mode 100644 index 00000000..999e30e6 --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LocationIgnorantLoggerImpl.kt @@ -0,0 +1,440 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim.internal.logging + +import com.atlarge.odcsim.ActorContext +import org.slf4j.Logger +import org.slf4j.Marker + +/** + * A [Logger] implementation that is not aware of the calling location. + * + * @param ctx The owning [ActorContext] of this logger. + * @param delegate The [Logger] to delegate the messages to. + */ +internal class LocationIgnorantLoggerImpl( + ctx: ActorContext<*>, + private val delegate: Logger +) : LoggerImpl(ctx), Logger by delegate { + override fun warn(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) { + if (!isWarnEnabled) { + return + } + + withMdc { delegate.warn(marker, format, arg1, arg2) } + } + + override fun warn(format: String?, arg1: Any?, arg2: Any?) { + if (!isWarnEnabled) { + return + } + + withMdc { delegate.warn(format, arg1, arg2) } + } + + override fun warn(msg: String?) { + if (!isWarnEnabled) { + return + } + + withMdc { delegate.warn(msg) } + } + + override fun warn(marker: Marker?, format: String?, arg: Any?) { + if (!isWarnEnabled) { + return + } + + withMdc { delegate.warn(marker, format, arg) } + } + + override fun warn(marker: Marker?, format: String?, vararg arguments: Any?) { + if (!isWarnEnabled) { + return + } + + withMdc { delegate.warn(marker, format, arguments) } + } + + override fun warn(format: String?, arg: Any?) { + if (!isWarnEnabled) { + return + } + + withMdc { delegate.warn(format, arg) } + } + + override fun warn(marker: Marker?, msg: String?) { + if (!isWarnEnabled) { + return + } + + withMdc { delegate.warn(marker, msg) } + } + + override fun warn(msg: String?, t: Throwable?) { + if (!isWarnEnabled) { + return + } + + withMdc { delegate.warn(msg, t) } + } + + override fun warn(format: String?, vararg arguments: Any?) { + if (!isWarnEnabled) { + return + } + + withMdc { delegate.warn(format, *arguments) } + } + + override fun warn(marker: Marker?, msg: String?, t: Throwable?) { + if (!isWarnEnabled) { + return + } + + withMdc { delegate.warn(marker, msg, t) } + } + + override fun info(marker: Marker?, format: String?, vararg arguments: Any?) { + if (!isInfoEnabled) { + return + } + + withMdc { delegate.info(marker, format, *arguments) } + } + + override fun info(format: String?, arg: Any?) { + if (!isInfoEnabled) { + return + } + + withMdc { delegate.info(format, arg) } + } + + override fun info(marker: Marker?, msg: String?, t: Throwable?) { + if (!isInfoEnabled) { + return + } + + withMdc { delegate.info(marker, msg, t) } + } + + override fun info(msg: String?) { + if (!isInfoEnabled) { + return + } + + withMdc { delegate.info(msg) } + } + + override fun info(format: String?, vararg arguments: Any?) { + if (!isInfoEnabled) { + return + } + + withMdc { delegate.info(format, *arguments) } + } + + override fun info(format: String?, arg1: Any?, arg2: Any?) { + if (!isInfoEnabled) { + return + } + + withMdc { delegate.info(format, arg1, arg2) } + } + + override fun info(marker: Marker?, msg: String?) { + if (!isInfoEnabled) { + return + } + + withMdc { delegate.info(marker, msg) } + } + + override fun info(marker: Marker?, format: String?, arg: Any?) { + if (!isInfoEnabled) { + return + } + + withMdc { delegate.info(marker, format, arg) } + } + + override fun info(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) { + if (!isInfoEnabled) { + return + } + + withMdc { delegate.info(marker, format, arg1, arg2) } + } + + override fun info(msg: String?, t: Throwable?) { + if (!isInfoEnabled) { + return + } + + withMdc { delegate.info(msg, t) } + } + + override fun error(msg: String?) { + if (!isErrorEnabled) { + return + } + + withMdc { delegate.error(msg) } + } + + override fun error(marker: Marker?, msg: String?) { + if (!isErrorEnabled) { + return + } + + withMdc { delegate.error(marker, msg) } + } + + override fun error(format: String?, vararg arguments: Any?) { + if (!isErrorEnabled) { + return + } + + withMdc { delegate.error(format, *arguments) } + } + + override fun error(format: String?, arg: Any?) { + if (!isErrorEnabled) { + return + } + + withMdc { delegate.error(format, arg) } + } + + override fun error(msg: String?, t: Throwable?) { + if (!isErrorEnabled) { + return + } + + withMdc { delegate.error(msg, t) } + } + + override fun error(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) { + if (!isErrorEnabled) { + return + } + + withMdc { delegate.error(marker, format, arg1, arg2) } + } + + override fun error(marker: Marker?, format: String?, vararg arguments: Any?) { + if (!isErrorEnabled) { + return + } + + withMdc { delegate.error(marker, format, *arguments) } + } + + override fun error(marker: Marker?, msg: String?, t: Throwable?) { + if (!isErrorEnabled) { + return + } + + withMdc { delegate.error(marker, msg, t) } + } + + override fun error(format: String?, arg1: Any?, arg2: Any?) { + if (!isErrorEnabled) { + return + } + + withMdc { delegate.error(format, arg1, arg2) } + } + + override fun error(marker: Marker?, format: String?, arg: Any?) { + if (!isErrorEnabled) { + return + } + + withMdc { delegate.error(marker, format, arg) } + } + + override fun debug(format: String?, vararg arguments: Any?) { + if (!isDebugEnabled) { + return + } + + withMdc { delegate.debug(format, *arguments) } + } + + override fun debug(format: String?, arg1: Any?, arg2: Any?) { + if (!isDebugEnabled) { + return + } + + withMdc { delegate.debug(format, arg1, arg2) } + } + + override fun debug(msg: String?, t: Throwable?) { + if (!isDebugEnabled) { + return + } + + withMdc { delegate.debug(msg, t) } + } + + override fun debug(format: String?, arg: Any?) { + if (!isDebugEnabled) { + return + } + + withMdc { delegate.debug(format, arg) } + } + + override fun debug(marker: Marker?, msg: String?) { + if (!isDebugEnabled) { + return + } + + withMdc { delegate.debug(marker, msg) } + } + + override fun debug(msg: String?) { + if (!isDebugEnabled) { + return + } + + withMdc { delegate.debug(msg) } + } + + override fun debug(marker: Marker?, msg: String?, t: Throwable?) { + if (!isDebugEnabled) { + return + } + + withMdc { delegate.debug(marker, msg, t) } + } + + override fun debug(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) { + if (!isDebugEnabled) { + return + } + + withMdc { delegate.debug(marker, format, arg1, arg2) } + } + + override fun debug(marker: Marker?, format: String?, arg: Any?) { + if (!isDebugEnabled) { + return + } + + withMdc { delegate.debug(marker, format, arg) } + } + + override fun debug(marker: Marker?, format: String?, vararg arguments: Any?) { + if (!isDebugEnabled) { + return + } + + withMdc { delegate.debug(marker, format, *arguments) } + } + + override fun trace(format: String?, arg: Any?) { + if (!isTraceEnabled) { + return + } + + withMdc { delegate.trace(format, arg) } + } + + override fun trace(marker: Marker?, msg: String?) { + if (!isTraceEnabled) { + return + } + + withMdc { delegate.trace(marker, msg) } + } + + override fun trace(msg: String?) { + if (!isTraceEnabled) { + return + } + + withMdc { delegate.trace(msg) } + } + + override fun trace(msg: String?, t: Throwable?) { + if (!isTraceEnabled) { + return + } + + withMdc { delegate.trace(msg, t) } + } + + override fun trace(format: String?, arg1: Any?, arg2: Any?) { + if (!isTraceEnabled) { + return + } + + withMdc { delegate.trace(format, arg1, arg2) } + } + + override fun trace(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) { + if (!isTraceEnabled) { + return + } + + withMdc { delegate.trace(marker, format, arg1, arg2) } + } + + override fun trace(marker: Marker?, format: String?, arg: Any?) { + if (!isTraceEnabled) { + return + } + + withMdc { delegate.trace(marker, format, arg) } + } + + override fun trace(marker: Marker?, format: String?, vararg argArray: Any?) { + if (!isTraceEnabled) { + return + } + + withMdc { delegate.trace(marker, format, *argArray) } + } + + override fun trace(marker: Marker?, msg: String?, t: Throwable?) { + if (!isTraceEnabled) { + return + } + + withMdc { delegate.trace(marker, msg, t) } + } + + override fun trace(format: String?, vararg arguments: Any?) { + if (!isTraceEnabled) { + return + } + + withMdc { delegate.trace(format, *arguments) } + } +} diff --git a/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LoggerImpl.kt b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LoggerImpl.kt new file mode 100644 index 00000000..f971f08d --- /dev/null +++ b/odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LoggerImpl.kt @@ -0,0 +1,77 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim.internal.logging + +import com.atlarge.odcsim.ActorContext +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.slf4j.spi.LocationAwareLogger + +/** + * An actor-specific [Logger] implementation. + * + * @param ctx The owning [ActorContext] of this logger. + */ +abstract class LoggerImpl internal constructor(protected val ctx: ActorContext<*>) : Logger { + /** + * Configure [MDC] with actor-specific information. + */ + protected inline fun withMdc(block: () -> Unit) { + MDC.put(MDC_ACTOR_SYSTEM, ctx.system.name) + MDC.put(MDC_ACTOR_TIME, String.format("%.2f", ctx.time)) + MDC.put(MDC_ACTOR_REF, ctx.self.path.toString()) + try { + block() + } finally { + MDC.remove(MDC_ACTOR_SYSTEM) + MDC.remove(MDC_ACTOR_TIME) + MDC.remove(MDC_ACTOR_REF) + } + } + + /** + * Mapped Diagnostic Context (MDC) attribute names. + */ + companion object { + val MDC_ACTOR_SYSTEM = "actor.system" + val MDC_ACTOR_TIME = "actor.time" + val MDC_ACTOR_REF = "actor.ref" + + /** + * Create a [Logger] for the specified [ActorContext]. + * + * @param ctx The actor context to create the logger for. + */ + operator fun invoke(ctx: ActorContext<*>): Logger { + val logger = LoggerFactory.getLogger(ctx.javaClass) + return if (logger is LocationAwareLogger) { + LocationAwareLoggerImpl(ctx, logger) + } else { + LocationIgnorantLoggerImpl(ctx, logger) + } + } + } +} diff --git a/odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorPathTest.kt b/odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorPathTest.kt new file mode 100644 index 00000000..023d3efd --- /dev/null +++ b/odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorPathTest.kt @@ -0,0 +1,86 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +/** + * A test suite for the [ActorPath] class. + */ +@DisplayName("ActorPath") +class ActorPathTest { + /** + * Test whether an [ActorPath.Root] may only start with a slash. + */ + @Test + fun `root node may only start with a slash`() { + ActorPath.Root() // Assert slash at start + assertThrows { ActorPath.Root("abc/") } + } + + /** + * Test whether an [ActorPath.Child] disallows names with a slash. + */ + @Test + fun `child node should not allow name with a slash`() { + assertThrows { ActorPath.Child(ActorPath.Root(), "/") } + } + + /** + * Test whether a root node can have a custom name. + */ + @Test + fun `root node can have a custom name`() { + val name = "user" + assertEquals(name, ActorPath.Root(name).name) + } + + /** + * Test whether a child node can be created on a root. + */ + @Test + fun `child node can be created on a root`() { + val root = ActorPath.Root(name = "/user") + val child = root.child("child") + + assertEquals(root, child.parent) + assertEquals("child", child.name) + } + + /** + * Test whether a child node can be created on a child. + */ + @Test + fun `child node can be created on a child`() { + val root = ActorPath.Root(name = "/user").child("child") + val child = root.child("child") + + assertEquals(root, child.parent) + assertEquals("child", child.name) + } +} diff --git a/odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/BehaviorTest.kt b/odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/BehaviorTest.kt new file mode 100644 index 00000000..1eb4f3b9 --- /dev/null +++ b/odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/BehaviorTest.kt @@ -0,0 +1,77 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim + +import com.atlarge.odcsim.internal.BehaviorInterpreter +import com.nhaarman.mockitokotlin2.mock +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +/** + * Test suite for [Behavior] and [BehaviorInterpreter]. + */ +@DisplayName("Behavior") +class BehaviorTest { + /** + * Test whether we cannot start an actor with the [unhandled] behavior. + */ + @Test + fun `should not start with unhandled behavior`() { + val ctx = mock>() + val interpreter = BehaviorInterpreter(unhandled()) + assertThrows { interpreter.start(ctx) } + } + + /** + * Test whether we cannot start an actor with deferred unhandled behavior. + */ + @Test + fun `should not start with deferred unhandled behavior`() { + val ctx = mock>() + val interpreter = BehaviorInterpreter(setup { unhandled() }) + assertThrows { interpreter.start(ctx) } + } + + /** + * Test whether deferred behavior that returns [same] fails. + */ + @Test + fun `should not allow setup to return same`() { + val ctx = mock>() + val interpreter = BehaviorInterpreter(setup { same() }) + assertThrows { interpreter.start(ctx) } + } + + /** + * Test whether deferred behavior that returns [unhandled] fails. + */ + @Test + fun `should not allow setup to return unhandled`() { + val ctx = mock>() + val interpreter = BehaviorInterpreter(setup { unhandled() }) + assertThrows { interpreter.start(ctx) } + } +} diff --git a/odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/CoroutinesTest.kt b/odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/CoroutinesTest.kt new file mode 100644 index 00000000..af7619e6 --- /dev/null +++ b/odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/CoroutinesTest.kt @@ -0,0 +1,63 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim + +import com.atlarge.odcsim.coroutines.SuspendingBehavior +import com.atlarge.odcsim.coroutines.suspending +import com.atlarge.odcsim.internal.BehaviorInterpreter +import com.atlarge.odcsim.internal.EmptyBehavior +import com.nhaarman.mockitokotlin2.mock +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.coroutines.suspendCoroutine + +/** + * Test suite for [SuspendingBehavior] using Kotlin Coroutines. + */ +@DisplayName("Coroutines") +internal class CoroutinesTest { + + @Test + fun `should immediately return new behavior`() { + val ctx = mock>() + val behavior = suspending { empty() } + val interpreter = BehaviorInterpreter(behavior) + interpreter.start(ctx) + assertTrue(interpreter.behavior as Behavior<*> is EmptyBehavior) + } + + @Test + fun `should be able to invoke regular suspend methods`() { + val ctx = mock>() + val behavior = suspending { + suspendCoroutine {} + stopped() + } + val interpreter = BehaviorInterpreter(behavior) + interpreter.start(ctx) + interpreter.interpretMessage(ctx, Unit) + } +} diff --git a/odcsim/odcsim-engine-omega/build.gradle.kts b/odcsim/odcsim-engine-omega/build.gradle.kts new file mode 100644 index 00000000..d4383303 --- /dev/null +++ b/odcsim/odcsim-engine-omega/build.gradle.kts @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) 2018 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. + */ + +/* Build configuration */ +plugins { + `kotlin-library-convention` +} + +/* Project configuration */ +repositories { + jcenter() +} + +dependencies { + api(project(":odcsim:odcsim-core")) + + implementation(kotlin("stdlib")) + implementation("org.jetbrains:annotations:17.0.0") + + testImplementation(project(":odcsim:odcsim-engine-tests")) + testImplementation("org.junit.jupiter:junit-jupiter-api:${Library.JUNIT_JUPITER}") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${Library.JUNIT_JUPITER}") + testImplementation("org.junit.platform:junit-platform-launcher:${Library.JUNIT_PLATFORM}") + testRuntimeOnly("org.slf4j:slf4j-simple:${Library.SLF4J}") +} diff --git a/odcsim/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystem.kt b/odcsim/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystem.kt new file mode 100644 index 00000000..dd92f90a --- /dev/null +++ b/odcsim/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystem.kt @@ -0,0 +1,360 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim.engine.omega + +import com.atlarge.odcsim.ActorContext +import com.atlarge.odcsim.ActorPath +import com.atlarge.odcsim.ActorRef +import com.atlarge.odcsim.ActorSystem +import com.atlarge.odcsim.Behavior +import com.atlarge.odcsim.Duration +import com.atlarge.odcsim.Envelope +import com.atlarge.odcsim.Instant +import com.atlarge.odcsim.PostStop +import com.atlarge.odcsim.PreStart +import com.atlarge.odcsim.Signal +import com.atlarge.odcsim.Terminated +import com.atlarge.odcsim.empty +import com.atlarge.odcsim.internal.BehaviorInterpreter +import com.atlarge.odcsim.internal.logging.LoggerImpl +import org.jetbrains.annotations.Async +import org.slf4j.Logger +import java.util.Collections +import java.util.PriorityQueue +import java.util.UUID +import java.util.WeakHashMap +import kotlin.math.max + +/** + * The reference implementation of the [ActorSystem] instance for the OpenDC simulation core. + * + * This engine implementation is a single-threaded implementation, running actors synchronously and + * provides a single priority queue for all events (messages, ticks, etc) that occur. + * + * @param guardianBehavior The behavior of the guardian (root) actor. + * @param name The name of the engine instance. + */ +class OmegaActorSystem(guardianBehavior: Behavior, override val name: String) : ActorSystem, ActorRef { + /** + * The state of the actor system. + */ + private var state: ActorSystemState = ActorSystemState.CREATED + + /** + * The event queue to process + */ + private val queue: PriorityQueue = PriorityQueue( + Comparator + .comparingDouble(EnvelopeImpl::time) + .thenComparingLong(EnvelopeImpl::id) + ) + + /** + * The registry of actors in the system. + */ + private val registry: MutableMap> = HashMap() + + /** + * The root actor path of the system. + */ + private val root: ActorPath = ActorPath.Root() + + /** + * The system actor path. + */ + private val system: ActorPath = root / "system" + + /** + * The current point in simulation time. + */ + override var time: Instant = .0 + + /** + * The path to the root actor. + */ + override val path: ActorPath = root / "user" + + init { + registry[system] = Actor(ActorRefImpl(this, system), empty()) + registry[path] = Actor(this, guardianBehavior) + schedule(path, PreStart, .0) + } + + override fun run(until: Duration) { + require(until >= .0) { "The given instant must be a non-negative number" } + + // Start the system/guardian actor on initial run + if (state == ActorSystemState.CREATED) { + state = ActorSystemState.STARTED + registry[system]!!.isolate { it.start() } + registry[path]!!.isolate { it.start() } + } else if (state == ActorSystemState.TERMINATED) { + throw IllegalStateException("The ActorSystem has been terminated.") + } + + while (time < until) { + // Check whether the system was interrupted + if (Thread.interrupted()) { + throw InterruptedException() + } + + val envelope = queue.peek() ?: break + val delivery = envelope.time.takeUnless { it > until } ?: break + + // A message should never be delivered out of order in this single-threaded implementation. Assert for + // sanity + assert(delivery >= time) { "Message delivered out of order [expected=$delivery, actual=$time]" } + + time = delivery + queue.poll() + + processEnvelope(envelope) + } + + // Jump forward in time as the caller expects the system to have run until the specified instant + // Taking the maximum value prevents the caller to jump backwards in time + time = max(time, until) + } + + override fun send(msg: T, after: Duration) = schedule(path, msg, after) + + override fun terminate() { + registry[path]?.stop(null) + registry[system]?.stop(null) + } + + override suspend fun spawnSystem(behavior: Behavior, name: String): ActorRef { + return registry[system]!!.spawn(behavior, name) + } + + override fun compareTo(other: ActorRef<*>): Int = path.compareTo(other.path) + + /** + * The identifier for the next message to be scheduled. + */ + private var nextId: Long = 0 + + /** + * Schedule a message to be processed by the engine. + * + * @param path The path to the destination of the message. + * @param message The message to schedule. + * @param delay The time to wait before processing the message. + */ + private fun schedule(@Async.Schedule path: ActorPath, message: Any, delay: Duration) { + require(delay >= .0) { "The given delay must be a non-negative number" } + scheduleEnvelope(EnvelopeImpl(nextId++, path, time + delay, message)) + } + + /** + * Schedule the specified envelope to be processed by the engine. + */ + private fun scheduleEnvelope(@Async.Schedule envelope: EnvelopeImpl) { + queue.add(envelope) + } + + /** + * Process the delivery of a message. + */ + private fun processEnvelope(@Async.Execute envelope: EnvelopeImpl) { + val actor = registry[envelope.destination] ?: return + + // Notice that messages for unknown/terminated actors are ignored for now + actor.isolate { it.interpretMessage(envelope.message) } + } + + /** + * An actor as represented in the Omega engine. + * + * @param self The [ActorRef] to this actor. + * @param initialBehavior The initial behavior of this actor. + */ + private inner class Actor(override val self: ActorRef, initialBehavior: Behavior) : ActorContext { + val childActors: MutableMap> = mutableMapOf() + val interpreter = BehaviorInterpreter(initialBehavior) + val watchers: MutableSet = Collections.newSetFromMap(WeakHashMap()) + + override val time: Instant + get() = this@OmegaActorSystem.time + + override val children: List> + get() = childActors.values.map { it.self } + + override val system: ActorSystem<*> + get() = this@OmegaActorSystem + + override val log: Logger by lazy(LazyThreadSafetyMode.NONE) { LoggerImpl(this) } + + override fun getChild(name: String): ActorRef<*>? = childActors[name]?.self + + override fun send(ref: ActorRef, msg: U, after: Duration) = schedule(ref.path, msg, after) + + override fun spawn(behavior: Behavior, name: String): ActorRef { + require(name.isNotEmpty()) { "Actor name may not be empty" } + require(!name.startsWith("$")) { "Actor name may not start with $-sign" } + return internalSpawn(behavior, name) + } + + override fun spawnAnonymous(behavior: Behavior): ActorRef { + val name = "$" + UUID.randomUUID() + return internalSpawn(behavior, name) + } + + private fun internalSpawn(behavior: Behavior, name: String): ActorRef { + require(name !in childActors) { "Actor name $name not unique" } + val ref = ActorRefImpl(this@OmegaActorSystem, self.path.child(name)) + val actor = Actor(ref, behavior) + registry[ref.path] = actor + childActors[name] = actor + schedule(ref.path, PreStart, .0) + actor.start() + return ref + } + + override fun stop(child: ActorRef<*>) { + when { + // Must be a direct child of this actor + child.path.parent == self.path -> { + val ref = childActors[child.path.name] ?: return + ref.stop(null) + } + self == child -> throw IllegalArgumentException( + "Only direct children of an actor may be stopped through the actor context, " + + "but you tried to stop [$self] by passing its ActorRef to the `stop` method. " + + "Stopping self has to be expressed as explicitly returning a Stop Behavior." + ) + else -> throw IllegalArgumentException( + "Only direct children of an actor may be stopped through the actor context, " + + "but [$child] is not a child of [$self]. Stopping other actors has to be expressed as " + + "an explicit stop message that the actor accepts." + ) + } + } + + override fun watch(target: ActorRef<*>) { + registry[target.path]?.watchers?.add(path) + } + + override fun unwatch(target: ActorRef<*>) { + registry[target.path]?.watchers?.remove(path) + } + + // Synchronization of actors in a single-threaded simulation is trivial: all actors are consistent in virtual + // time. + override fun sync(target: ActorRef<*>) {} + + override fun unsync(target: ActorRef<*>) {} + + override fun isSync(target: ActorRef<*>): Boolean = true + + /** + * Start this actor. + */ + fun start() { + interpreter.start(this) + } + + /** + * Stop this actor. + */ + fun stop(failure: Throwable?) { + interpreter.stop(this) + childActors.values.forEach { it.stop(failure) } + registry.remove(self.path) + interpreter.interpretSignal(this, PostStop) + val termination = Terminated(self, failure) + watchers.forEach { schedule(it, termination, 0.0) } + } + + /** + * Interpret the given message send to an actor. + */ + fun interpretMessage(msg: Any) { + if (msg is Signal) { + interpreter.interpretSignal(this, msg) + } else { + @Suppress("UNCHECKED_CAST") + interpreter.interpretMessage(this, msg as T) + } + + if (!interpreter.isAlive) { + stop(null) + } + } + + override fun equals(other: Any?): Boolean = + other is OmegaActorSystem<*>.Actor<*> && self.path == other.self.path + + override fun hashCode(): Int = self.path.hashCode() + } + + /** + * Isolate uncaught exceptions originating from actor interpreter invocations. + */ + private inline fun Actor.isolate(block: (Actor) -> U): U? { + return try { + block(this) + } catch (t: Throwable) { + // Forcefully stop the actor if it crashed + stop(t) + log.error("Unhandled exception in actor $path", t) + null + } + } + + /** + * Enumeration to track the state of the actor system. + */ + private enum class ActorSystemState { + CREATED, STARTED, TERMINATED + } + + /** + * Internal [ActorRef] implementation for this actor system. + */ + private data class ActorRefImpl( + private val owner: OmegaActorSystem<*>, + override val path: ActorPath + ) : ActorRef { + override fun toString(): String = "Actor[$path]" + + override fun compareTo(other: ActorRef<*>): Int = path.compareTo(other.path) + } + + /** + * A wrapper around a message that has been scheduled for processing. + * + * @property id The identifier of the message to keep the priority queue stable. + * @property destination The destination of the message. + * @property time The point in time to deliver the message. + * @property message The message to wrap. + */ + private class EnvelopeImpl( + val id: Long, + val destination: ActorPath, + override val time: Instant, + override val message: Any + ) : Envelope +} diff --git a/odcsim/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactory.kt b/odcsim/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactory.kt new file mode 100644 index 00000000..84bf1efb --- /dev/null +++ b/odcsim/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactory.kt @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim.engine.omega + +import com.atlarge.odcsim.ActorSystem +import com.atlarge.odcsim.ActorSystemFactory +import com.atlarge.odcsim.Behavior +import java.util.ServiceLoader + +/** + * An [ActorSystemFactory] for the Omega engine, used by the [ServiceLoader] API to create [OmegaActorSystem] instances. + */ +class OmegaActorSystemFactory : ActorSystemFactory { + override operator fun invoke(root: Behavior, name: String): ActorSystem = + OmegaActorSystem(root, name) +} diff --git a/odcsim/odcsim-engine-omega/src/main/resources/META-INF/services/com.atlarge.odcsim.ActorSystemFactory b/odcsim/odcsim-engine-omega/src/main/resources/META-INF/services/com.atlarge.odcsim.ActorSystemFactory new file mode 100644 index 00000000..d0ca8859 --- /dev/null +++ b/odcsim/odcsim-engine-omega/src/main/resources/META-INF/services/com.atlarge.odcsim.ActorSystemFactory @@ -0,0 +1 @@ +com.atlarge.odcsim.engine.omega.OmegaActorSystemFactory diff --git a/odcsim/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactoryTest.kt b/odcsim/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactoryTest.kt new file mode 100644 index 00000000..4e195e6e --- /dev/null +++ b/odcsim/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactoryTest.kt @@ -0,0 +1,37 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim.engine.omega + +import com.atlarge.odcsim.ActorSystemFactory +import com.atlarge.odcsim.engine.tests.ActorSystemFactoryContract +import org.junit.jupiter.api.DisplayName + +/** + * The [ActorSystemFactory] test suite for the Omega engine implementation. + */ +@DisplayName("OmegaActorSystemFactory") +class OmegaActorSystemFactoryTest : ActorSystemFactoryContract() { + override fun createFactory(): ActorSystemFactory = OmegaActorSystemFactory() +} diff --git a/odcsim/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemTest.kt b/odcsim/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemTest.kt new file mode 100644 index 00000000..dc310d47 --- /dev/null +++ b/odcsim/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemTest.kt @@ -0,0 +1,37 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim.engine.omega + +import com.atlarge.odcsim.ActorSystem +import com.atlarge.odcsim.engine.tests.ActorSystemContract +import org.junit.jupiter.api.DisplayName + +/** + * The [ActorSystem] test suite for the Omega engine implementation. + */ +@DisplayName("OmegaActorSystem") +class OmegaActorSystemTest : ActorSystemContract() { + override val factory = OmegaActorSystemFactory() +} diff --git a/odcsim/odcsim-engine-tests/build.gradle.kts b/odcsim/odcsim-engine-tests/build.gradle.kts new file mode 100644 index 00000000..7b236300 --- /dev/null +++ b/odcsim/odcsim-engine-tests/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * MIT License + * + * Copyright (c) 2019 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. + */ + +/* Build configuration */ +plugins { + `kotlin-library-convention` +} + +/* Project configuration */ +repositories { + jcenter() +} + +dependencies { + api(project(":odcsim:odcsim-core")) + + implementation(kotlin("stdlib")) + implementation("org.junit.jupiter:junit-jupiter-api:${Library.JUNIT_JUPITER}") +} diff --git a/odcsim/odcsim-engine-tests/src/main/kotlin/com/atlarge/odcsim/engine/tests/ActorSystemContract.kt b/odcsim/odcsim-engine-tests/src/main/kotlin/com/atlarge/odcsim/engine/tests/ActorSystemContract.kt new file mode 100644 index 00000000..593f587b --- /dev/null +++ b/odcsim/odcsim-engine-tests/src/main/kotlin/com/atlarge/odcsim/engine/tests/ActorSystemContract.kt @@ -0,0 +1,403 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim.engine.tests + +import com.atlarge.odcsim.ActorPath +import com.atlarge.odcsim.ActorRef +import com.atlarge.odcsim.ActorSystemFactory +import com.atlarge.odcsim.Behavior +import com.atlarge.odcsim.Terminated +import com.atlarge.odcsim.coroutines.dsl.timeout +import com.atlarge.odcsim.coroutines.suspending +import com.atlarge.odcsim.empty +import com.atlarge.odcsim.ignore +import com.atlarge.odcsim.receiveMessage +import com.atlarge.odcsim.receiveSignal +import com.atlarge.odcsim.same +import com.atlarge.odcsim.setup +import com.atlarge.odcsim.stopped +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +/** + * A conformance test suite for implementors of the [ActorSystem] interface. + */ +abstract class ActorSystemContract { + /** + * An [ActorSystemFactory] provided by implementors to create the [ActorSystem] to be tested. + */ + abstract val factory: ActorSystemFactory + + /** + * Test whether the created [ActorSystem] has the correct name. + */ + @Test + fun `should have a name`() { + val name = "test" + val system = factory(empty(), name) + + assertEquals(name, system.name) + system.terminate() + } + + /** + * Test whether the created [ActorSystem] has a path. + */ + @Test + fun `should have a path`() { + val system = factory(empty(), "test") + + assertTrue(system.path is ActorPath.Root) + system.terminate() + } + + /** + * Test whether creating an [ActorSystem] sets the initial time at 0. + */ + @Test + fun `should start at t=0`() { + val system = factory(empty(), name = "test") + + assertEquals(.0, system.time, DELTA) + system.terminate() + } + + /** + * Test whether an [ActorSystem] does not accept invalid points in time. + */ + @Test + fun `should not accept negative instants for running`() { + val system = factory(empty(), name = "test") + assertThrows { system.run(-10.0) } + system.terminate() + } + + /** + * Test whether an [ActorSystem] will not jump backward in time when asking to run until a specified instant + * that has already occurred. + */ + @Test + fun `should not jump backward in time`() { + val until = 10.0 + val system = factory(empty(), name = "test") + + system.run(until = until) + system.run(until = until - 0.5) + assertEquals(until, system.time, DELTA) + system.terminate() + } + + /** + * Test whether an [ActorSystem] will jump forward in time when asking to run until a specified instant. + */ + @Test + fun `should jump forward in time`() { + val until = 10.0 + val system = factory(empty(), name = "test") + + system.run(until = until) + assertEquals(until, system.time, DELTA) + system.terminate() + } + + /** + * Test whether an [ActorSystem] will jump forward in time when asking to run until a specified instant. + */ + @Test + fun `should order messages at the instant by insertion time`() { + val behavior = receiveMessage { msg -> + assertEquals(1, msg) + receiveMessage { + assertEquals(2, it) + ignore() + } + } + val system = factory(behavior, name = "test") + system.send(1, after = 1.0) + system.send(2, after = 1.0) + system.run(until = 10.0) + system.terminate() + } + + /** + * Test whether an [ActorSystem] will not process messages in the queue after the deadline. + */ + @Test + fun `should not process messages after deadline`() { + var counter = 0 + val behavior = receiveMessage { _ -> + counter++ + same() + } + val system = factory(behavior, name = "test") + system.send(Unit, after = 3.0) + system.send(Unit, after = 1.0) + system.run(until = 2.0) + assertEquals(1, counter) + system.terminate() + } + + /** + * Test whether an [ActorSystem] will not initialize the root actor if the system has not been run yet. + */ + @Test + fun `should not initialize root actor if not run`() { + val system = factory(setup { TODO() }, name = "test") + system.terminate() + } + + @Nested + @DisplayName("ActorRef") + inner class ActorRefTest { + /** + * Test whether an [ActorSystem] disallows sending messages in the past. + */ + @Test + fun `should disallow messages in the past`() { + val system = factory(empty(), name = "test") + assertThrows { system.send(Unit, after = -1.0) } + system.terminate() + } + } + + @Nested + @DisplayName("Actor") + inner class Actor { + /** + * Test whether the pre-start time of the root actor is at 0. + */ + @Test + fun `should pre-start at t=0 if root`() { + val behavior = setup { ctx -> + assertEquals(.0, ctx.time, DELTA) + ignore() + } + + val system = factory(behavior, "test") + system.run() + system.terminate() + } + + /** + * Test whether a child actor can be created from an actor. + */ + @Test + fun `should allow spawning of child actors`() { + var spawned = false + val behavior = setup { spawned = true; empty() } + + val system = factory(setup { ctx -> + val ref = ctx.spawn(behavior, "child") + assertEquals("child", ref.path.name) + ignore() + }, name = "test") + + system.run(until = 10.0) + assertTrue(spawned) + system.terminate() + } + + /** + * Test whether a child actor can be stopped from an actor. + */ + @Test + fun `should allow stopping of child actors`() { + val system = factory(setup { ctx -> + val ref = ctx.spawn(receiveMessage { throw UnsupportedOperationException() }, "child") + ctx.stop(ref) + assertEquals("child", ref.path.name) + ignore() + }, name = "test") + + system.run(until = 10.0) + system.terminate() + } + + /** + * Test whether only the parent of a child can terminate it. + */ + @Test + fun `should only be able to terminate child actors`() { + val system = factory(setup { ctx1 -> + val child1 = ctx1.spawn(ignore(), "child-1") + ctx1.spawn(setup { ctx2 -> + ctx2.stop(child1) + ignore() + }, "child-2") + + ignore() + }, name = "test") + system.run() + system.terminate() + } + + /** + * Test whether stopping a child is idempotent. + */ + @Test + fun `should be able to stop a child twice`() { + val system = factory(setup { ctx -> + val child = ctx.spawn(ignore(), "child") + ctx.stop(child) + ctx.stop(child) + ignore() + }, name = "test") + system.run() + system.terminate() + } + + /** + * Test whether termination of a child also results in termination of its children. + */ + @Test + fun `should terminate children of child when terminating it`() { + val system = factory(setup> { ctx -> + val root = ctx.self + val child = ctx.spawn(setup { + val child = it.spawn(receiveMessage { + throw IllegalStateException("DELIBERATE") + }, "child") + ctx.send(root, child) + ignore() + }, "child") + + receiveMessage { msg -> + ctx.stop(child) + ctx.send(msg, Unit) // This actor should be stopped now and not receive the message anymore + stopped() + } + }, name = "test") + + system.run() + system.terminate() + } + + /** + * Test whether [same] works correctly. + */ + @Test + fun `should keep same behavior on same`() { + var counter = 0 + + val behavior = setup { ctx -> + counter++ + ctx.send(ctx.self, Unit) + receiveMessage { + counter++ + same() + } + } + + val system = factory(behavior, "test") + system.run() + assertEquals(2, counter) + system.terminate() + } + + /** + * Test whether the reference to the actor itself is valid. + */ + @Test + fun `should have reference to itself`() { + var flag = false + val behavior: Behavior = setup { ctx -> + ctx.send(ctx.self, Unit) + receiveMessage { flag = true; same() } + } + + val system = factory(behavior, "test") + system.run() + assertTrue(flag) + system.terminate() + } + + /** + * Test whether we can start an actor with the [stopped] behavior. + */ + @Test + fun `should start with stopped behavior`() { + val system = factory(stopped(), "test") + system.run() + system.terminate() + } + + /** + * Test whether an actor that is crashed cannot receive more messages. + */ + @Test + fun `should stop if it crashes`() { + var counter = 0 + val system = factory(receiveMessage { + counter++ + throw IllegalArgumentException("STAGED") + }, "test") + + system.send(Unit) + system.send(Unit) + + system.run() + assertEquals(1, counter) + system.terminate() + } + + /** + * Test whether an actor can watch for termination. + */ + @Test + fun `should watch for termination`() { + var received = false + val system = factory(setup { ctx -> + val child = ctx.spawn(suspending { + timeout(50.0) + stopped() + }, "child") + ctx.watch(child) + + receiveSignal { _, signal -> + when (signal) { + is Terminated -> { + received = true + stopped() + } + else -> + same() + } + } + }, "test") + + system.run() + system.terminate() + assertTrue(received) + } + } + + companion object { + private const val DELTA: Double = 0.0001 + } +} diff --git a/odcsim/odcsim-engine-tests/src/main/kotlin/com/atlarge/odcsim/engine/tests/ActorSystemFactoryContract.kt b/odcsim/odcsim-engine-tests/src/main/kotlin/com/atlarge/odcsim/engine/tests/ActorSystemFactoryContract.kt new file mode 100644 index 00000000..565f4f4c --- /dev/null +++ b/odcsim/odcsim-engine-tests/src/main/kotlin/com/atlarge/odcsim/engine/tests/ActorSystemFactoryContract.kt @@ -0,0 +1,73 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.atlarge.odcsim.engine.tests + +import com.atlarge.odcsim.ActorSystemFactory +import com.atlarge.odcsim.empty +import com.atlarge.odcsim.setup +import com.atlarge.odcsim.stopped +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +/** + * A conformance test suite for implementors of the [ActorSystemFactory] interface. + */ +abstract class ActorSystemFactoryContract { + /** + * Create an [ActorSystemFactory] instance to test. + */ + abstract fun createFactory(): ActorSystemFactory + + /** + * Test whether the factory will create an [ActorSystem] with correct name. + */ + @Test + fun `should create a system with correct name`() { + val factory = createFactory() + val name = "test" + val system = factory(empty(), name) + + assertEquals(name, system.name) + system.terminate() + } + + /** + * Test whether the factory will create an [ActorSystem] with valid root behavior. + */ + @Test + fun `should create a system with correct root behavior`() { + var flag = false + val factory = createFactory() + val system = factory(setup { + flag = true + stopped() + }, "test") + + system.run(until = 10.0) + system.terminate() + assertTrue(flag) + } +} -- cgit v1.2.3