summaryrefslogtreecommitdiff
path: root/opendc-compute/opendc-compute-service
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2021-04-25 16:01:14 +0200
committerFabian Mastenbroek <mail.fabianm@gmail.com>2021-04-25 16:01:14 +0200
commitcd0b45627f0d8da8c8dc4edde223f3c36e9bcfbf (patch)
tree6ae1681630a0e270c23804e6dbb3bd414ebe5d6e /opendc-compute/opendc-compute-service
parent128a1db017545597a5c035b7960eb3fd36b5f987 (diff)
build: Migrate to flat project structure
This change updates the project structure to become flattened. Previously, the simulator, frontend and API each lived into their own directory. With this change, all modules of the project live in the top-level directory of the repository. This should improve discoverability of modules of the project.
Diffstat (limited to 'opendc-compute/opendc-compute-service')
-rw-r--r--opendc-compute/opendc-compute-service/build.gradle.kts41
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/ComputeService.kt85
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/Host.kt103
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostListener.kt41
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostModel.kt31
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostState.kt38
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt68
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt61
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt113
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt500
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt44
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt66
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt56
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt153
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ComputeScheduler.kt50
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/FilterScheduler.kt66
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayScheduler.kt64
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeCapabilitiesFilter.kt40
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeFilter.kt38
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/HostFilter.kt38
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/InstanceCountFilter.kt39
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/CoreMemoryWeigher.kt37
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/HostWeigher.kt37
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/InstanceCountWeigher.kt37
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/MemoryWeigher.kt37
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/ProvisionedCoresWeigher.kt37
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/RandomWeigher.kt36
-rw-r--r--opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt391
-rw-r--r--opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt80
-rw-r--r--opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt81
-rw-r--r--opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt285
-rw-r--r--opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml38
32 files changed, 2831 insertions, 0 deletions
diff --git a/opendc-compute/opendc-compute-service/build.gradle.kts b/opendc-compute/opendc-compute-service/build.gradle.kts
new file mode 100644
index 00000000..909e2dcd
--- /dev/null
+++ b/opendc-compute/opendc-compute-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 = "OpenDC Compute Service implementation"
+
+/* Build configuration */
+plugins {
+ `kotlin-library-conventions`
+ `testing-conventions`
+ `jacoco-conventions`
+}
+
+dependencies {
+ api(platform(project(":opendc-platform")))
+ api(project(":opendc-compute:opendc-compute-api"))
+ api(project(":opendc-telemetry:opendc-telemetry-api"))
+ 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/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/ComputeService.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/ComputeService.kt
new file mode 100644
index 00000000..1873eb99
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/ComputeService.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.compute.service
+
+import io.opentelemetry.api.metrics.Meter
+import org.opendc.compute.api.ComputeClient
+import org.opendc.compute.service.driver.Host
+import org.opendc.compute.service.internal.ComputeServiceImpl
+import org.opendc.compute.service.scheduler.ComputeScheduler
+import java.time.Clock
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * The [ComputeService] hosts the API implementation of the OpenDC Compute service.
+ */
+public interface ComputeService : AutoCloseable {
+ /**
+ * The hosts that are used by the compute service.
+ */
+ public val hosts: Set<Host>
+
+ /**
+ * The number of hosts available in the system.
+ */
+ public val hostCount: Int
+
+ /**
+ * Create a new [ComputeClient] to control the compute service.
+ */
+ public fun newClient(): ComputeClient
+
+ /**
+ * Add a [host] to the scheduling pool of the compute service.
+ */
+ public fun addHost(host: Host)
+
+ /**
+ * Remove a [host] from the scheduling pool of the compute service.
+ */
+ public fun removeHost(host: Host)
+
+ /**
+ * Terminate the lifecycle of the compute service, stopping all running instances.
+ */
+ public override fun close()
+
+ public companion object {
+ /**
+ * Construct a new [ComputeService] implementation.
+ *
+ * @param context The [CoroutineContext] to use in the service.
+ * @param clock The clock instance to use.
+ * @param scheduler The scheduler implementation to use.
+ */
+ public operator fun invoke(
+ context: CoroutineContext,
+ clock: Clock,
+ meter: Meter,
+ scheduler: ComputeScheduler,
+ schedulingQuantum: Long = 300000,
+ ): ComputeService {
+ return ComputeServiceImpl(context, clock, meter, scheduler, schedulingQuantum)
+ }
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/Host.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/Host.kt
new file mode 100644
index 00000000..bed15dfd
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/Host.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.compute.service.driver
+
+import org.opendc.compute.api.Server
+import java.util.*
+
+/**
+ * Base interface for representing compute resources that host virtualized [Server] instances.
+ */
+public interface Host {
+ /**
+ * A unique identifier representing the host.
+ */
+ public val uid: UUID
+
+ /**
+ * The name of this host.
+ */
+ public val name: String
+
+ /**
+ * The machine model of the host.
+ */
+ public val model: HostModel
+
+ /**
+ * The state of the host.
+ */
+ public val state: HostState
+
+ /**
+ * Meta-data associated with the host.
+ */
+ public val meta: Map<String, Any>
+
+ /**
+ * Determine whether the specified [instance][server] can still fit on this host.
+ */
+ public fun canFit(server: Server): Boolean
+
+ /**
+ * Register the specified [instance][server] on the host.
+ *
+ * Once the method returns, the instance should be running if [start] is true or else the instance should be
+ * stopped.
+ */
+ public suspend fun spawn(server: Server, start: Boolean = true)
+
+ /**
+ * Determine whether the specified [instance][server] exists on the host.
+ */
+ public operator fun contains(server: Server): Boolean
+
+ /**
+ * Start the server [instance][server] if it is currently not running on this host.
+ *
+ * @throws IllegalArgumentException if the server is not present on the host.
+ */
+ public suspend fun start(server: Server)
+
+ /**
+ * Stop the server [instance][server] if it is currently running on this host.
+ *
+ * @throws IllegalArgumentException if the server is not present on the host.
+ */
+ public suspend fun stop(server: Server)
+
+ /**
+ * Delete the specified [instance][server] on this host and cleanup all resources associated with it.
+ */
+ public suspend fun delete(server: Server)
+
+ /**
+ * Add a [HostListener] to this host.
+ */
+ public fun addListener(listener: HostListener)
+
+ /**
+ * Remove a [HostListener] from this host.
+ */
+ public fun removeListener(listener: HostListener)
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostListener.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostListener.kt
new file mode 100644
index 00000000..f076cae3
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostListener.kt
@@ -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.
+ */
+
+package org.opendc.compute.service.driver
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.api.ServerState
+
+/**
+ * Listener interface for events originating from a [Host].
+ */
+public interface HostListener {
+ /**
+ * This method is invoked when the state of an [instance][server] on [host] changes.
+ */
+ public fun onStateChanged(host: Host, server: Server, newState: ServerState) {}
+
+ /**
+ * This method is invoked when the state of a [Host] has changed.
+ */
+ public fun onStateChanged(host: Host, newState: HostState) {}
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostModel.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostModel.kt
new file mode 100644
index 00000000..5632a55e
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostModel.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.compute.service.driver
+
+/**
+ * Describes the static machine properties of the host.
+ *
+ * @property vcpuCount The number of logical processing cores available for this host.
+ * @property memorySize The amount of memory available for this host in MB.
+ */
+public data class HostModel(public val cpuCount: Int, public val memorySize: Long)
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostState.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostState.kt
new file mode 100644
index 00000000..6d85ee2d
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/driver/HostState.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.compute.service.driver
+
+/**
+ * The state of a host.
+ */
+public enum class HostState {
+ /**
+ * The host is up.
+ */
+ UP,
+
+ /**
+ * The host is down.
+ */
+ DOWN
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt
new file mode 100644
index 00000000..4a8d3046
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.compute.service.internal
+
+import org.opendc.compute.api.Flavor
+import java.util.UUID
+
+/**
+ * A [Flavor] implementation that is passed to clients but delegates its implementation to another class.
+ */
+internal class ClientFlavor(private val delegate: Flavor) : Flavor {
+ override val uid: UUID = delegate.uid
+
+ override var name: String = delegate.name
+ private set
+
+ override var cpuCount: Int = delegate.cpuCount
+ private set
+
+ override var memorySize: Long = delegate.memorySize
+ 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 refresh() {
+ delegate.refresh()
+
+ name = delegate.name
+ cpuCount = delegate.cpuCount
+ memorySize = delegate.memorySize
+ labels = delegate.labels
+ meta = delegate.meta
+ }
+
+ override fun equals(other: Any?): Boolean = other is Flavor && other.uid == uid
+
+ override fun hashCode(): Int = uid.hashCode()
+
+ override fun toString(): String = "Flavor[uid=$uid,name=$name]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt
new file mode 100644
index 00000000..e0b5c171
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.compute.service.internal
+
+import org.opendc.compute.api.Image
+import java.util.*
+
+/**
+ * An [Image] implementation that is passed to clients but delegates its implementation to another class.
+ */
+internal class ClientImage(private val delegate: Image) : Image {
+ 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()
+ refresh()
+ }
+
+ override suspend fun refresh() {
+ delegate.refresh()
+
+ name = delegate.name
+ labels = delegate.labels
+ meta = delegate.meta
+ }
+
+ override fun equals(other: Any?): Boolean = other is Image && other.uid == uid
+
+ override fun hashCode(): Int = uid.hashCode()
+
+ override fun toString(): String = "Image[uid=$uid,name=$name]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt
new file mode 100644
index 00000000..f2929bf3
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.compute.service.internal
+
+import org.opendc.compute.api.Flavor
+import org.opendc.compute.api.Image
+import org.opendc.compute.api.Server
+import org.opendc.compute.api.ServerState
+import org.opendc.compute.api.ServerWatcher
+import java.util.*
+
+/**
+ * A [Server] implementation that is passed to clients but delegates its implementation to another class.
+ */
+internal class ClientServer(private val delegate: Server) : Server, ServerWatcher {
+ private val watchers = mutableListOf<ServerWatcher>()
+
+ override val uid: UUID = delegate.uid
+
+ override var name: String = delegate.name
+ private set
+
+ override var flavor: Flavor = delegate.flavor
+ private set
+
+ override var image: Image = delegate.image
+ 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 var state: ServerState = delegate.state
+ private set
+
+ override suspend fun start() {
+ delegate.start()
+ refresh()
+ }
+
+ override suspend fun stop() {
+ delegate.stop()
+ refresh()
+ }
+
+ override suspend fun delete() {
+ delegate.delete()
+ refresh()
+ }
+
+ override fun watch(watcher: ServerWatcher) {
+ if (watchers.isEmpty()) {
+ delegate.watch(this)
+ }
+
+ watchers += watcher
+ }
+
+ override fun unwatch(watcher: ServerWatcher) {
+ watchers += watcher
+
+ if (watchers.isEmpty()) {
+ delegate.unwatch(this)
+ }
+ }
+
+ override suspend fun refresh() {
+ delegate.refresh()
+
+ name = delegate.name
+ flavor = delegate.flavor
+ image = delegate.image
+ labels = delegate.labels
+ meta = delegate.meta
+ state = delegate.state
+ }
+
+ override fun onStateChanged(server: Server, newState: ServerState) {
+ val watchers = watchers
+
+ for (watcher in watchers) {
+ watcher.onStateChanged(this, newState)
+ }
+ }
+
+ override fun equals(other: Any?): Boolean = other is Server && other.uid == uid
+
+ override fun hashCode(): Int = uid.hashCode()
+
+ override fun toString(): String = "Server[uid=$uid,name=$name,state=$state]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt
new file mode 100644
index 00000000..8af5f86e
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt
@@ -0,0 +1,500 @@
+/*
+ * 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.compute.service.internal
+
+import io.opentelemetry.api.metrics.Meter
+import kotlinx.coroutines.*
+import mu.KotlinLogging
+import org.opendc.compute.api.*
+import org.opendc.compute.service.ComputeService
+import org.opendc.compute.service.driver.Host
+import org.opendc.compute.service.driver.HostListener
+import org.opendc.compute.service.driver.HostState
+import org.opendc.compute.service.scheduler.ComputeScheduler
+import org.opendc.utils.TimerScheduler
+import java.time.Clock
+import java.util.*
+import kotlin.coroutines.CoroutineContext
+import kotlin.math.max
+
+/**
+ * Internal implementation of the OpenDC Compute service.
+ *
+ * @param context The [CoroutineContext] to use.
+ * @param clock The clock instance to keep track of time.
+ */
+internal class ComputeServiceImpl(
+ private val context: CoroutineContext,
+ private val clock: Clock,
+ private val meter: Meter,
+ private val scheduler: ComputeScheduler,
+ private val schedulingQuantum: Long
+) : ComputeService, HostListener {
+ /**
+ * 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 [Random] instance used to generate unique identifiers for the objects.
+ */
+ private val random = Random(0)
+
+ /**
+ * A mapping from host to host view.
+ */
+ private val hostToView = mutableMapOf<Host, HostView>()
+
+ /**
+ * The available hypervisors.
+ */
+ private val availableHosts: MutableSet<HostView> = mutableSetOf()
+
+ /**
+ * The servers that should be launched by the service.
+ */
+ private val queue: Deque<SchedulingRequest> = ArrayDeque()
+
+ /**
+ * The active servers in the system.
+ */
+ private val activeServers: MutableMap<Server, Host> = mutableMapOf()
+
+ /**
+ * The registered flavors for this compute service.
+ */
+ internal val flavors = mutableMapOf<UUID, InternalFlavor>()
+
+ /**
+ * The registered images for this compute service.
+ */
+ internal val images = mutableMapOf<UUID, InternalImage>()
+
+ /**
+ * The registered servers for this compute service.
+ */
+ private val servers = mutableMapOf<UUID, InternalServer>()
+
+ private var maxCores = 0
+ private var maxMemory = 0L
+
+ /**
+ * The number of servers that have been submitted to the service for provisioning.
+ */
+ private val _submittedServers = meter.longCounterBuilder("servers.submitted")
+ .setDescription("Number of start requests")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The number of servers that failed to be scheduled.
+ */
+ private val _unscheduledServers = meter.longCounterBuilder("servers.unscheduled")
+ .setDescription("Number of unscheduled servers")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The number of servers that are waiting to be provisioned.
+ */
+ private val _waitingServers = meter.longUpDownCounterBuilder("servers.waiting")
+ .setDescription("Number of servers waiting to be provisioned")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The number of servers that are waiting to be provisioned.
+ */
+ private val _runningServers = meter.longUpDownCounterBuilder("servers.active")
+ .setDescription("Number of servers currently running")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The number of servers that have finished running.
+ */
+ private val _finishedServers = meter.longCounterBuilder("servers.finished")
+ .setDescription("Number of servers that finished running")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The number of hosts registered at the compute service.
+ */
+ private val _hostCount = meter.longUpDownCounterBuilder("hosts.total")
+ .setDescription("Number of hosts")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The number of available hosts registered at the compute service.
+ */
+ private val _availableHostCount = meter.longUpDownCounterBuilder("hosts.available")
+ .setDescription("Number of available hosts")
+ .setUnit("1")
+ .build()
+
+ /**
+ * The [TimerScheduler] to use for scheduling the scheduler cycles.
+ */
+ private var timerScheduler: TimerScheduler<Unit> = TimerScheduler(scope.coroutineContext, clock)
+
+ override val hosts: Set<Host>
+ get() = hostToView.keys
+
+ override val hostCount: Int
+ get() = hostToView.size
+
+ override fun newClient(): ComputeClient {
+ check(scope.isActive) { "Service is already closed" }
+ return object : ComputeClient {
+ private var isClosed: Boolean = false
+
+ override suspend fun queryFlavors(): List<Flavor> {
+ check(!isClosed) { "Client is already closed" }
+
+ return flavors.values.map { ClientFlavor(it) }
+ }
+
+ override suspend fun findFlavor(id: UUID): Flavor? {
+ check(!isClosed) { "Client is already closed" }
+
+ return flavors[id]?.let { ClientFlavor(it) }
+ }
+
+ override suspend fun newFlavor(
+ name: String,
+ cpuCount: Int,
+ memorySize: Long,
+ labels: Map<String, String>,
+ meta: Map<String, Any>
+ ): Flavor {
+ check(!isClosed) { "Client is already closed" }
+
+ val uid = UUID(clock.millis(), random.nextLong())
+ val flavor = InternalFlavor(
+ this@ComputeServiceImpl,
+ uid,
+ name,
+ cpuCount,
+ memorySize,
+ labels,
+ meta
+ )
+
+ flavors[uid] = flavor
+
+ return ClientFlavor(flavor)
+ }
+
+ override suspend fun queryImages(): List<Image> {
+ check(!isClosed) { "Client is already closed" }
+
+ return images.values.map { ClientImage(it) }
+ }
+
+ override suspend fun findImage(id: UUID): Image? {
+ check(!isClosed) { "Client is already closed" }
+
+ return images[id]?.let { ClientImage(it) }
+ }
+
+ override suspend fun newImage(name: String, labels: Map<String, String>, meta: Map<String, Any>): Image {
+ check(!isClosed) { "Client is already closed" }
+
+ val uid = UUID(clock.millis(), random.nextLong())
+ val image = InternalImage(this@ComputeServiceImpl, uid, name, labels, meta)
+
+ images[uid] = image
+
+ return ClientImage(image)
+ }
+
+ override suspend fun newServer(
+ name: String,
+ image: Image,
+ flavor: Flavor,
+ labels: Map<String, String>,
+ meta: Map<String, Any>,
+ start: Boolean
+ ): Server {
+ check(!isClosed) { "Client is closed" }
+
+ val uid = UUID(clock.millis(), random.nextLong())
+ val server = InternalServer(
+ this@ComputeServiceImpl,
+ uid,
+ name,
+ requireNotNull(flavors[flavor.uid]) { "Unknown flavor" },
+ requireNotNull(images[image.uid]) { "Unknown image" },
+ labels.toMutableMap(),
+ meta.toMutableMap()
+ )
+
+ servers[uid] = server
+
+ if (start) {
+ server.start()
+ }
+
+ return ClientServer(server)
+ }
+
+ override suspend fun findServer(id: UUID): Server? {
+ check(!isClosed) { "Client is already closed" }
+
+ return servers[id]?.let { ClientServer(it) }
+ }
+
+ override suspend fun queryServers(): List<Server> {
+ check(!isClosed) { "Client is already closed" }
+
+ return servers.values.map { ClientServer(it) }
+ }
+
+ override fun close() {
+ isClosed = true
+ }
+
+ override fun toString(): String = "ComputeClient"
+ }
+ }
+
+ override fun addHost(host: Host) {
+ // Check if host is already known
+ if (host in hostToView) {
+ return
+ }
+
+ val hv = HostView(host)
+ maxCores = max(maxCores, host.model.cpuCount)
+ maxMemory = max(maxMemory, host.model.memorySize)
+ hostToView[host] = hv
+
+ if (host.state == HostState.UP) {
+ _availableHostCount.add(1)
+ availableHosts += hv
+ }
+
+ scheduler.addHost(hv)
+ _hostCount.add(1)
+ host.addListener(this)
+ }
+
+ override fun removeHost(host: Host) {
+ val view = hostToView.remove(host)
+ if (view != null) {
+ if (availableHosts.remove(view)) {
+ _availableHostCount.add(-1)
+ }
+ scheduler.removeHost(view)
+ host.removeListener(this)
+ _hostCount.add(-1)
+ }
+ }
+
+ override fun close() {
+ scope.cancel()
+ }
+
+ internal fun schedule(server: InternalServer): SchedulingRequest {
+ logger.debug { "Enqueueing server ${server.uid} to be assigned to host." }
+
+ val request = SchedulingRequest(server)
+ queue.add(request)
+ _submittedServers.add(1)
+ _waitingServers.add(1)
+ requestSchedulingCycle()
+ return request
+ }
+
+ internal fun delete(flavor: InternalFlavor) {
+ flavors.remove(flavor.uid)
+ }
+
+ internal fun delete(image: InternalImage) {
+ images.remove(image.uid)
+ }
+
+ internal fun delete(server: InternalServer) {
+ servers.remove(server.uid)
+ }
+
+ /**
+ * Indicate that a new scheduling cycle is needed due to a change to the service's state.
+ */
+ private fun requestSchedulingCycle() {
+ // Bail out in case we have already requested a new cycle or the queue is empty.
+ if (timerScheduler.isTimerActive(Unit) || queue.isEmpty()) {
+ return
+ }
+
+ // 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 = schedulingQuantum - (clock.millis() % schedulingQuantum)
+
+ timerScheduler.startSingleTimer(Unit, delay) {
+ doSchedule()
+ }
+ }
+
+ /**
+ * Run a single scheduling iteration.
+ */
+ private fun doSchedule() {
+ while (queue.isNotEmpty()) {
+ val request = queue.peek()
+
+ if (request.isCancelled) {
+ queue.poll()
+ _waitingServers.add(-1)
+ continue
+ }
+
+ val server = request.server
+ val hv = scheduler.select(request.server)
+ if (hv == null || !hv.host.canFit(server)) {
+ logger.trace { "Server $server selected for scheduling but no capacity available for it at the moment" }
+
+ if (server.flavor.memorySize > maxMemory || server.flavor.cpuCount > maxCores) {
+ // Remove the incoming image
+ queue.poll()
+ _waitingServers.add(-1)
+ _unscheduledServers.add(1)
+
+ logger.warn("Failed to spawn $server: does not fit [${clock.millis()}]")
+
+ server.state = ServerState.ERROR
+ continue
+ } else {
+ break
+ }
+ }
+
+ val host = hv.host
+
+ // Remove request from queue
+ queue.poll()
+ _waitingServers.add(-1)
+
+ logger.info { "Assigned server $server to host $host." }
+
+ // Speculatively update the hypervisor view information to prevent other images in the queue from
+ // deciding on stale values.
+ hv.instanceCount++
+ hv.provisionedCores += server.flavor.cpuCount
+ hv.availableMemory -= server.flavor.memorySize // XXX Temporary hack
+
+ scope.launch {
+ try {
+ server.host = host
+ host.spawn(server)
+ activeServers[server] = host
+ } catch (e: Throwable) {
+ logger.error("Failed to deploy VM", e)
+
+ hv.instanceCount--
+ hv.provisionedCores -= server.flavor.cpuCount
+ hv.availableMemory += server.flavor.memorySize
+ }
+ }
+ }
+ }
+
+ /**
+ * A request to schedule an [InternalServer] onto one of the [Host]s.
+ */
+ internal data class SchedulingRequest(val server: InternalServer) {
+ /**
+ * A flag to indicate that the request is cancelled.
+ */
+ var isCancelled: Boolean = false
+ }
+
+ override fun onStateChanged(host: Host, newState: HostState) {
+ when (newState) {
+ HostState.UP -> {
+ logger.debug { "[${clock.millis()}] Host ${host.uid} state changed: $newState" }
+
+ val hv = hostToView[host]
+ if (hv != null) {
+ // Corner case for when the hypervisor already exists
+ availableHosts += hv
+ _availableHostCount.add(1)
+ }
+
+ // Re-schedule on the new machine
+ requestSchedulingCycle()
+ }
+ HostState.DOWN -> {
+ logger.debug { "[${clock.millis()}] Host ${host.uid} state changed: $newState" }
+
+ val hv = hostToView[host] ?: return
+ availableHosts -= hv
+ _availableHostCount.add(-1)
+
+ requestSchedulingCycle()
+ }
+ }
+ }
+
+ override fun onStateChanged(host: Host, server: Server, newState: ServerState) {
+ require(server is InternalServer) { "Invalid server type passed to service" }
+
+ if (server.host != host) {
+ // This can happen when a server is rescheduled and started on another machine, while being deleted from
+ // the old machine.
+ return
+ }
+
+ server.state = newState
+
+ if (newState == ServerState.RUNNING) {
+ _runningServers.add(1)
+ } else if (newState == ServerState.TERMINATED || newState == ServerState.DELETED) {
+ logger.info { "[${clock.millis()}] Server ${server.uid} ${server.name} ${server.flavor} finished." }
+
+ activeServers -= server
+ _runningServers.add(-1)
+ _finishedServers.add(1)
+
+ val hv = hostToView[host]
+ if (hv != null) {
+ hv.provisionedCores -= server.flavor.cpuCount
+ hv.instanceCount--
+ hv.availableMemory += server.flavor.memorySize
+ } else {
+ logger.error { "Unknown host $host" }
+ }
+
+ // Try to reschedule if needed
+ requestSchedulingCycle()
+ }
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt
new file mode 100644
index 00000000..e2f33f11
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2020 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.compute.service.internal
+
+import org.opendc.compute.service.ComputeService
+import org.opendc.compute.service.driver.Host
+import java.util.UUID
+
+/**
+ * A view of a [Host] as seen from the [ComputeService]
+ */
+public class HostView(public val host: Host) {
+ /**
+ * The unique identifier of the host.
+ */
+ public val uid: UUID
+ get() = host.uid
+
+ public var instanceCount: Int = 0
+ public var availableMemory: Long = host.model.memorySize
+ public var provisionedCores: Int = 0
+
+ override fun toString(): String = "HostView[host=$host]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt
new file mode 100644
index 00000000..b8fb6279
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.compute.service.internal
+
+import org.opendc.compute.api.Flavor
+import java.util.*
+
+/**
+ * Internal stateful representation of a [Flavor].
+ */
+internal class InternalFlavor(
+ private val service: ComputeServiceImpl,
+ override val uid: UUID,
+ name: String,
+ cpuCount: Int,
+ memorySize: Long,
+ labels: Map<String, String>,
+ meta: Map<String, Any>
+) : Flavor {
+ override var name: String = name
+ private set
+
+ override var cpuCount: Int = cpuCount
+ private set
+
+ override var memorySize: Long = memorySize
+ 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 delete() {
+ service.delete(this)
+ }
+
+ override fun equals(other: Any?): Boolean = other is Flavor && uid == other.uid
+
+ override fun hashCode(): Int = uid.hashCode()
+
+ override fun toString(): String = "Flavor[uid=$uid,name=$name]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt
new file mode 100644
index 00000000..d9ed5896
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.compute.service.internal
+
+import org.opendc.compute.api.Image
+import java.util.*
+
+/**
+ * Internal stateful representation of an [Image].
+ */
+internal class InternalImage(
+ private val service: ComputeServiceImpl,
+ override val uid: UUID,
+ override val name: String,
+ labels: Map<String, String>,
+ meta: Map<String, Any>
+) : Image {
+
+ 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 delete() {
+ service.delete(this)
+ }
+
+ override fun equals(other: Any?): Boolean = other is Image && uid == other.uid
+
+ override fun hashCode(): Int = uid.hashCode()
+
+ override fun toString(): String = "Image[uid=$uid,name=$name]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt
new file mode 100644
index 00000000..d9d0f3fc
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.compute.service.internal
+
+import mu.KotlinLogging
+import org.opendc.compute.api.*
+import org.opendc.compute.service.driver.Host
+import java.util.UUID
+
+/**
+ * Internal implementation of the [Server] interface.
+ */
+internal class InternalServer(
+ private val service: ComputeServiceImpl,
+ override val uid: UUID,
+ override val name: String,
+ override val flavor: InternalFlavor,
+ override val image: InternalImage,
+ override val labels: MutableMap<String, String>,
+ override val meta: MutableMap<String, Any>
+) : Server {
+ /**
+ * The logger instance of this server.
+ */
+ private val logger = KotlinLogging.logger {}
+
+ /**
+ * The watchers of this server object.
+ */
+ private val watchers = mutableListOf<ServerWatcher>()
+
+ /**
+ * The [Host] that has been assigned to host the server.
+ */
+ internal var host: Host? = null
+
+ /**
+ * The current scheduling request.
+ */
+ private var request: ComputeServiceImpl.SchedulingRequest? = null
+
+ override suspend fun start() {
+ when (state) {
+ ServerState.RUNNING -> {
+ logger.debug { "User tried to start server but server is already running" }
+ return
+ }
+ ServerState.PROVISIONING -> {
+ logger.debug { "User tried to start server but request is already pending: doing nothing" }
+ return
+ }
+ ServerState.DELETED -> {
+ logger.warn { "User tried to start terminated server" }
+ throw IllegalStateException("Server is terminated")
+ }
+ else -> {
+ logger.info { "User requested to start server $uid" }
+ state = ServerState.PROVISIONING
+ assert(request == null) { "Scheduling request already active" }
+ request = service.schedule(this)
+ }
+ }
+ }
+
+ override suspend fun stop() {
+ when (state) {
+ ServerState.PROVISIONING -> {
+ cancelProvisioningRequest()
+ state = ServerState.TERMINATED
+ }
+ ServerState.RUNNING, ServerState.ERROR -> {
+ val host = checkNotNull(host) { "Server not running" }
+ host.stop(this)
+ }
+ ServerState.TERMINATED, ServerState.DELETED -> {} // No work needed
+ }
+ }
+
+ override suspend fun delete() {
+ when (state) {
+ ServerState.PROVISIONING, ServerState.TERMINATED -> {
+ cancelProvisioningRequest()
+ service.delete(this)
+ state = ServerState.DELETED
+ }
+ ServerState.RUNNING, ServerState.ERROR -> {
+ val host = checkNotNull(host) { "Server not running" }
+ host.delete(this)
+ service.delete(this)
+ state = ServerState.DELETED
+ }
+ else -> {} // No work needed
+ }
+ }
+
+ override fun watch(watcher: ServerWatcher) {
+ watchers += watcher
+ }
+
+ override fun unwatch(watcher: ServerWatcher) {
+ watchers -= watcher
+ }
+
+ override suspend fun refresh() {
+ // No-op: this object is the source-of-truth
+ }
+
+ override var state: ServerState = ServerState.TERMINATED
+ set(value) {
+ if (value != field) {
+ watchers.forEach { it.onStateChanged(this, value) }
+ }
+
+ field = value
+ }
+
+ /**
+ * Cancel the provisioning request if active.
+ */
+ private fun cancelProvisioningRequest() {
+ val request = request
+ if (request != null) {
+ this.request = null
+ request.isCancelled = true
+ }
+ }
+
+ override fun equals(other: Any?): Boolean = other is Server && uid == other.uid
+
+ override fun hashCode(): Int = uid.hashCode()
+
+ override fun toString(): String = "Server[uid=$uid,state=$state]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ComputeScheduler.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ComputeScheduler.kt
new file mode 100644
index 00000000..a2ab3a2e
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ComputeScheduler.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.compute.service.scheduler
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.ComputeService
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A generic scheduler interface used by the [ComputeService] to select hosts to place [Server]s on.
+ */
+public interface ComputeScheduler {
+ /**
+ * Register the specified [host] to be used for scheduling.
+ */
+ public fun addHost(host: HostView)
+
+ /**
+ * Remove the specified [host] to be removed from the scheduling pool.
+ */
+ public fun removeHost(host: HostView)
+
+ /**
+ * Select a host for the specified [server].
+ *
+ * @param server The server to select a host for.
+ * @return The host to schedule the server on or `null` if no server is available.
+ */
+ public fun select(server: Server): HostView?
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/FilterScheduler.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/FilterScheduler.kt
new file mode 100644
index 00000000..0fd5b2a4
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/FilterScheduler.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.compute.service.scheduler
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+import org.opendc.compute.service.scheduler.filters.HostFilter
+import org.opendc.compute.service.scheduler.weights.HostWeigher
+
+/**
+ * A [ComputeScheduler] implementation that uses filtering and weighing passes to select
+ * the host to schedule a [Server] on.
+ *
+ * This implementation is based on the filter scheduler from OpenStack Nova.
+ * See: https://docs.openstack.org/nova/latest/user/filter-scheduler.html
+ */
+public class FilterScheduler(private val filters: List<HostFilter>, private val weighers: List<Pair<HostWeigher, Double>>) : ComputeScheduler {
+ /**
+ * The pool of hosts available to the scheduler.
+ */
+ private val hosts = mutableListOf<HostView>()
+
+ override fun addHost(host: HostView) {
+ hosts.add(host)
+ }
+
+ override fun removeHost(host: HostView) {
+ hosts.remove(host)
+ }
+
+ override fun select(server: Server): HostView? {
+ return hosts.asSequence()
+ .filter { host ->
+ for (filter in filters) {
+ if (!filter.test(host, server))
+ return@filter false
+ }
+
+ true
+ }
+ .sortedByDescending { host ->
+ weighers.sumByDouble { (weigher, factor) -> weigher.getWeight(host, server) * factor }
+ }
+ .firstOrNull()
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayScheduler.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayScheduler.kt
new file mode 100644
index 00000000..284c1f91
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayScheduler.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2020 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.compute.service.scheduler
+
+import mu.KotlinLogging
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * Policy replaying VM-cluster assignment.
+ *
+ * Within each cluster, the active servers on each node determine which node gets
+ * assigned the VM image.
+ */
+public class ReplayScheduler(private val vmPlacements: Map<String, String>) : ComputeScheduler {
+ private val logger = KotlinLogging.logger {}
+
+ /**
+ * The pool of hosts available to the scheduler.
+ */
+ private val hosts = mutableListOf<HostView>()
+
+ override fun addHost(host: HostView) {
+ hosts.add(host)
+ }
+
+ override fun removeHost(host: HostView) {
+ hosts.remove(host)
+ }
+
+ override fun select(server: Server): HostView? {
+ val clusterName = vmPlacements[server.name]
+ ?: throw IllegalStateException("Could not find placement data in VM placement file for VM ${server.name}")
+ val machinesInCluster = hosts.filter { it.host.name.contains(clusterName) }
+
+ if (machinesInCluster.isEmpty()) {
+ logger.info { "Could not find any machines belonging to cluster $clusterName for image ${server.name}, assigning randomly." }
+ return hosts.maxByOrNull { it.availableMemory }
+ }
+
+ return machinesInCluster.maxByOrNull { it.availableMemory }
+ ?: throw IllegalStateException("Cloud not find any machine and could not randomly assign")
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeCapabilitiesFilter.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeCapabilitiesFilter.kt
new file mode 100644
index 00000000..072440c5
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeCapabilitiesFilter.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.compute.service.scheduler.filters
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostFilter] that checks whether the capabilities provided by the host satisfies the requirements of the server
+ * flavor.
+ */
+public class ComputeCapabilitiesFilter : HostFilter {
+ override fun test(host: HostView, server: Server): Boolean {
+ val fitsMemory = host.availableMemory >= server.flavor.memorySize
+ val fitsCpu = host.host.model.cpuCount >= server.flavor.cpuCount
+ return fitsMemory && fitsCpu
+ }
+
+ override fun toString(): String = "ComputeCapabilitiesFilter"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeFilter.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeFilter.kt
new file mode 100644
index 00000000..fb842415
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/ComputeFilter.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.compute.service.scheduler.filters
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.driver.HostState
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostFilter] that filters on active hosts.
+ */
+public class ComputeFilter : HostFilter {
+ override fun test(host: HostView, server: Server): Boolean {
+ return host.host.state == HostState.UP
+ }
+
+ override fun toString(): String = "ComputeFilter"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/HostFilter.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/HostFilter.kt
new file mode 100644
index 00000000..9e909ca6
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/HostFilter.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.compute.service.scheduler.filters
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+import org.opendc.compute.service.scheduler.FilterScheduler
+
+/**
+ * A filter used by the [FilterScheduler] to filter hosts.
+ */
+public fun interface HostFilter {
+ /**
+ * Test whether the specified [host] should be included in the selection
+ * for scheduling the specified [server].
+ */
+ public fun test(host: HostView, server: Server): Boolean
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/InstanceCountFilter.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/InstanceCountFilter.kt
new file mode 100644
index 00000000..ed6674b1
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/filters/InstanceCountFilter.kt
@@ -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.
+ */
+
+package org.opendc.compute.service.scheduler.filters
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostFilter] that filters hosts based on the number of instances on the host.
+ *
+ * @param limit The maximum number of instances on the host.
+ */
+public class InstanceCountFilter(private val limit: Int) : HostFilter {
+ override fun test(host: HostView, server: Server): Boolean {
+ return host.instanceCount < limit
+ }
+
+ override fun toString(): String = "InstanceCountFilter[limit=$limit]"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/CoreMemoryWeigher.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/CoreMemoryWeigher.kt
new file mode 100644
index 00000000..12e6510e
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/CoreMemoryWeigher.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.compute.service.scheduler.weights
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostWeigher] that weighs the hosts based on the available memory per core on the host.
+ */
+public class CoreMemoryWeigher : HostWeigher {
+ override fun getWeight(host: HostView, server: Server): Double {
+ return host.availableMemory.toDouble() / host.host.model.cpuCount
+ }
+
+ override fun toString(): String = "CoreMemoryWeigher"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/HostWeigher.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/HostWeigher.kt
new file mode 100644
index 00000000..d48ee9e0
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/HostWeigher.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.compute.service.scheduler.weights
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+import org.opendc.compute.service.scheduler.FilterScheduler
+
+/**
+ * An interface used by the [FilterScheduler] to weigh the pool of host for a scheduling request.
+ */
+public fun interface HostWeigher {
+ /**
+ * Obtain the weight of the specified [host] when scheduling the specified [server].
+ */
+ public fun getWeight(host: HostView, server: Server): Double
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/InstanceCountWeigher.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/InstanceCountWeigher.kt
new file mode 100644
index 00000000..2ef733e5
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/InstanceCountWeigher.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.compute.service.scheduler.weights
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostWeigher] that weighs the hosts based on the number of instances on the host.
+ */
+public class InstanceCountWeigher : HostWeigher {
+ override fun getWeight(host: HostView, server: Server): Double {
+ return host.instanceCount.toDouble()
+ }
+
+ override fun toString(): String = "InstanceCountWeigher"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/MemoryWeigher.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/MemoryWeigher.kt
new file mode 100644
index 00000000..115d8e4d
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/MemoryWeigher.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.compute.service.scheduler.weights
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostWeigher] that weighs the hosts based on the available memory on the host.
+ */
+public class MemoryWeigher : HostWeigher {
+ override fun getWeight(host: HostView, server: Server): Double {
+ return host.availableMemory.toDouble()
+ }
+
+ override fun toString(): String = "MemoryWeigher"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/ProvisionedCoresWeigher.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/ProvisionedCoresWeigher.kt
new file mode 100644
index 00000000..df5bcd6e
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/ProvisionedCoresWeigher.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.compute.service.scheduler.weights
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+
+/**
+ * A [HostWeigher] that weighs the hosts based on the number of provisioned cores on the host.
+ */
+public class ProvisionedCoresWeigher : HostWeigher {
+ override fun getWeight(host: HostView, server: Server): Double {
+ return host.provisionedCores.toDouble()
+ }
+
+ override fun toString(): String = "ProvisionedCoresWeigher"
+}
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/RandomWeigher.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/RandomWeigher.kt
new file mode 100644
index 00000000..1615df3a
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/weights/RandomWeigher.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.compute.service.scheduler.weights
+
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+import java.util.*
+
+/**
+ * A [HostWeigher] that assigns random weights to each host every selection.
+ */
+public class RandomWeigher(private val random: Random) : HostWeigher {
+ override fun getWeight(host: HostView, server: Server): Double = random.nextDouble()
+
+ override fun toString(): String = "RandomWeigher"
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt
new file mode 100644
index 00000000..a6258845
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt
@@ -0,0 +1,391 @@
+/*
+ * 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.compute.service
+
+import io.mockk.*
+import io.opentelemetry.api.metrics.MeterProvider
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.opendc.compute.api.*
+import org.opendc.compute.service.driver.Host
+import org.opendc.compute.service.driver.HostListener
+import org.opendc.compute.service.driver.HostModel
+import org.opendc.compute.service.driver.HostState
+import org.opendc.compute.service.scheduler.FilterScheduler
+import org.opendc.compute.service.scheduler.filters.ComputeCapabilitiesFilter
+import org.opendc.compute.service.scheduler.filters.ComputeFilter
+import org.opendc.compute.service.scheduler.weights.MemoryWeigher
+import org.opendc.simulator.core.SimulationCoroutineScope
+import org.opendc.simulator.core.runBlockingSimulation
+import java.util.*
+
+/**
+ * Test suite for the [ComputeService] interface.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class ComputeServiceTest {
+ lateinit var scope: SimulationCoroutineScope
+ lateinit var service: ComputeService
+
+ @BeforeEach
+ fun setUp() {
+ scope = SimulationCoroutineScope()
+ val clock = scope.clock
+ val computeScheduler = FilterScheduler(
+ filters = listOf(ComputeFilter(), ComputeCapabilitiesFilter()),
+ weighers = listOf(MemoryWeigher() to -1.0)
+ )
+ val meter = MeterProvider.noop().get("opendc-compute")
+ service = ComputeService(scope.coroutineContext, clock, meter, computeScheduler)
+ }
+
+ @Test
+ fun testClientClose() = scope.runBlockingSimulation {
+ val client = service.newClient()
+
+ assertEquals(emptyList<Flavor>(), client.queryFlavors())
+ assertEquals(emptyList<Image>(), client.queryImages())
+ assertEquals(emptyList<Server>(), client.queryServers())
+
+ client.close()
+
+ assertThrows<IllegalStateException> { client.queryFlavors() }
+ assertThrows<IllegalStateException> { client.queryImages() }
+ assertThrows<IllegalStateException> { client.queryServers() }
+
+ assertThrows<IllegalStateException> { client.findFlavor(UUID.randomUUID()) }
+ assertThrows<IllegalStateException> { client.findImage(UUID.randomUUID()) }
+ assertThrows<IllegalStateException> { client.findServer(UUID.randomUUID()) }
+
+ assertThrows<IllegalStateException> { client.newFlavor("test", 1, 2) }
+ assertThrows<IllegalStateException> { client.newImage("test") }
+ assertThrows<IllegalStateException> { client.newServer("test", mockk(), mockk()) }
+ }
+
+ @Test
+ fun testClientCreate() = scope.runBlockingSimulation {
+ val client = service.newClient()
+
+ val flavor = client.newFlavor("test", 1, 1024)
+ assertEquals(listOf(flavor), client.queryFlavors())
+ assertEquals(flavor, client.findFlavor(flavor.uid))
+ val image = client.newImage("test")
+ assertEquals(listOf(image), client.queryImages())
+ assertEquals(image, client.findImage(image.uid))
+ val server = client.newServer("test", image, flavor, start = false)
+ assertEquals(listOf(server), client.queryServers())
+ assertEquals(server, client.findServer(server.uid))
+
+ server.delete()
+ assertNull(client.findServer(server.uid))
+
+ image.delete()
+ assertNull(client.findImage(image.uid))
+
+ flavor.delete()
+ assertNull(client.findFlavor(flavor.uid))
+
+ assertThrows<IllegalStateException> { server.start() }
+ }
+
+ @Test
+ fun testClientOnClose() = scope.runBlockingSimulation {
+ service.close()
+ assertThrows<IllegalStateException> {
+ service.newClient()
+ }
+ }
+
+ @Test
+ fun testAddHost() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+
+ assertEquals(0, service.hostCount)
+ assertEquals(emptySet<Host>(), service.hosts)
+
+ service.addHost(host)
+
+ verify(exactly = 1) { host.addListener(any()) }
+
+ assertEquals(1, service.hostCount)
+ assertEquals(1, service.hosts.size)
+
+ service.removeHost(host)
+
+ verify(exactly = 1) { host.removeListener(any()) }
+ }
+
+ @Test
+ fun testAddHostDouble() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.DOWN
+
+ assertEquals(0, service.hostCount)
+ assertEquals(emptySet<Host>(), service.hosts)
+
+ service.addHost(host)
+ service.addHost(host)
+
+ verify(exactly = 1) { host.addListener(any()) }
+ }
+
+ @Test
+ fun testServerStartWithoutEnoughCpus() = scope.runBlockingSimulation {
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 0)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.ERROR, server.state)
+ }
+
+ @Test
+ fun testServerStartWithoutEnoughMemory() = scope.runBlockingSimulation {
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 0, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.ERROR, server.state)
+ }
+
+ @Test
+ fun testServerStartWithoutEnoughResources() = scope.runBlockingSimulation {
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.ERROR, server.state)
+ }
+
+ @Test
+ fun testServerCancelRequest() = scope.runBlockingSimulation {
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ server.stop()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.TERMINATED, server.state)
+ }
+
+ @Test
+ fun testServerCannotFitOnHost() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.canFit(any()) } returns false
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(10 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.PROVISIONING, server.state)
+
+ verify { host.canFit(server) }
+ }
+
+ @Test
+ fun testHostAvailableAfterSomeTime() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.DOWN
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+ every { host.canFit(any()) } returns false
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+
+ every { host.state } returns HostState.UP
+ listeners.forEach { it.onStateChanged(host, HostState.UP) }
+
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.PROVISIONING, server.state)
+
+ verify { host.canFit(server) }
+ }
+
+ @Test
+ fun testHostUnavailableAfterSomeTime() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+ every { host.canFit(any()) } returns false
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ delay(5 * 60 * 1000)
+
+ every { host.state } returns HostState.DOWN
+ listeners.forEach { it.onStateChanged(host, HostState.DOWN) }
+
+ server.start()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.PROVISIONING, server.state)
+
+ verify(exactly = 0) { host.canFit(server) }
+ }
+
+ @Test
+ fun testServerInvalidType() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.canFit(any()) } returns true
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ assertThrows<IllegalArgumentException> {
+ listeners.forEach { it.onStateChanged(host, server, ServerState.RUNNING) }
+ }
+ }
+
+ @Test
+ fun testServerDeploy() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.canFit(any()) } returns true
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+ val slot = slot<Server>()
+
+ val watcher = mockk<ServerWatcher>(relaxUnitFun = true)
+ server.watch(watcher)
+
+ // Start server
+ server.start()
+ delay(5 * 60 * 1000)
+ coVerify { host.spawn(capture(slot), true) }
+
+ listeners.forEach { it.onStateChanged(host, slot.captured, ServerState.RUNNING) }
+
+ server.refresh()
+ assertEquals(ServerState.RUNNING, server.state)
+
+ verify { watcher.onStateChanged(server, ServerState.RUNNING) }
+
+ // Stop server
+ listeners.forEach { it.onStateChanged(host, slot.captured, ServerState.TERMINATED) }
+
+ server.refresh()
+ assertEquals(ServerState.TERMINATED, server.state)
+
+ verify { watcher.onStateChanged(server, ServerState.TERMINATED) }
+ }
+
+ @Test
+ fun testServerDeployFailure() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.canFit(any()) } returns true
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+ coEvery { host.spawn(any(), true) } throws IllegalStateException()
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+
+ server.refresh()
+ assertEquals(ServerState.PROVISIONING, server.state)
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt
new file mode 100644
index 00000000..18d698c6
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.compute.service
+
+import io.mockk.*
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNotEquals
+import org.junit.jupiter.api.Test
+import org.opendc.compute.api.Flavor
+import org.opendc.compute.service.internal.ComputeServiceImpl
+import org.opendc.compute.service.internal.InternalFlavor
+import java.util.*
+
+/**
+ * Test suite for the [InternalFlavor] implementation.
+ */
+class InternalFlavorTest {
+ @Test
+ fun testEquality() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+ val b = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testEqualityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Flavor>(relaxUnitFun = true)
+ every { b.uid } returns uid
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Flavor>(relaxUnitFun = true)
+ every { b.uid } returns UUID.randomUUID()
+
+ assertNotEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithIncorrectType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+
+ assertNotEquals(a, Unit)
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt
new file mode 100644
index 00000000..e1cb0128
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.compute.service
+
+import io.mockk.*
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNotEquals
+import org.junit.jupiter.api.Test
+import org.opendc.compute.api.Image
+import org.opendc.compute.service.internal.ComputeServiceImpl
+import org.opendc.compute.service.internal.InternalFlavor
+import org.opendc.compute.service.internal.InternalImage
+import java.util.*
+
+/**
+ * Test suite for the [InternalFlavor] implementation.
+ */
+class InternalImageTest {
+ @Test
+ fun testEquality() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+ val b = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testEqualityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Image>(relaxUnitFun = true)
+ every { b.uid } returns uid
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Image>(relaxUnitFun = true)
+ every { b.uid } returns UUID.randomUUID()
+
+ assertNotEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithIncorrectType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+
+ assertNotEquals(a, Unit)
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt
new file mode 100644
index 00000000..20ea8d20
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt
@@ -0,0 +1,285 @@
+/*
+ * 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.compute.service
+
+import io.mockk.*
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.yield
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.opendc.compute.api.Server
+import org.opendc.compute.api.ServerState
+import org.opendc.compute.service.driver.Host
+import org.opendc.compute.service.internal.ComputeServiceImpl
+import org.opendc.compute.service.internal.InternalFlavor
+import org.opendc.compute.service.internal.InternalImage
+import org.opendc.compute.service.internal.InternalServer
+import org.opendc.simulator.core.runBlockingSimulation
+import java.util.*
+
+/**
+ * Test suite for the [InternalServer] implementation.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class InternalServerTest {
+ @Test
+ fun testEquality() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val a = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val b = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testEqualityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val a = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Server>(relaxUnitFun = true)
+ every { b.uid } returns uid
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val a = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Server>(relaxUnitFun = true)
+ every { b.uid } returns UUID.randomUUID()
+
+ assertNotEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithIncorrectType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val a = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ assertNotEquals(a, Unit)
+ }
+
+ @Test
+ fun testStartTerminatedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ every { service.schedule(any()) } answers { ComputeServiceImpl.SchedulingRequest(it.invocation.args[0] as InternalServer) }
+
+ server.start()
+
+ verify(exactly = 1) { service.schedule(server) }
+ assertEquals(ServerState.PROVISIONING, server.state)
+ }
+
+ @Test
+ fun testStartDeletedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.DELETED
+
+ assertThrows<IllegalStateException> { server.start() }
+ }
+
+ @Test
+ fun testStartProvisioningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.PROVISIONING
+
+ server.start()
+
+ assertEquals(ServerState.PROVISIONING, server.state)
+ }
+
+ @Test
+ fun testStartRunningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.RUNNING
+
+ server.start()
+
+ assertEquals(ServerState.RUNNING, server.state)
+ }
+
+ @Test
+ fun testStopProvisioningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val request = ComputeServiceImpl.SchedulingRequest(server)
+
+ every { service.schedule(any()) } returns request
+
+ server.start()
+ server.stop()
+
+ assertTrue(request.isCancelled)
+ assertEquals(ServerState.TERMINATED, server.state)
+ }
+
+ @Test
+ fun testStopTerminatedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.TERMINATED
+ server.stop()
+
+ assertEquals(ServerState.TERMINATED, server.state)
+ }
+
+ @Test
+ fun testStopDeletedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.DELETED
+ server.stop()
+
+ assertEquals(ServerState.DELETED, server.state)
+ }
+
+ @Test
+ fun testStopRunningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ server.state = ServerState.RUNNING
+ server.host = host
+ server.stop()
+ yield()
+
+ coVerify { host.stop(server) }
+ }
+
+ @Test
+ fun testDeleteProvisioningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>(relaxUnitFun = true)
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val request = ComputeServiceImpl.SchedulingRequest(server)
+
+ every { service.schedule(any()) } returns request
+
+ server.start()
+ server.delete()
+
+ assertTrue(request.isCancelled)
+ assertEquals(ServerState.DELETED, server.state)
+ verify { service.delete(server) }
+ }
+
+ @Test
+ fun testDeleteTerminatedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>(relaxUnitFun = true)
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.TERMINATED
+ server.delete()
+
+ assertEquals(ServerState.DELETED, server.state)
+
+ verify { service.delete(server) }
+ }
+
+ @Test
+ fun testDeleteDeletedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>(relaxUnitFun = true)
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.DELETED
+ server.delete()
+
+ assertEquals(ServerState.DELETED, server.state)
+ }
+
+ @Test
+ fun testDeleteRunningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>(relaxUnitFun = true)
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ server.state = ServerState.RUNNING
+ server.host = host
+ server.delete()
+ yield()
+
+ coVerify { host.delete(server) }
+ verify { service.delete(server) }
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml b/opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml
new file mode 100644
index 00000000..0dfb75f2
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<Configuration status="WARN" packages="org.apache.logging.log4j.core">
+ <Appenders>
+ <Console name="Console" target="SYSTEM_OUT">
+ <PatternLayout pattern="%d{HH:mm:ss.SSS} [%highlight{%-5level}] %logger{36} - %msg%n" disableAnsi="false"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Logger name="org.opendc" level="trace" additivity="false">
+ <AppenderRef ref="Console"/>
+ </Logger>
+ <Root level="info">
+ <AppenderRef ref="Console"/>
+ </Root>
+ </Loggers>
+</Configuration>