From d5aed4b1e6e5548728c5978e3b46d1472b62e791 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Mon, 17 Oct 2022 22:03:02 +0200 Subject: refactor(compute/sim): Use workload chaining for boot delay This change updates the implementation of `SimHost` to use workload chaining for modelling boot delays. Previously, this was implemented by sleeping 1 millisecond using Kotlin coroutines. With this change, we remove the need for coroutines and instead use the `SimDurationWorkload` to model the boot delay. In the future, we envision a user-supplied stochastic boot model to model the boot delay for VM instances. --- .../kotlin/org/opendc/compute/simulator/SimHost.kt | 10 ++-- .../opendc/compute/simulator/SimWorkloadMapper.kt | 1 + .../simulator/internal/DefaultWorkloadMapper.kt | 44 ++++++++++++++ .../org/opendc/compute/simulator/internal/Guest.kt | 66 +++++++------------- .../org/opendc/compute/simulator/SimHostTest.kt | 70 +++++++++++++++++++++- .../experiments/capelin/CapelinIntegrationTest.kt | 22 +++---- .../experiments/compute/HostsProvisioningStep.kt | 1 - .../opendc/workflow/service/WorkflowServiceTest.kt | 2 +- 8 files changed, 149 insertions(+), 67 deletions(-) create mode 100644 opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/internal/DefaultWorkloadMapper.kt diff --git a/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimHost.kt b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimHost.kt index d07c50bc..660c1ccc 100644 --- a/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimHost.kt +++ b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimHost.kt @@ -34,6 +34,7 @@ import org.opendc.compute.service.driver.telemetry.GuestCpuStats import org.opendc.compute.service.driver.telemetry.GuestSystemStats import org.opendc.compute.service.driver.telemetry.HostCpuStats import org.opendc.compute.service.driver.telemetry.HostSystemStats +import org.opendc.compute.simulator.internal.DefaultWorkloadMapper import org.opendc.compute.simulator.internal.Guest import org.opendc.compute.simulator.internal.GuestListener import org.opendc.simulator.compute.SimBareMetalMachine @@ -47,7 +48,6 @@ import org.opendc.simulator.flow2.FlowGraph import java.time.Duration import java.time.Instant import java.util.UUID -import kotlin.coroutines.CoroutineContext /** * A [Host] that is simulates virtual machines on a physical machine using [SimHypervisor]. @@ -56,11 +56,10 @@ public class SimHost( override val uid: UUID, override val name: String, override val meta: Map, - private val context: CoroutineContext, graph: FlowGraph, private val machine: SimBareMetalMachine, private val hypervisor: SimHypervisor, - private val mapper: SimWorkloadMapper = SimMetaWorkloadMapper(), + private val mapper: SimWorkloadMapper = DefaultWorkloadMapper, private val optimize: Boolean = false ) : Host, AutoCloseable { /** @@ -129,7 +128,6 @@ public class SimHost( val machine = hypervisor.newMachine(key.flavor.toMachineModel()) val newGuest = Guest( - context, clock, this, hypervisor, @@ -250,7 +248,7 @@ public class SimHost( override fun toString(): String = "SimHost[uid=$uid,name=$name,model=$model]" - public suspend fun fail() { + public fun fail() { reset(HostState.ERROR) for (guest in _guests) { @@ -272,7 +270,7 @@ public class SimHost( } /** - * The [Job] that represents the machine running the hypervisor. + * The [SimMachineContext] that represents the machine running the hypervisor. */ private var _ctx: SimMachineContext? = null diff --git a/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimWorkloadMapper.kt b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimWorkloadMapper.kt index 7082c5cf..83baa61a 100644 --- a/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimWorkloadMapper.kt +++ b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/SimWorkloadMapper.kt @@ -22,6 +22,7 @@ package org.opendc.compute.simulator +import org.opendc.compute.api.Image import org.opendc.compute.api.Server import org.opendc.simulator.compute.workload.SimWorkload diff --git a/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/internal/DefaultWorkloadMapper.kt b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/internal/DefaultWorkloadMapper.kt new file mode 100644 index 00000000..c5293a8d --- /dev/null +++ b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/internal/DefaultWorkloadMapper.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 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.simulator.internal + +import org.opendc.compute.api.Server +import org.opendc.compute.simulator.SimMetaWorkloadMapper +import org.opendc.compute.simulator.SimWorkloadMapper +import org.opendc.simulator.compute.workload.SimWorkload +import org.opendc.simulator.compute.workload.SimWorkloads +import java.time.Duration + +/** + * A [SimWorkloadMapper] to introduces a boot delay of 1 ms. This object exists to retain the old behavior while + * introducing the possibility of adding custom boot delays. + */ +internal object DefaultWorkloadMapper : SimWorkloadMapper { + private val delegate = SimMetaWorkloadMapper() + + override fun createWorkload(server: Server): SimWorkload { + val workload = delegate.createWorkload(server) + val bootWorkload = SimWorkloads.runtime(Duration.ofMillis(1), 0.8) + return SimWorkloads.chain(bootWorkload, workload) + } +} diff --git a/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/internal/Guest.kt b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/internal/Guest.kt index 790d8047..6d3a5bc7 100644 --- a/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/internal/Guest.kt +++ b/opendc-compute/opendc-compute-simulator/src/main/kotlin/org/opendc/compute/simulator/internal/Guest.kt @@ -22,12 +22,6 @@ package org.opendc.compute.simulator.internal -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import mu.KotlinLogging import org.opendc.compute.api.Server import org.opendc.compute.api.ServerState @@ -35,20 +29,17 @@ import org.opendc.compute.service.driver.telemetry.GuestCpuStats import org.opendc.compute.service.driver.telemetry.GuestSystemStats import org.opendc.compute.simulator.SimHost import org.opendc.compute.simulator.SimWorkloadMapper +import org.opendc.simulator.compute.SimMachineContext import org.opendc.simulator.compute.kernel.SimHypervisor import org.opendc.simulator.compute.kernel.SimVirtualMachine -import org.opendc.simulator.compute.runWorkload -import org.opendc.simulator.compute.workload.SimWorkload import java.time.Clock import java.time.Duration import java.time.Instant -import kotlin.coroutines.CoroutineContext /** * A virtual machine instance that is managed by a [SimHost]. */ internal class Guest( - context: CoroutineContext, private val clock: Clock, val host: SimHost, private val hypervisor: SimHypervisor, @@ -57,11 +48,6 @@ internal class Guest( val server: Server, val machine: SimVirtualMachine ) { - /** - * The [CoroutineScope] of the guest. - */ - private val scope: CoroutineScope = CoroutineScope(context + Job()) - /** * The logger instance of this guest. */ @@ -78,7 +64,7 @@ internal class Guest( /** * Start the guest. */ - suspend fun start() { + fun start() { when (state) { ServerState.TERMINATED, ServerState.ERROR -> { logger.info { "User requested to start server ${server.uid}" } @@ -96,7 +82,7 @@ internal class Guest( /** * Stop the guest. */ - suspend fun stop() { + fun stop() { when (state) { ServerState.RUNNING -> doStop(ServerState.TERMINATED) ServerState.ERROR -> doRecover() @@ -111,12 +97,11 @@ internal class Guest( * This operation will stop the guest if it is running on the host and remove all resources associated with the * guest. */ - suspend fun delete() { + fun delete() { stop() state = ServerState.DELETED hypervisor.removeMachine(machine) - scope.cancel() } /** @@ -124,7 +109,7 @@ internal class Guest( * * This operation forcibly stops the guest and puts the server into an error state. */ - suspend fun fail() { + fun fail() { if (state != ServerState.RUNNING) { return } @@ -135,7 +120,7 @@ internal class Guest( /** * Recover the guest if it is in an error state. */ - suspend fun recover() { + fun recover() { if (state != ServerState.ERROR) { return } @@ -175,37 +160,34 @@ internal class Guest( } /** - * The [Job] representing the current active virtual machine instance or `null` if no virtual machine is active. + * The [SimMachineContext] representing the current active virtual machine instance or `null` if no virtual machine + * is active. */ - private var job: Job? = null + private var ctx: SimMachineContext? = null /** * Launch the guest on the simulated */ - private suspend fun doStart() { - assert(job == null) { "Concurrent job running" } - val workload = mapper.createWorkload(server) - - val job = scope.launch { runMachine(workload) } - this.job = job + private fun doStart() { + assert(ctx == null) { "Concurrent job running" } - state = ServerState.RUNNING onStart() - job.invokeOnCompletion { cause -> - this.job = null - onStop(if (cause != null && cause !is CancellationException) ServerState.ERROR else ServerState.TERMINATED) + val workload = mapper.createWorkload(server) + val meta = mapOf("driver" to host, "server" to server) + server.meta + ctx = machine.startWorkload(workload, meta) { cause -> + onStop(if (cause != null) ServerState.ERROR else ServerState.TERMINATED) + ctx = null } } /** * Attempt to stop the server and put it into [target] state. */ - private suspend fun doStop(target: ServerState) { - assert(job != null) { "Invalid job state" } - val job = job ?: return - job.cancel() - job.join() + private fun doStop(target: ServerState) { + assert(ctx != null) { "Invalid job state" } + val ctx = ctx ?: return + ctx.shutdown() state = target } @@ -217,14 +199,6 @@ internal class Guest( state = ServerState.TERMINATED } - /** - * Converge the process that models the virtual machine lifecycle as a coroutine. - */ - private suspend fun runMachine(workload: SimWorkload) { - delay(1) // TODO Introduce model for boot time - machine.runWorkload(workload, mapOf("driver" to host, "server" to server) + server.meta) - } - /** * This method is invoked when the guest was started on the host and has booted into a running state. */ diff --git a/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimHostTest.kt b/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimHostTest.kt index a5999bcd..02be3f28 100644 --- a/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimHostTest.kt +++ b/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimHostTest.kt @@ -69,6 +69,74 @@ internal class SimHostTest { ) } + /** + * Test a single virtual machine hosted by the hypervisor. + */ + @Test + fun testSingle() = runSimulation { + val duration = 5 * 60L + + val engine = FlowEngine.create(coroutineContext, clock) + val graph = engine.newGraph() + + val machine = SimBareMetalMachine.create(graph, machineModel) + val hypervisor = SimHypervisor.create(FlowMultiplexerFactory.maxMinMultiplexer(), SplittableRandom(1)) + + val host = SimHost( + uid = UUID.randomUUID(), + name = "test", + meta = emptyMap(), + graph, + machine, + hypervisor + ) + val vmImage = MockImage( + UUID.randomUUID(), + "", + emptyMap(), + mapOf( + "workload" to + SimTrace.ofFragments( + SimTraceFragment(0, duration * 1000, 2 * 28.0, 2), + SimTraceFragment(duration * 1000, duration * 1000, 2 * 3500.0, 2), + SimTraceFragment(duration * 2000, duration * 1000, 0.0, 2), + SimTraceFragment(duration * 3000, duration * 1000, 2 * 183.0, 2) + ).createWorkload(1) + ) + ) + + val flavor = MockFlavor(2, 0) + + coroutineScope { + launch { host.spawn(MockServer(UUID.randomUUID(), "a", flavor, vmImage)) } + + suspendCancellableCoroutine { cont -> + host.addListener(object : HostListener { + private var finished = 0 + + override fun onStateChanged(host: Host, server: Server, newState: ServerState) { + if (newState == ServerState.TERMINATED && ++finished == 1) { + cont.resume(Unit) + } + } + }) + } + } + + // Ensure last cycle is collected + delay(1000L * duration) + host.close() + + val cpuStats = host.getCpuStats() + + assertAll( + { assertEquals(639, cpuStats.activeTime, "Active time does not match") }, + { assertEquals(2360, cpuStats.idleTime, "Idle time does not match") }, + { assertEquals(56, cpuStats.stealTime, "Steal time does not match") }, + { assertEquals(1500001, clock.millis()) } + ) + } + /** * Test overcommitting of resources by the hypervisor. */ @@ -86,7 +154,6 @@ internal class SimHostTest { uid = UUID.randomUUID(), name = "test", meta = emptyMap(), - coroutineContext, graph, machine, hypervisor @@ -169,7 +236,6 @@ internal class SimHostTest { uid = UUID.randomUUID(), name = "test", meta = emptyMap(), - coroutineContext, graph, machine, hypervisor diff --git a/opendc-experiments/opendc-experiments-capelin/src/test/kotlin/org/opendc/experiments/capelin/CapelinIntegrationTest.kt b/opendc-experiments/opendc-experiments-capelin/src/test/kotlin/org/opendc/experiments/capelin/CapelinIntegrationTest.kt index 47058caa..77b0d09f 100644 --- a/opendc-experiments/opendc-experiments-capelin/src/test/kotlin/org/opendc/experiments/capelin/CapelinIntegrationTest.kt +++ b/opendc-experiments/opendc-experiments-capelin/src/test/kotlin/org/opendc/experiments/capelin/CapelinIntegrationTest.kt @@ -120,9 +120,9 @@ class CapelinIntegrationTest { { assertEquals(0, monitor.serversActive, "All VMs should finish after a run") }, { assertEquals(0, monitor.attemptsFailure, "No VM should be unscheduled") }, { assertEquals(0, monitor.serversPending, "No VM should not be in the queue") }, - { assertEquals(223394204, monitor.idleTime) { "Incorrect idle time" } }, - { assertEquals(66976984, monitor.activeTime) { "Incorrect active time" } }, - { assertEquals(3160316, monitor.stealTime) { "Incorrect steal time" } }, + { assertEquals(223394101, monitor.idleTime) { "Incorrect idle time" } }, + { assertEquals(66977086, monitor.activeTime) { "Incorrect active time" } }, + { assertEquals(3160276, monitor.stealTime) { "Incorrect steal time" } }, { assertEquals(0, monitor.lostTime) { "Incorrect lost time" } }, { assertEquals(5.84093E9, monitor.energyUsage, 1E4) { "Incorrect power draw" } } ) @@ -160,8 +160,8 @@ class CapelinIntegrationTest { // Note that these values have been verified beforehand assertAll( - { assertEquals(10999504, monitor.idleTime) { "Idle time incorrect" } }, - { assertEquals(9741294, monitor.activeTime) { "Active time incorrect" } }, + { assertEquals(10999514, monitor.idleTime) { "Idle time incorrect" } }, + { assertEquals(9741285, monitor.activeTime) { "Active time incorrect" } }, { assertEquals(0, monitor.stealTime) { "Steal time incorrect" } }, { assertEquals(0, monitor.lostTime) { "Lost time incorrect" } }, { assertEquals(7.0116E8, monitor.energyUsage, 1E4) { "Incorrect power draw" } } @@ -199,10 +199,10 @@ class CapelinIntegrationTest { // Note that these values have been verified beforehand assertAll( - { assertEquals(6027979, monitor.idleTime) { "Idle time incorrect" } }, - { assertEquals(14712820, monitor.activeTime) { "Active time incorrect" } }, - { assertEquals(12532979, monitor.stealTime) { "Steal time incorrect" } }, - { assertEquals(445913, monitor.lostTime) { "Lost time incorrect" } } + { assertEquals(6028018, monitor.idleTime) { "Idle time incorrect" } }, + { assertEquals(14712781, monitor.activeTime) { "Active time incorrect" } }, + { assertEquals(12532934, monitor.stealTime) { "Steal time incorrect" } }, + { assertEquals(424267, monitor.lostTime) { "Lost time incorrect" } } ) } @@ -229,8 +229,8 @@ class CapelinIntegrationTest { // Note that these values have been verified beforehand assertAll( - { assertEquals(10085103, monitor.idleTime) { "Idle time incorrect" } }, - { assertEquals(8539212, monitor.activeTime) { "Active time incorrect" } }, + { assertEquals(10085111, monitor.idleTime) { "Idle time incorrect" } }, + { assertEquals(8539204, monitor.activeTime) { "Active time incorrect" } }, { assertEquals(0, monitor.stealTime) { "Steal time incorrect" } }, { assertEquals(0, monitor.lostTime) { "Lost time incorrect" } }, { assertEquals(2328039558, monitor.uptime) { "Uptime incorrect" } } diff --git a/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/HostsProvisioningStep.kt b/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/HostsProvisioningStep.kt index 292be929..16a57236 100644 --- a/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/HostsProvisioningStep.kt +++ b/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/HostsProvisioningStep.kt @@ -58,7 +58,6 @@ public class HostsProvisioningStep internal constructor( spec.uid, spec.name, spec.meta, - ctx.coroutineContext, graph, machine, hypervisor, diff --git a/opendc-workflow/opendc-workflow-service/src/test/kotlin/org/opendc/workflow/service/WorkflowServiceTest.kt b/opendc-workflow/opendc-workflow-service/src/test/kotlin/org/opendc/workflow/service/WorkflowServiceTest.kt index c7123000..b165418a 100644 --- a/opendc-workflow/opendc-workflow-service/src/test/kotlin/org/opendc/workflow/service/WorkflowServiceTest.kt +++ b/opendc-workflow/opendc-workflow-service/src/test/kotlin/org/opendc/workflow/service/WorkflowServiceTest.kt @@ -119,7 +119,7 @@ internal class WorkflowServiceTest { }, { assertEquals(0, metrics.tasksRunning, "Not all started tasks finished") }, { assertEquals(metrics.tasksSubmitted, metrics.tasksFinished, "Not all started tasks finished") }, - { assertEquals(46102707L, clock.millis()) { "Total duration incorrect" } } + { assertEquals(45977707L, clock.millis()) { "Total duration incorrect" } } ) } } -- cgit v1.2.3