summaryrefslogtreecommitdiff
path: root/simulator/opendc-compute
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2021-03-25 10:23:47 +0100
committerFabian Mastenbroek <mail.fabianm@gmail.com>2021-03-26 15:40:26 +0100
commit0bbb0adb97ba4783bbd0073f845781725e6212e8 (patch)
treed185a7c0e556946aff1334e3b92d6f3163046c21 /simulator/opendc-compute
parent074dee1cbca7b3a024d45a3b9dd7d8b51acdd4ee (diff)
compute: Add test suite for ComputeService
This change adds a test suite for the OpenDC compute service.
Diffstat (limited to 'simulator/opendc-compute')
-rw-r--r--simulator/opendc-compute/opendc-compute-service/build.gradle.kts3
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt6
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt6
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt6
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt274
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt2
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt4
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt4
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt46
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayAllocationPolicy.kt7
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt390
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt80
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt81
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt285
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/scheduler/AllocationPolicyTest.kt219
-rw-r--r--simulator/opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml38
16 files changed, 1297 insertions, 154 deletions
diff --git a/simulator/opendc-compute/opendc-compute-service/build.gradle.kts b/simulator/opendc-compute/opendc-compute-service/build.gradle.kts
index 1b09ef6d..1825e989 100644
--- a/simulator/opendc-compute/opendc-compute-service/build.gradle.kts
+++ b/simulator/opendc-compute/opendc-compute-service/build.gradle.kts
@@ -26,6 +26,7 @@ description = "OpenDC Compute Service implementation"
plugins {
`kotlin-library-conventions`
`testing-conventions`
+ `jacoco-conventions`
}
dependencies {
@@ -36,5 +37,5 @@ dependencies {
implementation("io.github.microutils:kotlin-logging")
testImplementation(project(":opendc-simulator:opendc-simulator-core"))
- testRuntimeOnly("org.slf4j:slf4j-simple:${versions.slf4j}")
+ testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j-impl")
}
diff --git a/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt
index 29f10e27..4a8d3046 100644
--- a/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt
+++ b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt
@@ -59,4 +59,10 @@ internal class ClientFlavor(private val delegate: Flavor) : Flavor {
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/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt
index 6c5b2ab0..e0b5c171 100644
--- a/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt
+++ b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt
@@ -52,4 +52,10 @@ internal class ClientImage(private val delegate: Image) : Image {
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/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt
index ae4cee3b..f2929bf3 100644
--- a/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt
+++ b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt
@@ -104,4 +104,10 @@ internal class ClientServer(private val delegate: Server) : Server, ServerWatche
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/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt
index aa7e0aa1..62808b4d 100644
--- a/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt
+++ b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt
@@ -57,7 +57,7 @@ public class ComputeServiceImpl(
/**
* The [CoroutineScope] of the service bounded by the lifecycle of the service.
*/
- private val scope = CoroutineScope(context)
+ private val scope = CoroutineScope(context + Job())
/**
* The logger instance of this server.
@@ -133,130 +133,133 @@ public class ComputeServiceImpl(
override val hostCount: Int
get() = hostToView.size
- override fun newClient(): ComputeClient = object : ComputeClient {
- private var isClosed: Boolean = false
+ 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" }
+ override suspend fun queryFlavors(): List<Flavor> {
+ check(!isClosed) { "Client is already closed" }
- return flavors.values.map { ClientFlavor(it) }
- }
+ return flavors.values.map { ClientFlavor(it) }
+ }
- override suspend fun findFlavor(id: UUID): Flavor? {
- check(!isClosed) { "Client is already closed" }
+ override suspend fun findFlavor(id: UUID): Flavor? {
+ check(!isClosed) { "Client is already closed" }
- return flavors[id]?.let { ClientFlavor(it) }
- }
+ 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
- )
+ 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
+ flavors[uid] = flavor
- return ClientFlavor(flavor)
- }
+ return ClientFlavor(flavor)
+ }
- override suspend fun queryImages(): List<Image> {
- check(!isClosed) { "Client is already closed" }
+ override suspend fun queryImages(): List<Image> {
+ check(!isClosed) { "Client is already closed" }
- return images.values.map { ClientImage(it) }
- }
+ return images.values.map { ClientImage(it) }
+ }
- override suspend fun findImage(id: UUID): Image? {
- check(!isClosed) { "Client is already closed" }
+ override suspend fun findImage(id: UUID): Image? {
+ check(!isClosed) { "Client is already closed" }
- return images[id]?.let { ClientImage(it) }
- }
+ 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" }
+ 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)
+ val uid = UUID(clock.millis(), random.nextLong())
+ val image = InternalImage(this@ComputeServiceImpl, uid, name, labels, meta)
- images[uid] = image
+ images[uid] = image
- return ClientImage(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" }
- tracer.commit(VmSubmissionEvent(name, image, flavor))
+ 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" }
+ tracer.commit(VmSubmissionEvent(name, image, flavor))
- _events.emit(
- ComputeServiceEvent.MetricsAvailable(
+ _events.emit(
+ ComputeServiceEvent.MetricsAvailable(
+ this@ComputeServiceImpl,
+ hostCount,
+ availableHosts.size,
+ ++submittedVms,
+ runningVms,
+ finishedVms,
+ ++queuedVms,
+ unscheduledVms
+ )
+ )
+
+ val uid = UUID(clock.millis(), random.nextLong())
+ val server = InternalServer(
this@ComputeServiceImpl,
- hostCount,
- availableHosts.size,
- ++submittedVms,
- runningVms,
- finishedVms,
- ++queuedVms,
- unscheduledVms
+ uid,
+ name,
+ requireNotNull(flavors[flavor.uid]) { "Unknown flavor" },
+ requireNotNull(images[image.uid]) { "Unknown image" },
+ labels.toMutableMap(),
+ meta.toMutableMap()
)
- )
- val uid = UUID(clock.millis(), random.nextLong())
- val server = InternalServer(
- this@ComputeServiceImpl,
- uid,
- name,
- flavor,
- image,
- labels.toMutableMap(),
- meta.toMutableMap()
- )
+ servers[uid] = server
- servers[uid] = server
+ if (start) {
+ server.start()
+ }
- if (start) {
- server.start()
+ return ClientServer(server)
}
- return ClientServer(server)
- }
+ override suspend fun findServer(id: UUID): Server? {
+ check(!isClosed) { "Client is already closed" }
- override suspend fun findServer(id: UUID): Server? {
- check(!isClosed) { "Client is already closed" }
+ return servers[id]?.let { ClientServer(it) }
+ }
- return servers[id]?.let { ClientServer(it) }
- }
+ override suspend fun queryServers(): List<Server> {
+ check(!isClosed) { "Client is already closed" }
- override suspend fun queryServers(): List<Server> {
- check(!isClosed) { "Client is already closed" }
+ return servers.values.map { ClientServer(it) }
+ }
- return servers.values.map { ClientServer(it) }
- }
+ override fun close() {
+ isClosed = true
+ }
- override fun close() {
- isClosed = true
+ override fun toString(): String = "ComputeClient"
}
-
- override fun toString(): String = "ComputeClient"
}
override fun addHost(host: Host) {
@@ -285,23 +288,25 @@ public class ComputeServiceImpl(
scope.cancel()
}
- internal fun schedule(server: InternalServer) {
+ internal fun schedule(server: InternalServer): SchedulingRequest {
logger.debug { "Enqueueing server ${server.uid} to be assigned to host." }
- queue.add(SchedulingRequest(server))
+ val request = SchedulingRequest(server)
+ queue.add(request)
requestSchedulingCycle()
+ return request
}
internal fun delete(flavor: InternalFlavor) {
- checkNotNull(flavors.remove(flavor.uid)) { "Flavor was not known" }
+ flavors.remove(flavor.uid)
}
internal fun delete(image: InternalImage) {
- checkNotNull(images.remove(image.uid)) { "Image was not known" }
+ images.remove(image.uid)
}
internal fun delete(server: InternalServer) {
- checkNotNull(servers.remove(server.uid)) { "Server was not known" }
+ servers.remove(server.uid)
}
/**
@@ -338,7 +343,7 @@ public class ComputeServiceImpl(
val server = request.server
val hv = allocationLogic.select(availableHosts, request.server)
if (hv == null || !hv.host.canFit(server)) {
- logger.trace { "Server $server selected for scheduling but no capacity available for it." }
+ 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) {
tracer.commit(VmSubmissionInvalidEvent(server.name))
@@ -360,6 +365,8 @@ public class ComputeServiceImpl(
queue.poll()
logger.warn("Failed to spawn $server: does not fit [${clock.millis()}]")
+
+ server.state = ServerState.ERROR
continue
} else {
break
@@ -372,42 +379,39 @@ public class ComputeServiceImpl(
queue.poll()
logger.info { "Assigned server $server to host $host." }
- try {
- // Speculatively update the hypervisor view information to prevent other images in the queue from
- // deciding on stale values.
- hv.numberOfActiveServers++
- hv.provisionedCores += server.flavor.cpuCount
- hv.availableMemory -= server.flavor.memorySize // XXX Temporary hack
-
- scope.launch {
- try {
- server.assignHost(host)
- host.spawn(server)
- activeServers[server] = host
-
- tracer.commit(VmScheduledEvent(server.name))
- _events.emit(
- ComputeServiceEvent.MetricsAvailable(
- this@ComputeServiceImpl,
- hostCount,
- availableHosts.size,
- submittedVms,
- ++runningVms,
- finishedVms,
- --queuedVms,
- unscheduledVms
- )
+
+ // Speculatively update the hypervisor view information to prevent other images in the queue from
+ // deciding on stale values.
+ hv.numberOfActiveServers++
+ 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
+
+ tracer.commit(VmScheduledEvent(server.name))
+ _events.emit(
+ ComputeServiceEvent.MetricsAvailable(
+ this@ComputeServiceImpl,
+ hostCount,
+ availableHosts.size,
+ submittedVms,
+ ++runningVms,
+ finishedVms,
+ --queuedVms,
+ unscheduledVms
)
- } catch (e: Throwable) {
- logger.error("Failed to deploy VM", e)
+ )
+ } catch (e: Throwable) {
+ logger.error("Failed to deploy VM", e)
- hv.numberOfActiveServers--
- hv.provisionedCores -= server.flavor.cpuCount
- hv.availableMemory += server.flavor.memorySize
- }
+ hv.numberOfActiveServers--
+ hv.provisionedCores -= server.flavor.cpuCount
+ hv.availableMemory += server.flavor.memorySize
}
- } catch (e: Exception) {
- logger.warn(e) { "Failed to assign server $server to $host. " }
}
}
}
@@ -415,7 +419,7 @@ public class ComputeServiceImpl(
/**
* A request to schedule an [InternalServer] onto one of the [Host]s.
*/
- private data class SchedulingRequest(val server: InternalServer) {
+ internal data class SchedulingRequest(val server: InternalServer) {
/**
* A flag to indicate that the request is cancelled.
*/
diff --git a/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt
index 1bdfdf1a..5793541f 100644
--- a/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt
+++ b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/HostView.kt
@@ -32,4 +32,6 @@ public class HostView(public val host: Host) {
public var numberOfActiveServers: Int = 0
public var availableMemory: Long = host.model.memorySize
public var provisionedCores: Int = 0
+
+ override fun toString(): String = "HostView[host=$host]"
}
diff --git a/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt
index 95e280df..b8fb6279 100644
--- a/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt
+++ b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt
@@ -58,7 +58,9 @@ internal class InternalFlavor(
service.delete(this)
}
- override fun equals(other: Any?): Boolean = other is InternalFlavor && uid == other.uid
+ 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/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt
index 86f2f6b9..d9ed5896 100644
--- a/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt
+++ b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt
@@ -48,7 +48,9 @@ internal class InternalImage(
service.delete(this)
}
- override fun equals(other: Any?): Boolean = other is InternalImage && uid == other.uid
+ 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/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt
index ff7c1d15..d9d0f3fc 100644
--- a/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt
+++ b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt
@@ -34,8 +34,8 @@ internal class InternalServer(
private val service: ComputeServiceImpl,
override val uid: UUID,
override val name: String,
- override val flavor: Flavor,
- override val image: Image,
+ override val flavor: InternalFlavor,
+ override val image: InternalImage,
override val labels: MutableMap<String, String>,
override val meta: MutableMap<String, Any>
) : Server {
@@ -54,6 +54,11 @@ internal class InternalServer(
*/
internal var host: Host? = null
+ /**
+ * The current scheduling request.
+ */
+ private var request: ComputeServiceImpl.SchedulingRequest? = null
+
override suspend fun start() {
when (state) {
ServerState.RUNNING -> {
@@ -66,35 +71,43 @@ internal class InternalServer(
}
ServerState.DELETED -> {
logger.warn { "User tried to start terminated server" }
- throw IllegalArgumentException("Server is terminated")
+ throw IllegalStateException("Server is terminated")
}
else -> {
logger.info { "User requested to start server $uid" }
state = ServerState.PROVISIONING
- service.schedule(this)
+ assert(request == null) { "Scheduling request already active" }
+ request = service.schedule(this)
}
}
}
override suspend fun stop() {
when (state) {
- ServerState.PROVISIONING -> {} // TODO Find way to interrupt these
+ ServerState.PROVISIONING -> {
+ cancelProvisioningRequest()
+ state = ServerState.TERMINATED
+ }
ServerState.RUNNING, ServerState.ERROR -> {
val host = checkNotNull(host) { "Server not running" }
host.stop(this)
}
- ServerState.TERMINATED -> {} // No work needed
- ServerState.DELETED -> throw IllegalStateException("Server is terminated")
+ ServerState.TERMINATED, ServerState.DELETED -> {} // No work needed
}
}
override suspend fun delete() {
when (state) {
- ServerState.PROVISIONING -> {} // TODO Find way to interrupt these
- ServerState.RUNNING -> {
+ 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
}
@@ -121,11 +134,20 @@ internal class InternalServer(
field = value
}
- internal fun assignHost(host: Host) {
- this.host = host
+ /**
+ * 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 InternalServer && uid == other.uid
+ 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/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayAllocationPolicy.kt b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayAllocationPolicy.kt
index ed1dc662..2c953f8b 100644
--- a/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayAllocationPolicy.kt
+++ b/simulator/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/scheduler/ReplayAllocationPolicy.kt
@@ -20,14 +20,11 @@
* SOFTWARE.
*/
-package org.opendc.compute.simulator.allocation
+package org.opendc.compute.service.scheduler
import mu.KotlinLogging
import org.opendc.compute.api.Server
import org.opendc.compute.service.internal.HostView
-import org.opendc.compute.service.scheduler.AllocationPolicy
-
-private val logger = KotlinLogging.logger {}
/**
* Policy replaying VM-cluster assignment.
@@ -36,6 +33,8 @@ private val logger = KotlinLogging.logger {}
* assigned the VM image.
*/
public class ReplayAllocationPolicy(private val vmPlacements: Map<String, String>) : AllocationPolicy {
+ private val logger = KotlinLogging.logger {}
+
override fun invoke(): AllocationPolicy.Logic = object : AllocationPolicy.Logic {
override fun select(
hypervisors: Set<HostView>,
diff --git a/simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt b/simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt
new file mode 100644
index 00000000..ffec92ea
--- /dev/null
+++ b/simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt
@@ -0,0 +1,390 @@
+/*
+ * 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.delay
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.AfterEach
+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.AvailableMemoryAllocationPolicy
+import org.opendc.simulator.utils.DelayControllerClockAdapter
+import org.opendc.trace.core.EventTracer
+import java.util.*
+
+/**
+ * Test suite for the [ComputeService] interface.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class ComputeServiceTest {
+ lateinit var scope: TestCoroutineScope
+ lateinit var service: ComputeService
+
+ @BeforeEach
+ fun setUp() {
+ scope = TestCoroutineScope()
+ val clock = DelayControllerClockAdapter(scope)
+ val tracer = EventTracer(clock)
+ val policy = AvailableMemoryAllocationPolicy()
+ service = ComputeService(scope.coroutineContext, clock, tracer, policy)
+ }
+
+ @AfterEach
+ fun tearDown() {
+ scope.cleanupTestCoroutines()
+ }
+
+ @Test
+ fun testClientClose() = scope.runBlockingTest {
+ 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.runBlockingTest {
+ 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.runBlockingTest {
+ service.close()
+ assertThrows<IllegalStateException> {
+ service.newClient()
+ }
+ }
+
+ @Test
+ fun testAddHost() = scope.runBlockingTest {
+ 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.runBlockingTest {
+ 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.runBlockingTest {
+ 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.runBlockingTest {
+ 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.runBlockingTest {
+ 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.runBlockingTest {
+ 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.runBlockingTest {
+ 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.runBlockingTest {
+ 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)
+
+ 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.runBlockingTest {
+ 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)
+
+ 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.runBlockingTest {
+ 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.runBlockingTest {
+ 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.runBlockingTest {
+ 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/simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt b/simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt
new file mode 100644
index 00000000..18d698c6
--- /dev/null
+++ b/simulator/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/simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt b/simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt
new file mode 100644
index 00000000..e1cb0128
--- /dev/null
+++ b/simulator/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/simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt b/simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt
new file mode 100644
index 00000000..81cb45df
--- /dev/null
+++ b/simulator/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.test.runBlockingTest
+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 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() = runBlockingTest {
+ 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() = runBlockingTest {
+ 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() = runBlockingTest {
+ 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() = runBlockingTest {
+ 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() = runBlockingTest {
+ 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() = runBlockingTest {
+ 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() = runBlockingTest {
+ 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() = runBlockingTest {
+ 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() = runBlockingTest {
+ 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() = runBlockingTest {
+ 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() = runBlockingTest {
+ 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() = runBlockingTest {
+ 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/simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/scheduler/AllocationPolicyTest.kt b/simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/scheduler/AllocationPolicyTest.kt
new file mode 100644
index 00000000..db377914
--- /dev/null
+++ b/simulator/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/scheduler/AllocationPolicyTest.kt
@@ -0,0 +1,219 @@
+/*
+ * 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 io.mockk.every
+import io.mockk.mockk
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+import org.opendc.compute.api.Server
+import org.opendc.compute.service.internal.HostView
+import java.util.*
+import java.util.stream.Stream
+import kotlin.random.Random
+
+/**
+ * Test suite for the [AllocationPolicy] interface.
+ */
+internal class AllocationPolicyTest {
+ @ParameterizedTest
+ @MethodSource("activeServersArgs")
+ fun testActiveServersPolicy(
+ reversed: Boolean,
+ hosts: Set<HostView>,
+ server: Server,
+ expectedHost: HostView?
+ ) {
+ val policy = NumberOfActiveServersAllocationPolicy(reversed)
+ assertEquals(expectedHost, policy.invoke().select(hosts, server))
+ }
+
+ @ParameterizedTest
+ @MethodSource("availableMemoryArgs")
+ fun testAvailableMemoryPolicy(
+ reversed: Boolean,
+ hosts: Set<HostView>,
+ server: Server,
+ expectedHost: HostView?
+ ) {
+ val policy = AvailableMemoryAllocationPolicy(reversed)
+ assertEquals(expectedHost, policy.invoke().select(hosts, server))
+ }
+
+ @ParameterizedTest
+ @MethodSource("availableCoreMemoryArgs")
+ fun testAvailableCoreMemoryPolicy(
+ reversed: Boolean,
+ hosts: Set<HostView>,
+ server: Server,
+ expectedHost: HostView?
+ ) {
+ val policy = AvailableMemoryAllocationPolicy(reversed)
+ assertEquals(expectedHost, policy.invoke().select(hosts, server))
+ }
+
+ @ParameterizedTest
+ @MethodSource("provisionedCoresArgs")
+ fun testProvisionedPolicy(
+ reversed: Boolean,
+ hosts: Set<HostView>,
+ server: Server,
+ expectedHost: HostView?
+ ) {
+ val policy = ProvisionedCoresAllocationPolicy(reversed)
+ assertEquals(expectedHost, policy.invoke().select(hosts, server))
+ }
+
+ @Suppress("unused")
+ private companion object {
+ /**
+ * Test arguments for the [NumberOfActiveServersAllocationPolicy].
+ */
+ @JvmStatic
+ fun activeServersArgs(): Stream<Arguments> {
+ val random = Random(1)
+ val hosts = List(4) { i ->
+ val view = mockk<HostView>()
+ every { view.host.uid } returns UUID(0, i.toLong())
+ every { view.host.model.cpuCount } returns random.nextInt(1, 16)
+ every { view.host.model.memorySize } returns random.nextLong(1024, 1024 * 1024)
+ every { view.availableMemory } returns random.nextLong(0, view.host.model.memorySize)
+ every { view.numberOfActiveServers } returns random.nextInt(0, 6)
+ every { view.provisionedCores } returns random.nextInt(0, view.host.model.cpuCount)
+ every { view.toString() } returns "HostView[$i,numberOfActiveServers=${view.numberOfActiveServers}]"
+ view
+ }
+
+ val servers = List(2) {
+ val server = mockk<Server>()
+ every { server.flavor.cpuCount } returns random.nextInt(1, 8)
+ every { server.flavor.memorySize } returns random.nextLong(1024, 1024 * 512)
+ server
+ }
+
+ return Stream.of(
+ Arguments.of(false, hosts.toSet(), servers[0], hosts[2]),
+ Arguments.of(false, hosts.toSet(), servers[1], hosts[1]),
+ Arguments.of(true, hosts.toSet(), servers[1], hosts[0]),
+ )
+ }
+
+ /**
+ * Test arguments for the [AvailableCoreMemoryAllocationPolicy].
+ */
+ @JvmStatic
+ fun availableCoreMemoryArgs(): Stream<Arguments> {
+ val random = Random(1)
+ val hosts = List(4) { i ->
+ val view = mockk<HostView>()
+ every { view.host.uid } returns UUID(0, i.toLong())
+ every { view.host.model.cpuCount } returns random.nextInt(1, 16)
+ every { view.host.model.memorySize } returns random.nextLong(1024, 1024 * 1024)
+ every { view.availableMemory } returns random.nextLong(0, view.host.model.memorySize)
+ every { view.numberOfActiveServers } returns random.nextInt(0, 6)
+ every { view.provisionedCores } returns random.nextInt(0, view.host.model.cpuCount)
+ every { view.toString() } returns "HostView[$i,availableMemory=${view.availableMemory}]"
+ view
+ }
+
+ val servers = List(2) {
+ val server = mockk<Server>()
+ every { server.flavor.cpuCount } returns random.nextInt(1, 8)
+ every { server.flavor.memorySize } returns random.nextLong(1024, 1024 * 512)
+ server
+ }
+
+ return Stream.of(
+ Arguments.of(false, hosts.toSet(), servers[0], hosts[2]),
+ Arguments.of(false, hosts.toSet(), servers[1], hosts[2]),
+ Arguments.of(true, hosts.toSet(), servers[1], hosts[1]),
+ )
+ }
+
+ /**
+ * Test arguments for the [AvailableMemoryAllocationPolicy].
+ */
+ @JvmStatic
+ fun availableMemoryArgs(): Stream<Arguments> {
+ val random = Random(1)
+ val hosts = List(4) { i ->
+ val view = mockk<HostView>()
+ every { view.host.uid } returns UUID(0, i.toLong())
+ every { view.host.model.cpuCount } returns random.nextInt(1, 16)
+ every { view.host.model.memorySize } returns random.nextLong(1024, 1024 * 1024)
+ every { view.availableMemory } returns random.nextLong(0, view.host.model.memorySize)
+ every { view.numberOfActiveServers } returns random.nextInt(0, 6)
+ every { view.provisionedCores } returns random.nextInt(0, view.host.model.cpuCount)
+ every { view.toString() } returns "HostView[$i,availableMemory=${view.availableMemory}]"
+ view
+ }
+
+ val servers = List(2) {
+ val server = mockk<Server>()
+ every { server.flavor.cpuCount } returns random.nextInt(1, 8)
+ every { server.flavor.memorySize } returns random.nextLong(1024, 1024 * 512)
+ server
+ }
+
+ return Stream.of(
+ Arguments.of(false, hosts.toSet(), servers[0], hosts[2]),
+ Arguments.of(false, hosts.toSet(), servers[1], hosts[2]),
+ Arguments.of(true, hosts.toSet(), servers[1], hosts[1]),
+ )
+ }
+
+ /**
+ * Test arguments for the [ProvisionedCoresAllocationPolicy].
+ */
+ @JvmStatic
+ fun provisionedCoresArgs(): Stream<Arguments> {
+ val random = Random(1)
+ val hosts = List(4) { i ->
+ val view = mockk<HostView>()
+ every { view.host.uid } returns UUID(0, i.toLong())
+ every { view.host.model.cpuCount } returns random.nextInt(1, 16)
+ every { view.host.model.memorySize } returns random.nextLong(1024, 1024 * 1024)
+ every { view.availableMemory } returns random.nextLong(0, view.host.model.memorySize)
+ every { view.numberOfActiveServers } returns random.nextInt(0, 6)
+ every { view.provisionedCores } returns random.nextInt(0, view.host.model.cpuCount)
+ every { view.toString() } returns "HostView[$i,provisionedCores=${view.provisionedCores}]"
+ view
+ }
+
+ val servers = List(2) {
+ val server = mockk<Server>()
+ every { server.flavor.cpuCount } returns random.nextInt(1, 8)
+ every { server.flavor.memorySize } returns random.nextLong(1024, 1024 * 512)
+ server
+ }
+
+ return Stream.of(
+ Arguments.of(false, hosts.toSet(), servers[0], hosts[2]),
+ Arguments.of(false, hosts.toSet(), servers[1], hosts[0]),
+ Arguments.of(true, hosts.toSet(), servers[1], hosts[0]),
+ )
+ }
+ }
+}
diff --git a/simulator/opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml b/simulator/opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml
new file mode 100644
index 00000000..0dfb75f2
--- /dev/null
+++ b/simulator/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>