diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2019-11-20 17:51:58 +0100 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2019-11-20 21:49:20 +0100 |
| commit | 6ca0ae07669d20a5a34ef697610df90754024035 (patch) | |
| tree | 3c26a21970fa5693b18edb34e8203d711c381ba5 /odcsim/odcsim-core | |
| parent | 4cc3c6dea5c5536d47fcbaf8414d74de7b6fdc4b (diff) | |
refactor: Move build logic to buildSrc
Diffstat (limited to 'odcsim/odcsim-core')
28 files changed, 3511 insertions, 0 deletions
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) + } +} |
