diff options
Diffstat (limited to 'simulator/opendc-serverless')
20 files changed, 1435 insertions, 0 deletions
diff --git a/simulator/opendc-serverless/build.gradle.kts b/simulator/opendc-serverless/build.gradle.kts new file mode 100644 index 00000000..a458c809 --- /dev/null +++ b/simulator/opendc-serverless/build.gradle.kts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +description = "Serverless platform for OpenDC" diff --git a/simulator/opendc-serverless/opendc-serverless-api/build.gradle.kts b/simulator/opendc-serverless/opendc-serverless-api/build.gradle.kts new file mode 100644 index 00000000..ec33ccce --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-api/build.gradle.kts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +description = "Serverless API for OpenDC" + +/* Build configuration */ +plugins { + `kotlin-library-conventions` +} + +dependencies { + api(platform(project(":opendc-platform"))) +} diff --git a/simulator/opendc-serverless/opendc-serverless-api/src/main/kotlin/org/opendc/serverless/api/ServerlessClient.kt b/simulator/opendc-serverless/opendc-serverless-api/src/main/kotlin/org/opendc/serverless/api/ServerlessClient.kt new file mode 100644 index 00000000..f14f4977 --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-api/src/main/kotlin/org/opendc/serverless/api/ServerlessClient.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.api + +import java.util.* + +/** + * Client interface to the OpenDC Serverless service. + */ +public interface ServerlessClient : AutoCloseable { + /** + * Obtain the list of [ServerlessFunction]s accessible by the requesting user. + */ + public suspend fun queryFunctions(): List<ServerlessFunction> + + /** + * Obtain a [ServerlessFunction] by its unique identifier. + * + * @param id The identifier of the flavor. + */ + public suspend fun findFunction(id: UUID): ServerlessFunction? + + /** + * Obtain a [ServerlessFunction] by its name. + * + * @param name The name of the function. + */ + public suspend fun findFunction(name: String): ServerlessFunction? + + /** + * Create a new serverless function. + * + * @param name The name of the function. + * @param labels The labels associated with the function. + * @param meta The metadata associated with the function. + */ + public suspend fun newFunction( + name: String, + labels: Map<String, String> = emptyMap(), + meta: Map<String, Any> = emptyMap() + ): ServerlessFunction + + /** + * Invoke the function with the specified [name]. + */ + public suspend fun invoke(name: String) + + /** + * Release the resources associated with this client, preventing any further API calls. + */ + public override fun close() +} diff --git a/simulator/opendc-serverless/opendc-serverless-api/src/main/kotlin/org/opendc/serverless/api/ServerlessFunction.kt b/simulator/opendc-serverless/opendc-serverless-api/src/main/kotlin/org/opendc/serverless/api/ServerlessFunction.kt new file mode 100644 index 00000000..ec0fad71 --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-api/src/main/kotlin/org/opendc/serverless/api/ServerlessFunction.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.api + +import java.util.UUID + +/** + * A serverless function instance. + */ +public interface ServerlessFunction { + /** + * The unique identifier of the function. + */ + public val uid: UUID + + /** + * The name of the function. + */ + public val name: String + + /** + * The identifying labels attached to the resource. + */ + public val labels: Map<String, String> + + /** + * The non-identifying metadata attached to the resource. + */ + public val meta: Map<String, Any> + + /** + * Invoke the serverless function. + */ + public suspend operator fun invoke() + + /** + * Request the function to be deleted. + */ + public suspend fun delete() + + /** + * Refresh the local state of this object. + */ + public suspend fun refresh() +} diff --git a/simulator/opendc-serverless/opendc-serverless-service/build.gradle.kts b/simulator/opendc-serverless/opendc-serverless-service/build.gradle.kts new file mode 100644 index 00000000..bcb08be7 --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-service/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +description = "Serverless service for OpenDC" + +/* Build configuration */ +plugins { + `kotlin-library-conventions` + `testing-conventions` + `jacoco-conventions` +} + +dependencies { + api(platform(project(":opendc-platform"))) + api(project(":opendc-serverless:opendc-serverless-api")) + api(project(":opendc-trace:opendc-trace-core")) + implementation(project(":opendc-utils")) + implementation("io.github.microutils:kotlin-logging") + + testImplementation(project(":opendc-simulator:opendc-simulator-core")) + testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j-impl") +} diff --git a/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/ServerlessService.kt b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/ServerlessService.kt new file mode 100644 index 00000000..18717ef5 --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/ServerlessService.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.service + +import org.opendc.serverless.api.ServerlessClient +import org.opendc.serverless.service.deployer.FunctionDeployer +import org.opendc.serverless.service.internal.ServerlessServiceImpl +import org.opendc.serverless.service.router.RoutingPolicy +import java.time.Clock +import kotlin.coroutines.CoroutineContext + +/** + * The [ServerlessService] hosts the API implementation of the OpenDC Serverless service. + */ +public interface ServerlessService : AutoCloseable { + /** + * Create a new [ServerlessClient] to control the compute service. + */ + public fun newClient(): ServerlessClient + + /** + * Terminate the lifecycle of the serverless service, stopping all running function instances. + */ + public override fun close() + + public companion object { + /** + * Construct a new [ServerlessService] implementation. + * + * @param context The [CoroutineContext] to use in the service. + * @param clock The clock instance to use. + */ + public operator fun invoke( + context: CoroutineContext, + clock: Clock, + deployer: FunctionDeployer, + routingPolicy: RoutingPolicy, + ): ServerlessService { + return ServerlessServiceImpl(context, clock, deployer, routingPolicy) + } + } +} diff --git a/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/deployer/FunctionDeployer.kt b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/deployer/FunctionDeployer.kt new file mode 100644 index 00000000..e0a37009 --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/deployer/FunctionDeployer.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.service.deployer + +import org.opendc.serverless.api.ServerlessFunction + +/** + * A [FunctionDeployer] is responsible for ensuring that an instance of an arbitrary function, a [FunctionInstance], + * is deployed. + * + * The function deployer should combines the configuration stored in the function registry, the parameters supplied by + * the requester, and other factors into a decision of how the function should be deployed, including how many and + * what kind of resources it should receive. + * + * Though it decides how the function instance should be deployed, the deployment of the function instance itself is + * delegated to the Resource Orchestration Layer. + */ +public interface FunctionDeployer { + /** + * Deploy the specified [function]. + */ + public fun deploy(function: ServerlessFunction): FunctionInstance +} diff --git a/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/deployer/FunctionInstance.kt b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/deployer/FunctionInstance.kt new file mode 100644 index 00000000..410df5d4 --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/deployer/FunctionInstance.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.service.deployer + +import org.opendc.serverless.api.ServerlessFunction + +/** + * A [FunctionInstance] is a a self-contained worker—typically a container—capable of handling function executions. + * + * Multiple, concurrent function instances can exists for a single function, for scalability purposes. + */ +public interface FunctionInstance : AutoCloseable { + /** + * The state of the instance. + */ + public val state: FunctionInstanceState + + /** + * The [ServerlessFunction] that is represented by this instance. + */ + public val function: ServerlessFunction + + /** + * Invoke the function instance. + * + * This method will suspend execution util the function instance has returned. + */ + public suspend fun invoke() + + /** + * Indicate to the resource manager that the instance is not needed anymore and may be cleaned up by the resource + * manager. + */ + public override fun close() +} diff --git a/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/deployer/FunctionInstanceState.kt b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/deployer/FunctionInstanceState.kt new file mode 100644 index 00000000..44ad80ee --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/deployer/FunctionInstanceState.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.service.deployer + +/** + * This enumeration describes the states of a [FunctionInstance]. + */ +public enum class FunctionInstanceState { + /** + * The function instance is currently being provisioned. + */ + Provisioning, + + /** + * The function instance is idle and ready to execute. + */ + Idle, + + /** + * The function instance is executing. + */ + Active, + + /** + * The function instance is stopped but can be started. + */ + Terminated, + + /** + * The function instance is released and cannot be used anymore. + */ + Deleted +} diff --git a/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/internal/ClientFunction.kt b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/internal/ClientFunction.kt new file mode 100644 index 00000000..1258a037 --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/internal/ClientFunction.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.service.internal + +import org.opendc.serverless.api.ServerlessFunction +import java.util.* + +/** + * A [ServerlessFunction] implementation that is passed to clients but delegates its implementation to another class. + */ +internal class ClientFunction(private val delegate: ServerlessFunction) : ServerlessFunction { + override val uid: UUID = delegate.uid + + override var name: String = delegate.name + private set + + override var labels: Map<String, String> = delegate.labels.toMap() + private set + + override var meta: Map<String, Any> = delegate.meta.toMap() + private set + + override suspend fun delete() { + delegate.delete() + } + + override suspend fun invoke() { + delegate.invoke() + } + + override suspend fun refresh() { + delegate.refresh() + + name = delegate.name + labels = delegate.labels + meta = delegate.meta + } + + override fun equals(other: Any?): Boolean = other is ClientFunction && uid == other.uid + + override fun hashCode(): Int = uid.hashCode() + + override fun toString(): String = "ServerlessFunction[uid=$uid,name=$name]" +} diff --git a/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/internal/InternalFunction.kt b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/internal/InternalFunction.kt new file mode 100644 index 00000000..a6e22912 --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/internal/InternalFunction.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.service.internal + +import org.opendc.serverless.api.ServerlessFunction +import java.util.* + +/** + * Internal stateful representation of a [ServerlessFunction]. + */ +internal class InternalFunction( + private val service: ServerlessServiceImpl, + override val uid: UUID, + name: String, + labels: Map<String, String>, + meta: Map<String, Any> +) : ServerlessFunction { + override var name: String = name + private set + + override val labels: MutableMap<String, String> = labels.toMutableMap() + + override val meta: MutableMap<String, Any> = meta.toMutableMap() + + override suspend fun refresh() { + // No-op: this object is the source-of-truth + } + + override suspend fun invoke() { + service.invoke(this) + } + + override suspend fun delete() { + service.delete(this) + } + + override fun equals(other: Any?): Boolean = other is ServerlessFunction && uid == other.uid + + override fun hashCode(): Int = uid.hashCode() +} diff --git a/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/internal/ServerlessServiceImpl.kt b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/internal/ServerlessServiceImpl.kt new file mode 100644 index 00000000..b3f395c3 --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/internal/ServerlessServiceImpl.kt @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.service.internal + +import kotlinx.coroutines.* +import kotlinx.coroutines.intrinsics.startCoroutineCancellable +import mu.KotlinLogging +import org.opendc.serverless.api.ServerlessClient +import org.opendc.serverless.api.ServerlessFunction +import org.opendc.serverless.service.ServerlessService +import org.opendc.serverless.service.deployer.FunctionDeployer +import org.opendc.serverless.service.deployer.FunctionInstance +import org.opendc.serverless.service.router.RoutingPolicy +import org.opendc.utils.TimerScheduler +import java.lang.IllegalStateException +import java.time.Clock +import java.util.* +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resumeWithException + +/** + * Implementation of the [ServerlessService] interface. + * + * This component acts as the function router from the SPEC RG Reference Architecture for FaaS and is responsible + * for routing incoming requests or events to the correct [FunctionInstance]. If no [FunctionInstance] is available, + * this component queues the events to await the deployment of new instances. + */ +internal class ServerlessServiceImpl( + context: CoroutineContext, + private val clock: Clock, + private val deployer: FunctionDeployer, + private val routingPolicy: RoutingPolicy +) : ServerlessService { + /** + * The [CoroutineScope] of the service bounded by the lifecycle of the service. + */ + private val scope = CoroutineScope(context + Job()) + + /** + * The logger instance of this server. + */ + private val logger = KotlinLogging.logger {} + + /** + * The [TimerScheduler] to use for scheduling the scheduler cycles. + */ + private val scheduler: TimerScheduler<Unit> = TimerScheduler(scope.coroutineContext, clock) + + /** + * The [Random] instance used to generate unique identifiers for the objects. + */ + private val random = Random(0) + + /** + * The registered functions for this service. + */ + private val functions = mutableMapOf<UUID, InternalFunction>() + private val functionsByName = mutableMapOf<String, InternalFunction>() + + /** + * The queue of invocation requests. + */ + private val queue = ArrayDeque<InvocationRequest>() + + /** + * The active function instances. + */ + private val instancesByFunction = mutableMapOf<InternalFunction, MutableList<FunctionInstance>>() + + override fun newClient(): ServerlessClient { + return object : ServerlessClient { + private var isClosed: Boolean = false + + override suspend fun queryFunctions(): List<ServerlessFunction> { + check(!isClosed) { "Client is already closed" } + + return functions.values.map { ClientFunction(it) } + } + + override suspend fun findFunction(id: UUID): ServerlessFunction? { + check(!isClosed) { "Client is already closed" } + + return functions[id]?.let { ClientFunction(it) } + } + + override suspend fun findFunction(name: String): ServerlessFunction? { + check(!isClosed) { "Client is already closed" } + + return functionsByName[name]?.let { ClientFunction(it) } + } + + override suspend fun newFunction( + name: String, + labels: Map<String, String>, + meta: Map<String, Any> + ): ServerlessFunction { + check(!isClosed) { "Client is already closed" } + require(name !in functionsByName) { "Function with same name exists" } + + val uid = UUID(clock.millis(), random.nextLong()) + val function = InternalFunction( + this@ServerlessServiceImpl, + uid, + name, + labels, + meta + ) + + functionsByName[name] = function + functions[uid] = function + + return ClientFunction(function) + } + + override suspend fun invoke(name: String) { + check(!isClosed) { "Client is already closed" } + + requireNotNull(functionsByName[name]) { "Unknown function" }.invoke() + } + + override fun close() { + isClosed = true + } + } + } + + /** + * Indicate that a new scheduling cycle is needed due to a change to the service's state. + */ + private fun schedule() { + // Bail out in case we have already requested a new cycle or the queue is empty. + if (scheduler.isTimerActive(Unit) || queue.isEmpty()) { + return + } + + val quantum = 1000 + + // We assume that the provisioner runs at a fixed slot every time quantum (e.g t=0, t=60, t=120). + // This is important because the slices of the VMs need to be aligned. + // We calculate here the delay until the next scheduling slot. + val delay = quantum - (clock.millis() % quantum) + + scheduler.startSingleTimer(Unit, delay, ::doSchedule) + } + + /** + * Run a single scheduling iteration. + */ + @OptIn(InternalCoroutinesApi::class) + private fun doSchedule() { + try { + while (queue.isNotEmpty()) { + val (function, cont) = queue.poll() + + val instances = instancesByFunction[function] + + // Check if there exists an instance of the function + val activeInstance = if (instances != null && instances.isNotEmpty()) { + routingPolicy.select(instances, function) + } else { + null + } + + val instance = if (activeInstance != null) { + activeInstance + } else { + val instance = deployer.deploy(function) + instancesByFunction.compute(function) { _, v -> + if (v != null) { + v.add(instance) + v + } else { + mutableListOf(instance) + } + } + + instance + } + + // Invoke the function instance + suspend { instance.invoke() }.startCoroutineCancellable(cont) + } + } catch (cause: Throwable) { + logger.error(cause) { "Exception occurred during scheduling cycle" } + } + } + + internal suspend fun invoke(function: InternalFunction) { + check(function.uid in functions) { "Function does not exist (anymore)" } + + return suspendCancellableCoroutine { cont -> + if (!queue.add(InvocationRequest(function, cont))) { + cont.resumeWithException(IllegalStateException("Failed to enqueue request")) + } else { + schedule() + } + } + } + + internal fun delete(function: InternalFunction) { + functions.remove(function.uid) + functionsByName.remove(function.name) + } + + override fun close() { + scope.cancel() + + // Stop all function instances + for ((_, instances) in instancesByFunction) { + instances.forEach(FunctionInstance::close) + } + instancesByFunction.clear() + } + + /** + * A request to invoke a function. + */ + private data class InvocationRequest(val function: InternalFunction, val cont: Continuation<Unit>) +} diff --git a/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/router/RandomRoutingPolicy.kt b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/router/RandomRoutingPolicy.kt new file mode 100644 index 00000000..015704ca --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/router/RandomRoutingPolicy.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.service.router + +import org.opendc.serverless.api.ServerlessFunction +import org.opendc.serverless.service.deployer.FunctionInstance +import kotlin.random.Random + +/** + * A [RoutingPolicy] that selects a random function instance. + */ +public class RandomRoutingPolicy(private val random: Random = Random(0)) : RoutingPolicy { + override fun select(instances: List<FunctionInstance>, function: ServerlessFunction): FunctionInstance { + return instances.random(random) + } +} diff --git a/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/router/RoutingPolicy.kt b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/router/RoutingPolicy.kt new file mode 100644 index 00000000..77f43059 --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-service/src/main/kotlin/org/opendc/serverless/service/router/RoutingPolicy.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.service.router + +import org.opendc.serverless.api.ServerlessFunction +import org.opendc.serverless.service.deployer.FunctionInstance + +/** + * A [RoutingPolicy] decides to which [FunctionInstance] a function invocation should be routed. + */ +public interface RoutingPolicy { + /** + * Select the instance to which the request should be routed to. + */ + public fun select(instances: List<FunctionInstance>, function: ServerlessFunction): FunctionInstance? +} diff --git a/simulator/opendc-serverless/opendc-serverless-service/src/test/kotlin/org/opendc/serverless/service/ServerlessServiceTest.kt b/simulator/opendc-serverless/opendc-serverless-service/src/test/kotlin/org/opendc/serverless/service/ServerlessServiceTest.kt new file mode 100644 index 00000000..d9c2bcd2 --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-service/src/test/kotlin/org/opendc/serverless/service/ServerlessServiceTest.kt @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.service + +import io.mockk.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.opendc.serverless.api.ServerlessFunction +import org.opendc.serverless.service.deployer.FunctionDeployer +import org.opendc.serverless.service.deployer.FunctionInstance +import org.opendc.serverless.service.deployer.FunctionInstanceState +import org.opendc.simulator.utils.DelayControllerClockAdapter +import java.util.* + +/** + * Test suite for the [ServerlessService] implementation. + */ +@OptIn(ExperimentalCoroutinesApi::class) +internal class ServerlessServiceTest { + + @Test + fun testClientState() = runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val service = ServerlessService(coroutineContext, clock, mockk(), mockk()) + + val client = assertDoesNotThrow { service.newClient() } + assertDoesNotThrow { client.close() } + + assertThrows<IllegalStateException> { client.queryFunctions() } + assertThrows<IllegalStateException> { client.newFunction("test") } + assertThrows<IllegalStateException> { client.invoke("test") } + assertThrows<IllegalStateException> { client.findFunction(UUID.randomUUID()) } + assertThrows<IllegalStateException> { client.findFunction("name") } + } + + @Test + fun testClientInvokeUnknown() = runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val service = ServerlessService(coroutineContext, clock, mockk(), mockk()) + + val client = service.newClient() + + assertThrows<IllegalArgumentException> { client.invoke("test") } + } + + @Test + fun testClientFunctionCreation() = runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val service = ServerlessService(coroutineContext, clock, mockk(), mockk()) + + val client = service.newClient() + + val function = client.newFunction("test") + + assertEquals("test", function.name) + } + + @Test + fun testClientFunctionQuery() = runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val service = ServerlessService(coroutineContext, clock, mockk(), mockk()) + + val client = service.newClient() + + assertEquals(emptyList<ServerlessFunction>(), client.queryFunctions()) + + val function = client.newFunction("test") + + assertEquals(listOf(function), client.queryFunctions()) + } + + @Test + fun testClientFunctionFindById() = runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val service = ServerlessService(coroutineContext, clock, mockk(), mockk()) + + val client = service.newClient() + + assertEquals(emptyList<ServerlessFunction>(), client.queryFunctions()) + + val function = client.newFunction("test") + + assertNotNull(client.findFunction(function.uid)) + } + + @Test + fun testClientFunctionFindByName() = runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val service = ServerlessService(coroutineContext, clock, mockk(), mockk()) + + val client = service.newClient() + + assertEquals(emptyList<ServerlessFunction>(), client.queryFunctions()) + + val function = client.newFunction("test") + + assertNotNull(client.findFunction(function.name)) + } + + @Test + fun testClientFunctionDuplicateName() = runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val service = ServerlessService(coroutineContext, clock, mockk(), mockk()) + + val client = service.newClient() + + client.newFunction("test") + + assertThrows<IllegalArgumentException> { client.newFunction("test") } + } + + @Test + fun testClientFunctionDelete() = runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val service = ServerlessService(coroutineContext, clock, mockk(), mockk()) + + val client = service.newClient() + val function = client.newFunction("test") + assertNotNull(client.findFunction(function.uid)) + function.delete() + assertNull(client.findFunction(function.uid)) + + // Delete should be idempotent + function.delete() + } + + @Test + fun testClientFunctionCannotInvokeDeleted() = runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val service = ServerlessService(coroutineContext, clock, mockk(), mockk()) + + val client = service.newClient() + val function = client.newFunction("test") + assertNotNull(client.findFunction(function.uid)) + function.delete() + + assertThrows<IllegalStateException> { function.invoke() } + } + + @Test + fun testClientFunctionInvoke() = runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val deployer = mockk<FunctionDeployer>() + val service = ServerlessService(coroutineContext, clock, deployer, mockk()) + + every { deployer.deploy(any()) } answers { + object : FunctionInstance { + override val state: FunctionInstanceState = FunctionInstanceState.Idle + override val function: ServerlessFunction = it.invocation.args[0] as ServerlessFunction + + override suspend fun invoke() {} + + override fun close() {} + } + } + + val client = service.newClient() + val function = client.newFunction("test") + + function.invoke() + } +} diff --git a/simulator/opendc-serverless/opendc-serverless-simulator/build.gradle.kts b/simulator/opendc-serverless/opendc-serverless-simulator/build.gradle.kts new file mode 100644 index 00000000..fe3dca41 --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-simulator/build.gradle.kts @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +description = "Simulator for OpenDC Serverles" + +/* Build configuration */ +plugins { + `kotlin-library-conventions` + `testing-conventions` + `jacoco-conventions` +} + +dependencies { + api(platform(project(":opendc-platform"))) + api(project(":opendc-serverless:opendc-serverless-service")) + api(project(":opendc-simulator:opendc-simulator-compute")) + + testImplementation(project(":opendc-simulator:opendc-simulator-core")) + testRuntimeOnly("org.slf4j:slf4j-simple:${versions.slf4j}") +} diff --git a/simulator/opendc-serverless/opendc-serverless-simulator/src/main/kotlin/org/opendc/serverless/simulator/SimFunctionDeployer.kt b/simulator/opendc-serverless/opendc-serverless-simulator/src/main/kotlin/org/opendc/serverless/simulator/SimFunctionDeployer.kt new file mode 100644 index 00000000..7a48609c --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-simulator/src/main/kotlin/org/opendc/serverless/simulator/SimFunctionDeployer.kt @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.simulator + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import org.opendc.serverless.api.ServerlessFunction +import org.opendc.serverless.service.deployer.FunctionDeployer +import org.opendc.serverless.service.deployer.FunctionInstance +import org.opendc.serverless.service.deployer.FunctionInstanceState +import org.opendc.serverless.simulator.workload.SimServerlessWorkloadMapper +import org.opendc.simulator.compute.SimBareMetalMachine +import org.opendc.simulator.compute.SimMachine +import org.opendc.simulator.compute.SimMachineModel +import java.time.Clock +import java.util.ArrayDeque +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/** + * A [FunctionDeployer] that uses that simulates the [FunctionInstance]s. + */ +public class SimFunctionDeployer( + private val clock: Clock, + private val scope: CoroutineScope, + private val model: SimMachineModel, + private val mapper: SimServerlessWorkloadMapper +) : FunctionDeployer { + + override fun deploy(function: ServerlessFunction): Instance { + val instance = Instance(function) + instance.start() + return instance + } + + /** + * A simulated [FunctionInstance]. + */ + public inner class Instance(override val function: ServerlessFunction) : FunctionInstance { + /** + * The workload associated with this instance. + */ + private val workload = mapper.createWorkload(function) + + /** + * The machine that will execute the workloads. + */ + public val machine: SimMachine = SimBareMetalMachine(scope.coroutineContext, clock, model) + + /** + * The job associated with the lifecycle of the instance. + */ + private var job: Job? = null + + /** + * The invocation request queue. + */ + private val queue = ArrayDeque<InvocationRequest>() + + /** + * A channel used to signal that new invocations have been enqueued. + */ + private val chan = Channel<Unit>(Channel.RENDEZVOUS) + + override var state: FunctionInstanceState = FunctionInstanceState.Provisioning + + override suspend fun invoke() { + check(state != FunctionInstanceState.Deleted) { "Function instance has been released" } + return suspendCancellableCoroutine { cont -> + queue.add(InvocationRequest(cont)) + chan.offer(Unit) + } + } + + override fun close() { + state = FunctionInstanceState.Deleted + stop() + machine.close() + } + + override fun toString(): String = "FunctionInstance[state=$state]" + + /** + * Start the function instance. + */ + @OptIn(InternalCoroutinesApi::class) + internal fun start() { + check(state == FunctionInstanceState.Provisioning) { "Invalid state of function instance" } + job = scope.launch { + workload.onStart() + + try { + while (isActive) { + chan.receive() + + if (queue.isNotEmpty()) { + state = FunctionInstanceState.Active + } + + while (queue.isNotEmpty()) { + val request = queue.poll() + try { + machine.run(workload.onInvoke()) + request.cont.resume(Unit) + } catch (cause: CancellationException) { + request.cont.resumeWithException(cause) + throw cause + } catch (cause: Throwable) { + request.cont.resumeWithException(cause) + } + } + state = FunctionInstanceState.Idle + } + } finally { + state = FunctionInstanceState.Terminated + workload.onStop() + } + } + } + + /** + * Stop the function instance. + */ + private fun stop() { + val job = job + + if (job != null) { + this.job = null + job.cancel() + } + } + } + + /** + * A function invocation request. + */ + private data class InvocationRequest(val cont: Continuation<Unit>) +} diff --git a/simulator/opendc-serverless/opendc-serverless-simulator/src/main/kotlin/org/opendc/serverless/simulator/workload/SimServerlessWorkload.kt b/simulator/opendc-serverless/opendc-serverless-simulator/src/main/kotlin/org/opendc/serverless/simulator/workload/SimServerlessWorkload.kt new file mode 100644 index 00000000..afdc05af --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-simulator/src/main/kotlin/org/opendc/serverless/simulator/workload/SimServerlessWorkload.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.simulator.workload + +import org.opendc.simulator.compute.workload.SimWorkload + +/** + * A model for a serverless workload, which may be invoked multiple times. + */ +public interface SimServerlessWorkload { + /** + * This method is invoked when a function instance is launched. + */ + public fun onStart() {} + + /** + * This method is invoked when an active function instance is invoked. + */ + public fun onInvoke(): SimWorkload + + /** + * This method is invoked when the function instance is stopped. + */ + public fun onStop() {} +} diff --git a/simulator/opendc-serverless/opendc-serverless-simulator/src/main/kotlin/org/opendc/serverless/simulator/workload/SimServerlessWorkloadMapper.kt b/simulator/opendc-serverless/opendc-serverless-simulator/src/main/kotlin/org/opendc/serverless/simulator/workload/SimServerlessWorkloadMapper.kt new file mode 100644 index 00000000..670f978d --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-simulator/src/main/kotlin/org/opendc/serverless/simulator/workload/SimServerlessWorkloadMapper.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.simulator.workload + +import org.opendc.serverless.api.ServerlessFunction + +/** + * A [SimServerlessWorkloadMapper] is responsible for mapping a [ServerlessFunction] to a [SimServerlessWorkload] that + * can be simulated. + */ +public fun interface SimServerlessWorkloadMapper { + /** + * Map the specified [function] to a [SimServerlessWorkload] that can be simulated. + */ + public fun createWorkload(function: ServerlessFunction): SimServerlessWorkload +} diff --git a/simulator/opendc-serverless/opendc-serverless-simulator/src/test/kotlin/org/opendc/serverless/simulator/SimServerlessServiceTest.kt b/simulator/opendc-serverless/opendc-serverless-simulator/src/test/kotlin/org/opendc/serverless/simulator/SimServerlessServiceTest.kt new file mode 100644 index 00000000..a80365de --- /dev/null +++ b/simulator/opendc-serverless/opendc-serverless-simulator/src/test/kotlin/org/opendc/serverless/simulator/SimServerlessServiceTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.serverless.simulator + +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.yield +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.opendc.serverless.service.ServerlessService +import org.opendc.serverless.service.router.RandomRoutingPolicy +import org.opendc.serverless.simulator.workload.SimServerlessWorkload +import org.opendc.simulator.compute.SimMachineModel +import org.opendc.simulator.compute.model.SimMemoryUnit +import org.opendc.simulator.compute.model.SimProcessingNode +import org.opendc.simulator.compute.model.SimProcessingUnit +import org.opendc.simulator.compute.workload.SimFlopsWorkload +import org.opendc.simulator.compute.workload.SimWorkload +import org.opendc.simulator.utils.DelayControllerClockAdapter + +/** + * A test suite for the [ServerlessService] implementation under simulated conditions. + */ +@OptIn(ExperimentalCoroutinesApi::class) +internal class SimServerlessServiceTest { + + private lateinit var machineModel: SimMachineModel + + @BeforeEach + fun setUp() { + val cpuNode = SimProcessingNode("Intel", "Xeon", "amd64", 2) + + machineModel = SimMachineModel( + cpus = List(cpuNode.coreCount) { SimProcessingUnit(cpuNode, it, 1000.0) }, + memory = List(4) { SimMemoryUnit("Crucial", "MTA18ASF4G72AZ-3G2B1", 3200.0, 32_000) } + ) + } + + @Test + fun testSmoke() = runBlockingTest { + val clock = DelayControllerClockAdapter(this) + val workload = spyk(object : SimServerlessWorkload { + override fun onInvoke(): SimWorkload = SimFlopsWorkload(1000) + }) + val deployer = SimFunctionDeployer(clock, this, machineModel) { workload } + val service = ServerlessService(coroutineContext, clock, deployer, RandomRoutingPolicy()) + + val client = service.newClient() + + val function = client.newFunction("test") + function.invoke() + delay(2000) + + service.close() + + yield() + + assertAll( + { verify { workload.onStart() } }, + { verify { workload.onInvoke() } }, + { verify { workload.onStop() } } + ) + } +} |
