summaryrefslogtreecommitdiff
path: root/odcsim
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2019-11-20 17:51:58 +0100
committerFabian Mastenbroek <mail.fabianm@gmail.com>2019-11-20 21:49:20 +0100
commit6ca0ae07669d20a5a34ef697610df90754024035 (patch)
tree3c26a21970fa5693b18edb34e8203d711c381ba5 /odcsim
parent4cc3c6dea5c5536d47fcbaf8414d74de7b6fdc4b (diff)
refactor: Move build logic to buildSrc
Diffstat (limited to 'odcsim')
-rw-r--r--odcsim/README.md100
-rw-r--r--odcsim/build.gradle.kts23
-rw-r--r--odcsim/odcsim-core/build.gradle.kts38
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorContext.kt175
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorPath.kt142
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorRef.kt50
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorSystem.kt76
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorSystemFactory.kt38
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behavior.kt193
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behaviors.kt223
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Envelope.kt57
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Signals.kt60
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/StashBuffer.kt88
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Time.kt75
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/TimerScheduler.kt92
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/Behavior.kt116
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/dsl/Receive.kt66
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/coroutines/dsl/Timeout.kt42
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/ActorContext.kt43
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/Behavior.kt48
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/BehaviorInterpreter.kt201
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/Coroutines.kt182
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/StashBufferImpl.kt74
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/TimerSchedulerImpl.kt122
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LocationAwareLoggerImpl.kt567
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LocationIgnorantLoggerImpl.kt440
-rw-r--r--odcsim/odcsim-core/src/main/kotlin/com/atlarge/odcsim/internal/logging/LoggerImpl.kt77
-rw-r--r--odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorPathTest.kt86
-rw-r--r--odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/BehaviorTest.kt77
-rw-r--r--odcsim/odcsim-core/src/test/kotlin/com/atlarge/odcsim/CoroutinesTest.kt63
-rw-r--r--odcsim/odcsim-engine-omega/build.gradle.kts46
-rw-r--r--odcsim/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystem.kt360
-rw-r--r--odcsim/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactory.kt38
-rw-r--r--odcsim/odcsim-engine-omega/src/main/resources/META-INF/services/com.atlarge.odcsim.ActorSystemFactory1
-rw-r--r--odcsim/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactoryTest.kt37
-rw-r--r--odcsim/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemTest.kt37
-rw-r--r--odcsim/odcsim-engine-tests/build.gradle.kts40
-rw-r--r--odcsim/odcsim-engine-tests/src/main/kotlin/com/atlarge/odcsim/engine/tests/ActorSystemContract.kt403
-rw-r--r--odcsim/odcsim-engine-tests/src/main/kotlin/com/atlarge/odcsim/engine/tests/ActorSystemFactoryContract.kt73
39 files changed, 4669 insertions, 0 deletions
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 @@
+<h1 align="center">
+ <a href="http://opendc.org/">
+ <img src="../misc/artwork/logo.png" width="100" alt="OpenDC">
+ </a>
+ <br>
+ odcsim
+</h1>
+
+## 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
+<dependency>
+ <groupId>com.atlarge.odcsim</groupId>
+ <artifactId>odcsim-core</artifactId>
+ <version>2.0.0</version>
+</dependency>
+
+<dependency>
+ <groupId>com.atlarge.odcsim</groupId>
+ <artifactId>odcsim-engine-omega</artifactId>
+ <version>2.0.0</version>
+</dependency>
+```
+
+### 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<Nothing> =
+ 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<T : Any> {
+ /**
+ * The identity of the actor, bound to the lifecycle of this actor instance.
+ */
+ val self: ActorRef<T>
+
+ /**
+ * A view of the children of this actor.
+ */
+ val children: List<ActorRef<*>>
+
+ /**
+ * 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 <U : Any> send(ref: ActorRef<U>, 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 <U : Any> spawn(behavior: Behavior<U>, name: String): ActorRef<U>
+
+ /**
+ * 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 <U : Any> spawnAnonymous(behavior: Behavior<U>): ActorRef<U>
+
+ /**
+ * 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 <U : Any, T : U> ActorContext<T>.unsafeCast(): ActorContext<U> {
+ @Suppress("UNCHECKED_CAST")
+ return this as ActorContext<U>
+}
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<ActorPath>, 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<String>): 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<in T : Any> : Comparable<ActorRef<*>>, 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 <U : Any, T : U> ActorRef<T>.unsafeCast(): ActorRef<U> {
+ @Suppress("UNCHECKED_CAST")
+ return this as ActorRef<U>
+}
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<in T : Any> : ActorRef<T> {
+ /**
+ * 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 <U : Any> spawnSystem(behavior: Behavior<U>, name: String): ActorRef<U>
+}
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 <T : Any> invoke(root: Behavior<T>, name: String): ActorSystem<T>
+}
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<T : Any> {
+ /**
+ * 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 <U : T> narrow(): Behavior<U> = 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 <U : Any> widen(transform: (U) -> T?): Behavior<U> {
+ return wrap(this) { interpreter ->
+ receive<U> { ctx, msg ->
+ val res = transform(msg)
+ @Suppress("UNCHECKED_CAST")
+ if (res == null || interpreter.interpretMessage(ctx as ActorContext<T>, 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<T>): Behavior<T> =
+ wrap(this) { left ->
+ wrap(that) { right ->
+ object : ReceivingBehavior<T>() {
+ override fun receive(ctx: ActorContext<T>, msg: T): Behavior<T> {
+ if (left.interpretMessage(ctx, msg)) {
+ return left.behavior
+ } else if (right.interpretMessage(ctx, msg)) {
+ return right.behavior
+ }
+
+ return unhandled()
+ }
+
+ override fun receiveSignal(ctx: ActorContext<T>, signal: Signal): Behavior<T> {
+ 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 <U : Any> unsafeCast(): Behavior<U> {
+ @Suppress("UNCHECKED_CAST")
+ return this as Behavior<U>
+ }
+}
+
+/**
+ * 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<T : Any> : Behavior<T>() {
+ /**
+ * 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<T>): Behavior<T>
+}
+
+/**
+ * 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<T : Any> : Behavior<T>() {
+ /**
+ * 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<T>, msg: T): Behavior<T> = 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<T>, signal: Signal): Behavior<T> = unhandled()
+}
+
+/**
+ * A flag to indicate whether a [Behavior] instance is still alive.
+ */
+val <T : Any> Behavior<T>.isAlive get() = this !is StoppedBehavior
+
+/**
+ * A flag to indicate whether the last message/signal went unhandled.
+ */
+val <T : Any> Behavior<T>.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<Any>() {
+ override fun toString() = "Stopped"
+}
+
+/**
+ * A special [Behavior] object to signal that the actor wants to reuse its previous behavior.
+ */
+internal object SameBehavior : Behavior<Nothing>() {
+ override fun toString() = "Same"
+}
+
+/**
+ * A special [Behavior] object that indicates that the last message or signal was not handled.
+ */
+internal object UnhandledBehavior : Behavior<Nothing>() {
+ 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 <T : Any> stopped(): Behavior<T> = StoppedBehavior.unsafeCast()
+
+/**
+ * This [Behavior] is used to signal that this actor wants to reuse its previous behavior.
+ */
+fun <T : Any> same(): Behavior<T> = 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 <T : Any> unhandled(): Behavior<T> = UnhandledBehavior.unsafeCast()
+
+/**
+ * A factory for [Behavior]. Creation of the behavior instance is deferred until the actor is started.
+ */
+fun <T : Any> setup(block: (ActorContext<T>) -> Behavior<T>): Behavior<T> {
+ return object : DeferredBehavior<T>() {
+ override fun invoke(ctx: ActorContext<T>): Behavior<T> = block(ctx)
+ }
+}
+
+/**
+ * A [Behavior] that ignores any incoming message or signal and keeps the same behavior.
+ */
+fun <T : Any> ignore(): Behavior<T> = IgnoreBehavior.narrow()
+
+/**
+ * A [Behavior] that treats every incoming message or signal as unhandled.
+ */
+fun <T : Any> empty(): Behavior<T> = EmptyBehavior.narrow()
+
+/**
+ * Construct a [Behavior] that reacts to incoming messages, provides access to the [ActorContext] and returns the
+ * actor's next behavior.
+ */
+fun <T : Any> receive(handler: (ActorContext<T>, T) -> Behavior<T>): Behavior<T> {
+ return object : ReceivingBehavior<T>() {
+ override fun receive(ctx: ActorContext<T>, msg: T): Behavior<T> = 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 <T : Any, reified U : T> receiveOf(crossinline handler: (ActorContext<T>, U) -> Behavior<T>): Behavior<T> {
+ return object : ReceivingBehavior<T>() {
+ override fun receive(ctx: ActorContext<T>, msg: T): Behavior<T> {
+ 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 <T : Any> receiveMessage(handler: (T) -> Behavior<T>): Behavior<T> {
+ return object : ReceivingBehavior<T>() {
+ override fun receive(ctx: ActorContext<T>, msg: T): Behavior<T> = handler(msg)
+ }
+}
+
+/**
+ * Construct a [Behavior] that reacts to incoming signals, provides access to the [ActorContext] and returns the
+ * actor's next behavior.
+ */
+fun <T : Any> receiveSignal(handler: (ActorContext<T>, Signal) -> Behavior<T>): Behavior<T> {
+ return object : ReceivingBehavior<T>() {
+ override fun receiveSignal(ctx: ActorContext<T>, signal: Signal): Behavior<T> = 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 <T : Any> wrap(behavior: Behavior<T>, wrap: (BehaviorInterpreter<T>) -> Behavior<T>): Behavior<T> {
+ return setup { ctx -> wrap(BehaviorInterpreter(behavior, ctx)) }
+}
+
+/**
+ * Obtain a [TimerScheduler] for building a [Behavior] instance.
+ */
+fun <T : Any> withTimers(handler: (TimerScheduler<T>) -> Behavior<T>): Behavior<T> {
+ return setup { ctx ->
+ val scheduler = TimerSchedulerImpl(ctx)
+ receiveSignal<T> { _, 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 <T : Any> withTimeout(after: Duration, handler: (ActorContext<T>) -> Behavior<T>): Behavior<T> =
+ 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 <T : Any> Behavior<T>.join(that: Behavior<T>): Behavior<T> =
+ wrap(this) { left ->
+ wrap(that) { right ->
+ object : ReceivingBehavior<T>() {
+ override fun receive(ctx: ActorContext<T>, msg: T): Behavior<T> {
+ 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<T>, signal: Signal): Behavior<T> {
+ 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 <U : Any, reified T : U> Behavior<T>.widen(): Behavior<U> = widen {
+ if (it is T)
+ it
+ else
+ null
+}
+
+/**
+ * Keep the specified [Behavior] alive if it returns the stopped behavior.
+ */
+fun <T : Any> Behavior<T>.keepAlive(): Behavior<T> =
+ wrap(this) { interpreter ->
+ object : ReceivingBehavior<T>() {
+ override fun receive(ctx: ActorContext<T>, msg: T): Behavior<T> {
+ if (interpreter.interpretMessage(ctx, msg)) {
+ return this
+ }
+ return empty()
+ }
+
+ override fun receiveSignal(ctx: ActorContext<T>, signal: Signal): Behavior<T> {
+ 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<T : Any> : Comparable<Envelope<*>>, 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<T : Any> {
+ /**
+ * 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<T>, behavior: Behavior<T>): Behavior<T>
+
+ companion object {
+ /**
+ * Construct a [StashBuffer] with the specified [capacity].
+ *
+ * @param capacity The capacity of the buffer.
+ */
+ operator fun <T : Any> invoke(capacity: Int): StashBuffer<T> = 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<T : Any> {
+ /**
+ * 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<T : Any> : DeferredBehavior<T>() {
+ /**
+ * 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<T>): Behavior<T>
+
+ // Immediately transfer to implementation
+ override fun invoke(ctx: ActorContext<T>): Behavior<T> = 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<T : Any> : ActorContext<T>, 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<SuspendingActorContext<*>>
+}
+
+/**
+ * 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 <T : Any, U> suspendWithBehavior(block: (Continuation<U>, () -> Behavior<T>) -> Behavior<T>): U =
+ suspendCoroutine { cont ->
+ @Suppress("UNCHECKED_CAST")
+ val ctx = cont.context[SuspendingActorContext] as? SuspendingActorContextImpl<T>
+ ?: 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 <T : Any> actorContext(): SuspendingActorContext<T> =
+ suspendCoroutineUninterceptedOrReturn { cont ->
+ @Suppress("UNCHECKED_CAST")
+ cont.context[SuspendingActorContext] as? SuspendingActorContext<T>
+ ?: throw UnsupportedOperationException("Coroutine does not run inside SuspendingBehavior")
+ }
+
+/**
+ * Construct a [Behavior] that uses Kotlin coroutines functionality to handle incoming messages and signals.
+ */
+fun <T : Any> suspending(block: suspend (SuspendingActorContext<T>) -> Behavior<T>): Behavior<T> {
+ return object : SuspendingBehavior<T>() {
+ override suspend fun invoke(ctx: SuspendingActorContext<T>) = 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 <T : Any, reified U : T> SuspendingActorContext<T>.receiveOf(): U =
+ suspendWithBehavior<T, U> { 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 <T : Any, U : Any, reified V : T> SuspendingActorContext<T>.ask(
+ ref: ActorRef<U>,
+ after: Duration = 0.0,
+ transform: (ActorRef<T>) -> 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<Any, Unit> { 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<Any>, 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<Any>() {
+ override fun receive(ctx: ActorContext<Any>, msg: Any): Behavior<Any> = this
+
+ override fun receiveSignal(ctx: ActorContext<Any>, signal: Signal): Behavior<Any> = this
+
+ override fun toString() = "Ignore"
+}
+
+/**
+ * A [Behavior] object that does not handle any message it receives.
+ */
+internal object EmptyBehavior : ReceivingBehavior<Any>() {
+ 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<T : Any>(initialBehavior: Behavior<T>) {
+ /**
+ * The current [Behavior] instance.
+ */
+ var behavior: Behavior<T> = 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<T>, ctx: ActorContext<T>) : this(initialBehavior) {
+ start(ctx)
+ }
+
+ /**
+ * Start the initial behavior.
+ *
+ * @param ctx The [ActorContext] to start the behavior in.
+ */
+ fun start(ctx: ActorContext<T>) {
+ 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<T>) {
+ 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<T>, next: Behavior<T>) {
+ 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<T>): Behavior<T> =
+ 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<T>, 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<T>, 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<T>, msg: Any, isSignal: Boolean): Boolean =
+ if (isAlive) {
+ val next = when (val current = behavior) {
+ is DeferredBehavior<T> ->
+ throw IllegalStateException("Deferred [$current] should not be passed to interpreter")
+ is ReceivingBehavior<T> ->
+ 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<T>): Behavior<T> =
+ 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 <T : Any> start(ctx: ActorContext<T>, behavior: Behavior<T>): Behavior<T> =
+ when (behavior) {
+ is DeferredBehavior<T> -> 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 <T : Any> canonicalize(ctx: ActorContext<T>, current: Behavior<T>, next: Behavior<T>): Behavior<T> =
+ when (next) {
+ is SameBehavior, current -> current
+ is UnhandledBehavior -> current
+ is DeferredBehavior<T> -> 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<T : Any> : SuspendingActorContext<T> {
+ /**
+ * The current active behavior
+ */
+ val behavior: Behavior<T>
+
+ /**
+ * Replace the current active behavior with the specified new behavior.
+ *
+ * @param next The behavior to replace the current behavior with.
+ */
+ fun become(next: Behavior<T>)
+}
+
+/**
+ * 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<T : Any>(
+ private var actorContext: ActorContext<T>,
+ initialBehavior: SuspendingBehavior<T>
+) : ReceivingBehavior<T>(), SuspendingActorContextImpl<T> {
+
+ /**
+ * The next behavior to use.
+ */
+ private var next: Behavior<T> = this
+
+ /**
+ * The [BehaviorInterpreter] to wrap the suspending behavior.
+ */
+ private val interpreter = BehaviorInterpreter(initialBehavior)
+
+ override fun receive(ctx: ActorContext<T>, msg: T): Behavior<T> {
+ this.actorContext = ctx
+ return interpreter.also { it.interpretMessage(ctx, msg) }.propagate(next)
+ }
+
+ override fun receiveSignal(ctx: ActorContext<T>, signal: Signal): Behavior<T> {
+ this.actorContext = ctx
+ return interpreter.also { it.interpretSignal(ctx, signal) }.propagate(next)
+ }
+
+ override val self: ActorRef<T> get() = actorContext.self
+
+ override val time: Instant get() = actorContext.time
+
+ override val children: List<ActorRef<*>>
+ 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 <U : Any> send(ref: ActorRef<U>, msg: U, after: Duration) = actorContext.send(ref, msg, after)
+
+ override fun <U : Any> spawn(behavior: Behavior<U>, name: String) = actorContext.spawn(behavior, name)
+
+ override fun <U : Any> spawnAnonymous(behavior: Behavior<U>) = 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<T, T> { cont, next ->
+ receiveMessage { msg ->
+ cont.resume(msg)
+ next()
+ }
+ }
+
+ override suspend fun receiveSignal(): Signal = suspendWithBehavior<T, Signal> { cont, next ->
+ receiveSignal { _, signal ->
+ cont.resume(signal)
+ next()
+ }
+ }
+
+ override val behavior: Behavior<T> get() = interpreter.behavior
+
+ override fun become(next: Behavior<T>) {
+ interpreter.become(actorContext, next)
+ }
+
+ override val key: CoroutineContext.Key<*> = SuspendingActorContext.Key
+
+ /**
+ * Start the suspending behavior.
+ */
+ internal fun start(): Behavior<T> {
+ val behavior = interpreter.behavior as SuspendingBehavior<T>
+ 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<Behavior<T>> {
+ override val context = this@SuspendingBehaviorImpl
+
+ override fun resumeWith(result: Result<Behavior<T>>) {
+ 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<T : Any>(private val capacity: Int) : StashBuffer<T> {
+ /**
+ * The internal queue used to store the messages.
+ */
+ private val queue = ArrayDeque<T>(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<T>, behavior: Behavior<T>): Behavior<T> {
+ val messages = queue.toList()
+ queue.clear()
+
+ val interpreter = BehaviorInterpreter<T>(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<T : Any>(private val ctx: ActorContext<T>) : TimerScheduler<T> {
+ private val timers = mutableMapOf<Any, Timer<T>>()
+
+ 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<T : Any>(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<Any?>) {
+ 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<Any?>) {
+ 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<Any?>) {
+ 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<Any?>) {
+ 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<Any?>) {
+ 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<Any?>) {
+ 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<Any?>) {
+ 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<Any?>) {
+ 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<Any?>) {
+ 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<Any?>) {
+ 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<IllegalArgumentException> { 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<IllegalArgumentException> { 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<ActorContext<Unit>>()
+ val interpreter = BehaviorInterpreter(unhandled<Unit>())
+ assertThrows<IllegalArgumentException> { 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<ActorContext<Unit>>()
+ val interpreter = BehaviorInterpreter(setup<Unit> { unhandled() })
+ assertThrows<IllegalArgumentException> { interpreter.start(ctx) }
+ }
+
+ /**
+ * Test whether deferred behavior that returns [same] fails.
+ */
+ @Test
+ fun `should not allow setup to return same`() {
+ val ctx = mock<ActorContext<Unit>>()
+ val interpreter = BehaviorInterpreter(setup<Unit> { same() })
+ assertThrows<IllegalArgumentException> { interpreter.start(ctx) }
+ }
+
+ /**
+ * Test whether deferred behavior that returns [unhandled] fails.
+ */
+ @Test
+ fun `should not allow setup to return unhandled`() {
+ val ctx = mock<ActorContext<Unit>>()
+ val interpreter = BehaviorInterpreter(setup<Unit> { unhandled() })
+ assertThrows<IllegalArgumentException> { 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<ActorContext<Nothing>>()
+ val behavior = suspending<Nothing> { 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<ActorContext<Unit>>()
+ val behavior = suspending<Unit> {
+ suspendCoroutine<Unit> {}
+ 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<in T : Any>(guardianBehavior: Behavior<T>, override val name: String) : ActorSystem<T>, ActorRef<T> {
+ /**
+ * The state of the actor system.
+ */
+ private var state: ActorSystemState = ActorSystemState.CREATED
+
+ /**
+ * The event queue to process
+ */
+ private val queue: PriorityQueue<EnvelopeImpl> = PriorityQueue(
+ Comparator
+ .comparingDouble(EnvelopeImpl::time)
+ .thenComparingLong(EnvelopeImpl::id)
+ )
+
+ /**
+ * The registry of actors in the system.
+ */
+ private val registry: MutableMap<ActorPath, Actor<*>> = 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<Nothing>())
+ 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 <U : Any> spawnSystem(behavior: Behavior<U>, name: String): ActorRef<U> {
+ 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<T : Any>(override val self: ActorRef<T>, initialBehavior: Behavior<T>) : ActorContext<T> {
+ val childActors: MutableMap<String, Actor<*>> = mutableMapOf()
+ val interpreter = BehaviorInterpreter(initialBehavior)
+ val watchers: MutableSet<ActorPath> = Collections.newSetFromMap(WeakHashMap<ActorPath, Boolean>())
+
+ override val time: Instant
+ get() = this@OmegaActorSystem.time
+
+ override val children: List<ActorRef<*>>
+ 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 <U : Any> send(ref: ActorRef<U>, msg: U, after: Duration) = schedule(ref.path, msg, after)
+
+ override fun <U : Any> spawn(behavior: Behavior<U>, name: String): ActorRef<U> {
+ 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 <U : Any> spawnAnonymous(behavior: Behavior<U>): ActorRef<U> {
+ val name = "$" + UUID.randomUUID()
+ return internalSpawn(behavior, name)
+ }
+
+ private fun <U : Any> internalSpawn(behavior: Behavior<U>, name: String): ActorRef<U> {
+ require(name !in childActors) { "Actor name $name not unique" }
+ val ref = ActorRefImpl<U>(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 <T : Any, U> Actor<T>.isolate(block: (Actor<T>) -> 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<T : Any>(
+ private val owner: OmegaActorSystem<*>,
+ override val path: ActorPath
+ ) : ActorRef<T> {
+ 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<Any>
+}
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 <T : Any> invoke(root: Behavior<T>, name: String): ActorSystem<T> =
+ 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<Unit>(), 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<Unit>(), "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<Unit>(), 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<Unit>(), name = "test")
+ assertThrows<IllegalArgumentException> { 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<Unit>(), 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<Unit>(), 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<Int> { 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<Unit> { _ ->
+ 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<Unit> { 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<Unit>(), name = "test")
+ assertThrows<IllegalArgumentException> { 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<Unit> { 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<Unit> { spawned = true; empty() }
+
+ val system = factory(setup<Unit> { 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<Unit> { ctx ->
+ val ref = ctx.spawn(receiveMessage<Unit> { 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<Unit> { ctx1 ->
+ val child1 = ctx1.spawn(ignore<Unit>(), "child-1")
+ ctx1.spawn(setup<Unit> { 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<Unit> { ctx ->
+ val child = ctx.spawn(ignore<Unit>(), "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<ActorRef<Unit>> { ctx ->
+ val root = ctx.self
+ val child = ctx.spawn(setup<Unit> {
+ val child = it.spawn(receiveMessage<Unit> {
+ 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<Unit> { 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<Unit> = 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<Unit>(), "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<Unit> {
+ 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<Nothing> { ctx ->
+ val child = ctx.spawn(suspending<Nothing> {
+ 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<Unit>(), 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<Unit> {
+ flag = true
+ stopped()
+ }, "test")
+
+ system.run(until = 10.0)
+ system.terminate()
+ assertTrue(flag)
+ }
+}