summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt1
-rw-r--r--opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/telemetry/SchedulerStats.kt2
-rw-r--r--opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/ComputeMetricReader.kt5
-rw-r--r--opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/table/ServiceData.kt3
-rw-r--r--opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/table/ServiceTableReader.kt5
-rw-r--r--opendc-experiments/opendc-experiments-compute/src/test/kotlin/org/opendc/experiments/compute/export/parquet/ServiceDataWriterTest.kt1
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Job.kt7
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/User.kt31
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/UserAccounting.kt34
-rw-r--r--opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/JobManager.kt10
-rw-r--r--opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/OpenDCRunner.kt29
-rw-r--r--opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/internal/JobManagerImpl.kt15
-rw-r--r--opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/internal/WebComputeMonitor.kt33
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt11
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt81
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt3
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt88
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.kt (renamed from opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/GenericExceptionMapper.kt)8
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt2
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/UserResource.kt45
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt22
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt2
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt11
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt128
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt44
-rw-r--r--opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt15
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql22
-rw-r--r--opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/runner/JobResourceTest.kt10
-rw-r--r--opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/UserResourceTest.kt69
-rw-r--r--opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/service/UserAccountingServiceTest.kt203
-rw-r--r--opendc-web/opendc-web-ui/src/api/users.js32
-rw-r--r--opendc-web/opendc-web-ui/src/components/AppHeaderUser.js32
-rw-r--r--opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js1
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddContainer.js3
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListComponent.js7
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js3
-rw-r--r--opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitType.js25
-rw-r--r--opendc-web/opendc-web-ui/src/data/query.js2
-rw-r--r--opendc-web/opendc-web-ui/src/data/user.js40
-rw-r--r--opendc-web/opendc-web-ui/src/util/available-metrics.js27
40 files changed, 1016 insertions, 96 deletions
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
index caa95e09..0fe016aa 100644
--- 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
@@ -295,6 +295,7 @@ internal class ComputeServiceImpl(
_attemptsSuccess,
_attemptsFailure,
_attemptsError,
+ servers.size,
_serversPending,
_serversActive
)
diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/telemetry/SchedulerStats.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/telemetry/SchedulerStats.kt
index 4dc70286..6e9f458a 100644
--- a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/telemetry/SchedulerStats.kt
+++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/telemetry/SchedulerStats.kt
@@ -32,6 +32,7 @@ import org.opendc.compute.service.ComputeService
* @property attemptsSuccess Scheduling attempts that resulted into an allocation onto a host.
* @property attemptsFailure The number of failed scheduling attempt due to insufficient capacity at the moment.
* @property attemptsError The number of scheduling attempts that failed due to system error.
+ * @property serversTotal The number of servers registered with the service.
* @property serversPending The number of servers that are pending to be scheduled.
* @property serversActive The number of servers that are currently managed by the service and running.
*/
@@ -41,6 +42,7 @@ public data class SchedulerStats(
val attemptsSuccess: Long,
val attemptsFailure: Long,
val attemptsError: Long,
+ val serversTotal: Int,
val serversPending: Int,
val serversActive: Int
)
diff --git a/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/ComputeMetricReader.kt b/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/ComputeMetricReader.kt
index 7e89eab1..ac058171 100644
--- a/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/ComputeMetricReader.kt
+++ b/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/ComputeMetricReader.kt
@@ -138,6 +138,10 @@ public class ComputeMetricReader(
get() = _hostsDown
private var _hostsDown = 0
+ override val serversTotal: Int
+ get() = _serversTotal
+ private var _serversTotal = 0
+
override val serversPending: Int
get() = _serversPending
private var _serversPending = 0
@@ -167,6 +171,7 @@ public class ComputeMetricReader(
val stats = service.getSchedulerStats()
_hostsUp = stats.hostsAvailable
_hostsDown = stats.hostsUnavailable
+ _serversTotal = stats.serversTotal
_serversPending = stats.serversPending
_serversActive = stats.serversActive
_attemptsSuccess = stats.attemptsSuccess.toInt()
diff --git a/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/table/ServiceData.kt b/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/table/ServiceData.kt
index 394c6bd6..5b6960a3 100644
--- a/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/table/ServiceData.kt
+++ b/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/table/ServiceData.kt
@@ -31,6 +31,7 @@ public data class ServiceData(
val timestamp: Instant,
val hostsUp: Int,
val hostsDown: Int,
+ val serversTotal: Int,
val serversPending: Int,
val serversActive: Int,
val attemptsSuccess: Int,
@@ -42,5 +43,5 @@ public data class ServiceData(
* Convert a [ServiceTableReader] into a persistent object.
*/
public fun ServiceTableReader.toServiceData(): ServiceData {
- return ServiceData(timestamp, hostsUp, hostsDown, serversPending, serversActive, attemptsSuccess, attemptsFailure, attemptsError)
+ return ServiceData(timestamp, hostsUp, hostsDown, serversTotal, serversPending, serversActive, attemptsSuccess, attemptsFailure, attemptsError)
}
diff --git a/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/table/ServiceTableReader.kt b/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/table/ServiceTableReader.kt
index 0155a879..bb926298 100644
--- a/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/table/ServiceTableReader.kt
+++ b/opendc-experiments/opendc-experiments-compute/src/main/kotlin/org/opendc/experiments/compute/telemetry/table/ServiceTableReader.kt
@@ -44,6 +44,11 @@ public interface ServiceTableReader {
public val hostsDown: Int
/**
+ * The number of servers that are registered with the compute service..
+ */
+ public val serversTotal: Int
+
+ /**
* The number of servers that are pending to be scheduled.
*/
public val serversPending: Int
diff --git a/opendc-experiments/opendc-experiments-compute/src/test/kotlin/org/opendc/experiments/compute/export/parquet/ServiceDataWriterTest.kt b/opendc-experiments/opendc-experiments-compute/src/test/kotlin/org/opendc/experiments/compute/export/parquet/ServiceDataWriterTest.kt
index 20301185..978cc9d4 100644
--- a/opendc-experiments/opendc-experiments-compute/src/test/kotlin/org/opendc/experiments/compute/export/parquet/ServiceDataWriterTest.kt
+++ b/opendc-experiments/opendc-experiments-compute/src/test/kotlin/org/opendc/experiments/compute/export/parquet/ServiceDataWriterTest.kt
@@ -56,6 +56,7 @@ class ServiceDataWriterTest {
override val timestamp: Instant = Instant.now()
override val hostsUp: Int = 1
override val hostsDown: Int = 0
+ override val serversTotal: Int = 1
override val serversPending: Int = 1
override val serversActive: Int = 1
override val attemptsSuccess: Int = 1
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Job.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Job.kt
index dfaaa09e..4f21f0bb 100644
--- a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Job.kt
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/runner/Job.kt
@@ -36,11 +36,16 @@ public data class Job(
val state: JobState,
val createdAt: Instant,
val updatedAt: Instant,
+ val runtime: Int,
val results: Map<String, Any>? = null
) {
/**
* A request to update the state of a job.
+ *
+ * @property state The next state of the job.
+ * @property runtime The runtime of the job (in seconds).
+ * @property results The results of the job.
*/
@Schema(name = "Runner.Job.Update")
- public data class Update(val state: JobState, val results: Map<String, Any>? = null)
+ public data class Update(val state: JobState, val runtime: Int, val results: Map<String, Any>? = null)
}
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/User.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/User.kt
new file mode 100644
index 00000000..f18cda61
--- /dev/null
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/User.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.web.proto.user
+
+/**
+ * A user of OpenDC.
+ */
+public data class User(
+ val userId: String,
+ val accounting: UserAccounting
+)
diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/UserAccounting.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/UserAccounting.kt
new file mode 100644
index 00000000..2441983a
--- /dev/null
+++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/UserAccounting.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.web.proto.user
+
+import java.time.LocalDate
+
+/**
+ * Accounting data for a user.
+ */
+public data class UserAccounting(
+ val periodEnd: LocalDate,
+ val simulationTime: Int,
+ val simulationTimeBudget: Int
+)
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/JobManager.kt b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/JobManager.kt
index 50aa03d8..d6c06889 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/JobManager.kt
+++ b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/JobManager.kt
@@ -42,18 +42,22 @@ public interface JobManager {
/**
* Update the heartbeat of the specified job.
+ *
+ * @param id The identifier of the job.
+ * @param runtime The total runtime of the job.
+ * @return `true` if the job can continue, `false` if the job has been cancelled.
*/
- public fun heartbeat(id: Long)
+ public fun heartbeat(id: Long, runtime: Int): Boolean
/**
* Mark the job as failed.
*/
- public fun fail(id: Long)
+ public fun fail(id: Long, runtime: Int)
/**
* Persist the specified results for the specified job.
*/
- public fun finish(id: Long, results: Map<String, Any>)
+ public fun finish(id: Long, runtime: Int, results: Map<String, Any>)
public companion object {
/**
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/OpenDCRunner.kt b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/OpenDCRunner.kt
index 226bad47..1bc4e938 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/OpenDCRunner.kt
+++ b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/OpenDCRunner.kt
@@ -48,6 +48,8 @@ import org.opendc.web.proto.runner.Topology
import org.opendc.web.runner.internal.WebComputeMonitor
import java.io.File
import java.time.Duration
+import java.time.Instant
+import java.time.temporal.ChronoUnit
import java.util.Random
import java.util.UUID
import java.util.concurrent.Executors
@@ -72,7 +74,7 @@ public class OpenDCRunner(
private val manager: JobManager,
private val tracePath: File,
parallelism: Int = Runtime.getRuntime().availableProcessors(),
- private val jobTimeout: Duration = Duration.ofMillis(10),
+ private val jobTimeout: Duration = Duration.ofMinutes(10),
private val pollInterval: Duration = Duration.ofSeconds(30),
private val heartbeatInterval: Duration = Duration.ofMinutes(1)
) : Runnable {
@@ -144,9 +146,15 @@ public class OpenDCRunner(
override fun compute() {
val id = job.id
val scenario = job.scenario
+ val startTime = Instant.now()
+ val currentThread = Thread.currentThread()
val heartbeat = scheduler.scheduleWithFixedDelay(
- { manager.heartbeat(id) },
+ {
+ if (!manager.heartbeat(id, startTime.secondsSince())) {
+ currentThread.interrupt()
+ }
+ },
0,
heartbeatInterval.toMillis(),
TimeUnit.MILLISECONDS
@@ -163,12 +171,14 @@ public class OpenDCRunner(
}
val results = invokeAll(jobs).map { it.rawResult }
- logger.info { "Finished simulation for job $id" }
-
heartbeat.cancel(true)
+ val duration = startTime.secondsSince()
+ logger.info { "Finished simulation for job $id (in $duration seconds)" }
+
manager.finish(
id,
+ duration,
mapOf(
"total_requested_burst" to results.map { it.totalActiveTime + it.totalIdleTime },
"total_granted_burst" to results.map { it.totalActiveTime },
@@ -190,19 +200,26 @@ public class OpenDCRunner(
} catch (e: Exception) {
// Check whether the job failed due to exceeding its time budget
if (Thread.interrupted()) {
- logger.info { "Simulation job $id exceeded time limit" }
+ logger.info { "Simulation job $id exceeded time limit (${startTime.secondsSince()} seconds)" }
} else {
logger.info(e) { "Simulation job $id failed" }
}
try {
heartbeat.cancel(true)
- manager.fail(id)
+ manager.fail(id, startTime.secondsSince())
} catch (e: Throwable) {
logger.error(e) { "Failed to update job" }
}
}
}
+
+ /**
+ * Calculate the seconds since the specified instant.
+ */
+ private fun Instant.secondsSince(): Int {
+ return ChronoUnit.SECONDS.between(this, Instant.now()).toInt()
+ }
}
/**
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/internal/JobManagerImpl.kt b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/internal/JobManagerImpl.kt
index 39a6851c..5b1b7132 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/internal/JobManagerImpl.kt
+++ b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/internal/JobManagerImpl.kt
@@ -37,22 +37,23 @@ internal class JobManagerImpl(private val client: OpenDCRunnerClient) : JobManag
override fun claim(id: Long): Boolean {
return try {
- client.jobs.update(id, Job.Update(JobState.CLAIMED))
+ client.jobs.update(id, Job.Update(JobState.CLAIMED, 0))
true
} catch (e: IllegalStateException) {
false
}
}
- override fun heartbeat(id: Long) {
- client.jobs.update(id, Job.Update(JobState.RUNNING))
+ override fun heartbeat(id: Long, runtime: Int): Boolean {
+ val res = client.jobs.update(id, Job.Update(JobState.RUNNING, runtime))
+ return res?.state != JobState.FAILED
}
- override fun fail(id: Long) {
- client.jobs.update(id, Job.Update(JobState.FAILED))
+ override fun fail(id: Long, runtime: Int) {
+ client.jobs.update(id, Job.Update(JobState.FAILED, runtime))
}
- override fun finish(id: Long, results: Map<String, Any>) {
- client.jobs.update(id, Job.Update(JobState.FINISHED, results))
+ override fun finish(id: Long, runtime: Int, results: Map<String, Any>) {
+ client.jobs.update(id, Job.Update(JobState.FINISHED, runtime))
}
}
diff --git a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/internal/WebComputeMonitor.kt b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/internal/WebComputeMonitor.kt
index 4db70d3d..d6722115 100644
--- a/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/internal/WebComputeMonitor.kt
+++ b/opendc-web/opendc-web-runner/src/main/kotlin/org/opendc/web/runner/internal/WebComputeMonitor.kt
@@ -24,8 +24,9 @@ package org.opendc.web.runner.internal
import org.opendc.experiments.compute.telemetry.ComputeMonitor
import org.opendc.experiments.compute.telemetry.table.HostTableReader
+import org.opendc.experiments.compute.telemetry.table.ServiceData
import org.opendc.experiments.compute.telemetry.table.ServiceTableReader
-import kotlin.math.max
+import org.opendc.experiments.compute.telemetry.table.toServiceData
import kotlin.math.roundToLong
/**
@@ -76,30 +77,20 @@ internal class WebComputeMonitor : ComputeMonitor {
val count: Long
)
- private var serviceMetrics: AggregateServiceMetrics = AggregateServiceMetrics()
+ private lateinit var serviceData: ServiceData
override fun record(reader: ServiceTableReader) {
- serviceMetrics = AggregateServiceMetrics(
- max(reader.attemptsSuccess, serviceMetrics.vmTotalCount),
- max(reader.serversPending, serviceMetrics.vmWaitingCount),
- max(reader.serversActive, serviceMetrics.vmActiveCount),
- max(0, serviceMetrics.vmInactiveCount),
- max(reader.attemptsFailure, serviceMetrics.vmFailedCount)
- )
+ serviceData = reader.toServiceData()
}
- private data class AggregateServiceMetrics(
- val vmTotalCount: Int = 0,
- val vmWaitingCount: Int = 0,
- val vmActiveCount: Int = 0,
- val vmInactiveCount: Int = 0,
- val vmFailedCount: Int = 0
- )
-
/**
* Collect the results of the simulation.
*/
fun collectResults(): Results {
+ val hostAggregateMetrics = hostAggregateMetrics
+ val hostMetrics = hostMetrics
+ val serviceData = serviceData
+
return Results(
hostAggregateMetrics.totalActiveTime,
hostAggregateMetrics.totalIdleTime,
@@ -112,10 +103,10 @@ internal class WebComputeMonitor : ComputeMonitor {
hostAggregateMetrics.totalPowerDraw,
hostAggregateMetrics.totalFailureSlices.roundToLong(),
hostAggregateMetrics.totalFailureVmSlices.roundToLong(),
- serviceMetrics.vmTotalCount,
- serviceMetrics.vmWaitingCount,
- serviceMetrics.vmInactiveCount,
- serviceMetrics.vmFailedCount
+ serviceData.serversTotal,
+ serviceData.serversPending,
+ serviceData.serversTotal - serviceData.serversPending - serviceData.serversActive,
+ serviceData.attemptsError + serviceData.attemptsFailure
)
}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt
index c07e07f0..84a71acf 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt
@@ -55,7 +55,7 @@ import javax.persistence.Table
name = "Job.updateOne",
query = """
UPDATE Job j
- SET j.state = :newState, j.updatedAt = :updatedAt, j.results = :results
+ SET j.state = :newState, j.updatedAt = :updatedAt, j.runtime = :runtime, j.results = :results
WHERE j.id = :id AND j.state = :oldState
"""
)
@@ -66,6 +66,9 @@ class Job(
@GeneratedValue(strategy = GenerationType.AUTO)
val id: Long,
+ @Column(name = "created_by", nullable = false, updatable = false)
+ val createdBy: String,
+
@OneToOne(optional = false, mappedBy = "job", fetch = FetchType.EAGER)
@JoinColumn(name = "scenario_id", nullable = false)
val scenario: Scenario,
@@ -92,6 +95,12 @@ class Job(
var state: JobState = JobState.PENDING
/**
+ * The runtime of the job (in seconds).
+ */
+ @Column(nullable = false)
+ var runtime: Int = 0
+
+ /**
* Experiment results in JSON
*/
@Type(type = "json")
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt
new file mode 100644
index 00000000..5b813044
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.web.server.model
+
+import java.time.LocalDate
+import javax.persistence.Column
+import javax.persistence.Entity
+import javax.persistence.Id
+import javax.persistence.NamedQueries
+import javax.persistence.NamedQuery
+import javax.persistence.Table
+
+/**
+ * Entity to track the number of simulation minutes used by a user.
+ */
+@Entity
+@Table(name = "user_accounting")
+@NamedQueries(
+ value = [
+ NamedQuery(
+ name = "UserAccounting.consumeBudget",
+ query = """
+ UPDATE UserAccounting a
+ SET a.simulationTime = a.simulationTime + :seconds
+ WHERE a.userId = :userId AND a.periodEnd = :periodEnd
+ """
+ ),
+ NamedQuery(
+ name = "UserAccounting.resetBudget",
+ query = """
+ UPDATE UserAccounting a
+ SET a.periodEnd = :periodEnd, a.simulationTime = :seconds
+ WHERE a.userId = :userId AND a.periodEnd = :oldPeriodEnd
+ """
+ )
+ ]
+)
+class UserAccounting(
+ @Id
+ @Column(name = "user_id", nullable = false)
+ val userId: String,
+
+ /**
+ * The end of the accounting period.
+ */
+ @Column(name = "period_end", nullable = false)
+ var periodEnd: LocalDate,
+
+ /**
+ * The number of simulation seconds to be used per accounting period.
+ */
+ @Column(name = "simulation_time_budget", nullable = false)
+ var simulationTimeBudget: Int
+) {
+ /**
+ * The number of simulation seconds used in this period. This number should reset once the accounting period has
+ * been reached.
+ */
+ @Column(name = "simulation_time", nullable = false)
+ var simulationTime: Int = 0
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt
index 5fee07a3..e9bf0af0 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt
@@ -79,12 +79,13 @@ class JobRepository @Inject constructor(private val em: EntityManager) {
* @param results The results to possible set.
* @return `true` when the update succeeded`, `false` when there was a conflict.
*/
- fun updateOne(job: Job, newState: JobState, time: Instant, results: Map<String, Any>?): Boolean {
+ fun updateOne(job: Job, newState: JobState, time: Instant, runtime: Int, results: Map<String, Any>?): Boolean {
val count = em.createNamedQuery("Job.updateOne")
.setParameter("id", job.id)
.setParameter("oldState", job.state)
.setParameter("newState", newState)
.setParameter("updatedAt", Instant.now())
+ .setParameter("runtime", runtime)
.setParameter("results", results)
.executeUpdate()
em.refresh(job)
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt
new file mode 100644
index 00000000..f0265d3d
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.web.server.repository
+
+import org.opendc.web.server.model.UserAccounting
+import java.time.LocalDate
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import javax.persistence.EntityManager
+
+/**
+ * A repository to manage [UserAccounting] entities.
+ */
+@ApplicationScoped
+class UserAccountingRepository @Inject constructor(private val em: EntityManager) {
+ /**
+ * Find the [UserAccounting] object for the specified [userId].
+ *
+ * @param userId The unique identifier of the user.
+ * @return The [UserAccounting] object or `null` if it does not exist.
+ */
+ fun findForUser(userId: String): UserAccounting? {
+ return em.find(UserAccounting::class.java, userId)
+ }
+
+ /**
+ * Save the specified [UserAccounting] object to the database.
+ */
+ fun save(accounting: UserAccounting) {
+ em.persist(accounting)
+ }
+
+ /**
+ * Atomically consume the budget for the specified [UserAccounting] object.
+ *
+ * @param accounting The [UserAccounting] object to update atomically.
+ * @param seconds The number of seconds to consume from the user.
+ * @return `true` when the update succeeded`, `false` when there was a conflict.
+ */
+ fun consumeBudget(accounting: UserAccounting, seconds: Int): Boolean {
+ val count = em.createNamedQuery("UserAccounting.consumeBudget")
+ .setParameter("userId", accounting.userId)
+ .setParameter("periodEnd", accounting.periodEnd)
+ .setParameter("seconds", seconds)
+ .executeUpdate()
+ em.refresh(accounting)
+ return count > 0
+ }
+
+ /**
+ * Atomically reset the budget for the specified [UserAccounting] object.
+ *
+ * @param accounting The [UserAccounting] object to update atomically.
+ * @param periodEnd The new end period for the budget.
+ * @param seconds The number of seconds that have already been consumed.
+ * @return `true` when the update succeeded`, `false` when there was a conflict.
+ */
+ fun resetBudget(accounting: UserAccounting, periodEnd: LocalDate, seconds: Int): Boolean {
+ val count = em.createNamedQuery("UserAccounting.resetBudget")
+ .setParameter("userId", accounting.userId)
+ .setParameter("oldPeriodEnd", accounting.periodEnd)
+ .setParameter("periodEnd", periodEnd)
+ .setParameter("seconds", seconds)
+ .executeUpdate()
+ em.refresh(accounting)
+ return count > 0
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/GenericExceptionMapper.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.kt
index d8df72e0..aa046abf 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/GenericExceptionMapper.kt
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.kt
@@ -30,12 +30,12 @@ import javax.ws.rs.ext.ExceptionMapper
import javax.ws.rs.ext.Provider
/**
- * Helper class to transform an exception into an JSON error response.
+ * Helper class to transform a [WebApplicationException] into an JSON error response.
*/
@Provider
-class GenericExceptionMapper : ExceptionMapper<Exception> {
- override fun toResponse(exception: Exception): Response {
- val code = if (exception is WebApplicationException) exception.response.status else 500
+class WebApplicationExceptionMapper : ExceptionMapper<WebApplicationException> {
+ override fun toResponse(exception: WebApplicationException): Response {
+ val code = exception.response.status
return Response.status(code)
.entity(ProtocolError(code, exception.message ?: "Unknown error"))
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt
index 4aa2f6a1..d0432360 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt
@@ -65,7 +65,7 @@ class JobResource @Inject constructor(private val jobService: JobService) {
@Transactional
fun update(@PathParam("job") id: Long, @Valid update: Job.Update): Job {
return try {
- jobService.updateState(id, update.state, update.results)
+ jobService.updateState(id, update.state, update.runtime, update.results)
?: throw WebApplicationException("Job not found", 404)
} catch (e: IllegalArgumentException) {
throw WebApplicationException(e, 400)
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/UserResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/UserResource.kt
new file mode 100644
index 00000000..d640cc08
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/UserResource.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.web.server.rest.user
+
+import io.quarkus.security.identity.SecurityIdentity
+import org.opendc.web.proto.user.User
+import org.opendc.web.server.service.UserService
+import javax.annotation.security.RolesAllowed
+import javax.inject.Inject
+import javax.ws.rs.GET
+import javax.ws.rs.Path
+
+/**
+ * A resource representing the active user.
+ */
+@Path("/users")
+@RolesAllowed("openid")
+class UserResource @Inject constructor(private val userService: UserService, private val identity: SecurityIdentity) {
+ /**
+ * Get the current active user data.
+ */
+ @GET
+ @Path("me")
+ fun get(): User = userService.getUser(identity)
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt
index 6b49e8b6..a0ebd4f4 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt
@@ -33,7 +33,10 @@ import javax.inject.Inject
* Service for managing [Job]s.
*/
@ApplicationScoped
-class JobService @Inject constructor(private val repository: JobRepository) {
+class JobService @Inject constructor(
+ private val repository: JobRepository,
+ private val accountingService: UserAccountingService
+) {
/**
* Query the pending simulation jobs.
*/
@@ -50,8 +53,13 @@ class JobService @Inject constructor(private val repository: JobRepository) {
/**
* Atomically update the state of a [Job].
+ *
+ * @param id The identifier of the job.
+ * @param newState The next state for the job.
+ * @param runtime The runtime of the job (in seconds).
+ * @param results The potential results of the job.
*/
- fun updateState(id: Long, newState: JobState, results: Map<String, Any>?): Job? {
+ fun updateState(id: Long, newState: JobState, runtime: Int, results: Map<String, Any>?): Job? {
val entity = repository.findOne(id) ?: return null
val state = entity.state
if (!state.isTransitionLegal(newState)) {
@@ -59,7 +67,15 @@ class JobService @Inject constructor(private val repository: JobRepository) {
}
val now = Instant.now()
- if (!repository.updateOne(entity, newState, now, results)) {
+ var nextState = newState
+ val consumedBudget = (runtime - entity.runtime).coerceAtLeast(1)
+
+ // Check whether the user still has any simulation budget left
+ if (accountingService.consumeSimulationBudget(entity.createdBy, consumedBudget) && nextState == JobState.RUNNING) {
+ nextState = JobState.FAILED // User has consumed all their budget; cancel the job
+ }
+
+ if (!repository.updateOne(entity, nextState, now, runtime, results)) {
throw IllegalStateException("Conflicting update")
}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt
index 1dcc95ee..465ac2df 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt
@@ -49,7 +49,7 @@ internal fun Portfolio.toRunnerDto(): org.opendc.web.proto.runner.Portfolio {
* Convert a [Job] into a runner-facing DTO.
*/
internal fun Job.toRunnerDto(): org.opendc.web.proto.runner.Job {
- return org.opendc.web.proto.runner.Job(id, scenario.toRunnerDto(), state, createdAt, updatedAt, results)
+ return org.opendc.web.proto.runner.Job(id, scenario.toRunnerDto(), state, createdAt, updatedAt, runtime, results)
}
/**
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt
index fad4e56f..083f2451 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt
@@ -22,6 +22,7 @@
package org.opendc.web.server.service
+import org.opendc.web.proto.JobState
import org.opendc.web.server.model.Job
import org.opendc.web.server.model.Scenario
import org.opendc.web.server.model.Workload
@@ -44,7 +45,8 @@ class ScenarioService @Inject constructor(
private val portfolioRepository: PortfolioRepository,
private val topologyRepository: TopologyRepository,
private val traceRepository: TraceRepository,
- private val scenarioRepository: ScenarioRepository
+ private val scenarioRepository: ScenarioRepository,
+ private val accountingService: UserAccountingService
) {
/**
* List all [Scenario]s that belong a certain portfolio.
@@ -123,7 +125,12 @@ class ScenarioService @Inject constructor(
request.phenomena,
request.schedulerName
)
- val job = Job(0, scenario, now, portfolio.targets.repeats)
+ val job = Job(0, userId, scenario, now, portfolio.targets.repeats)
+
+ // Fail the job if there is not enough budget for the simulation
+ if (!accountingService.hasSimulationBudget(userId)) {
+ job.state = JobState.FAILED
+ }
scenario.job = job
portfolio.scenarios.add(scenario)
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt
new file mode 100644
index 00000000..11066bfb
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.web.server.service
+
+import org.eclipse.microprofile.config.inject.ConfigProperty
+import org.opendc.web.server.model.UserAccounting
+import org.opendc.web.server.repository.UserAccountingRepository
+import java.time.Duration
+import java.time.LocalDate
+import java.time.temporal.TemporalAdjusters
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+import javax.persistence.EntityExistsException
+import org.opendc.web.proto.user.UserAccounting as UserAccountingDto
+
+/**
+ * Service for tracking the simulation budget of users.
+ *
+ * @param repository The [UserAccountingRepository] used to communicate with the database.
+ * @param simulationBudget The default simulation budget for new users.
+ */
+@ApplicationScoped
+class UserAccountingService @Inject constructor(
+ private val repository: UserAccountingRepository,
+ @ConfigProperty(name = "opendc.accounting.simulation-budget", defaultValue = "2000m")
+ private val simulationBudget: Duration
+) {
+ /**
+ * Return the [UserAccountingDto] object for the user with the specified [userId]. If the object does not exist in the
+ * database, a default value is constructed.
+ */
+ fun getAccounting(userId: String): UserAccountingDto {
+ val accounting = repository.findForUser(userId)
+ return if (accounting != null) {
+ UserAccountingDto(accounting.periodEnd, accounting.simulationTime, accounting.simulationTimeBudget)
+ } else {
+ UserAccountingDto(getNextAccountingPeriod(), 0, simulationBudget.toSeconds().toInt())
+ }
+ }
+
+ /**
+ * Determine whether the user with [userId] has any remaining simulation budget.
+ *
+ * @param userId The unique identifier of the user.
+ * @return `true` when the user still has budget left, `false` otherwise.
+ */
+ fun hasSimulationBudget(userId: String): Boolean {
+ val accounting = repository.findForUser(userId) ?: return true
+ val today = LocalDate.now()
+
+ // The accounting period must be over or there must be budget remaining.
+ return !today.isBefore(accounting.periodEnd) || accounting.simulationTimeBudget > accounting.simulationTime
+ }
+
+ /**
+ * Consume [seconds] from the simulation budget of the user with [userId].
+ *
+ * @param userId The unique identifier of the user.
+ * @param seconds The seconds to consume from the simulation budget.
+ * @param `true` if the user has consumed his full budget or `false` if there is still budget remaining.
+ */
+ fun consumeSimulationBudget(userId: String, seconds: Int): Boolean {
+ val today = LocalDate.now()
+ val nextAccountingPeriod = getNextAccountingPeriod(today)
+ val repository = repository
+
+ // We need to be careful to prevent conflicts in case of concurrency
+ // 1. First, we try to create the accounting object if it does not exist yet. This may fail if another instance
+ // creates the object concurrently.
+ // 2. Second, we check if the budget needs to be reset and try this atomically.
+ // 3. Finally, we atomically consume the budget from the object
+ // This is repeated three times in case there is a conflict
+ repeat(3) {
+ val accounting = repository.findForUser(userId)
+
+ if (accounting == null) {
+ try {
+ val newAccounting = UserAccounting(userId, nextAccountingPeriod, simulationBudget.toSeconds().toInt())
+ newAccounting.simulationTime = seconds
+ repository.save(newAccounting)
+
+ return newAccounting.simulationTime >= newAccounting.simulationTimeBudget
+ } catch (e: EntityExistsException) {
+ // Conflict due to concurrency; retry
+ }
+ } else {
+ val success = if (!today.isBefore(accounting.periodEnd)) {
+ repository.resetBudget(accounting, nextAccountingPeriod, seconds)
+ } else {
+ repository.consumeBudget(accounting, seconds)
+ }
+
+ if (success) {
+ return accounting.simulationTimeBudget <= accounting.simulationTime
+ }
+ }
+ }
+
+ throw IllegalStateException("Failed to allocate consume budget due to conflict")
+ }
+
+ /**
+ * Helper method to find next accounting period.
+ */
+ private fun getNextAccountingPeriod(today: LocalDate = LocalDate.now()): LocalDate {
+ return today.with(TemporalAdjusters.firstDayOfNextMonth())
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt
new file mode 100644
index 00000000..39352267
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.web.server.service
+
+import io.quarkus.security.identity.SecurityIdentity
+import org.opendc.web.proto.user.User
+import javax.enterprise.context.ApplicationScoped
+import javax.inject.Inject
+
+/**
+ * Service for managing [User]s.
+ */
+@ApplicationScoped
+class UserService @Inject constructor(private val accounting: UserAccountingService) {
+ /**
+ * Obtain the [User] object for the specified [identity].
+ */
+ fun getUser(identity: SecurityIdentity): User {
+ val userId = identity.principal.name
+ val accounting = accounting.getAccounting(userId)
+
+ return User(userId, accounting)
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt
index 4704c5ae..742a510c 100644
--- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt
+++ b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt
@@ -43,7 +43,7 @@ class QuarkusJobManager @Inject constructor(private val service: JobService) : J
@Transactional
override fun claim(id: Long): Boolean {
return try {
- service.updateState(id, JobState.CLAIMED, null)
+ service.updateState(id, JobState.CLAIMED, 0, null)
true
} catch (e: IllegalStateException) {
false
@@ -51,17 +51,18 @@ class QuarkusJobManager @Inject constructor(private val service: JobService) : J
}
@Transactional
- override fun heartbeat(id: Long) {
- service.updateState(id, JobState.RUNNING, null)
+ override fun heartbeat(id: Long, runtime: Int): Boolean {
+ val res = service.updateState(id, JobState.RUNNING, runtime, null)
+ return res?.state != JobState.FAILED
}
@Transactional
- override fun fail(id: Long) {
- service.updateState(id, JobState.FAILED, null)
+ override fun fail(id: Long, runtime: Int) {
+ service.updateState(id, JobState.FAILED, runtime, null)
}
@Transactional
- override fun finish(id: Long, results: Map<String, Any>) {
- service.updateState(id, JobState.FINISHED, results)
+ override fun finish(id: Long, runtime: Int, results: Map<String, Any>) {
+ service.updateState(id, JobState.FINISHED, runtime, results)
}
}
diff --git a/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql b/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql
index 183a70ea..1a0e4046 100644
--- a/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql
+++ b/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql
@@ -65,15 +65,27 @@ create table scenarios
create table jobs
(
- id bigint not null,
- created_at timestamp not null,
- repeats integer not null,
+ id bigint not null,
+ created_by varchar(255) not null,
+ created_at timestamp not null,
+ repeats integer not null,
results jsonb,
- state integer not null,
- updated_at timestamp not null,
+ state integer not null,
+ runtime integer not null,
+ updated_at timestamp not null,
primary key (id)
);
+-- User accounting
+create table user_accounting
+(
+ user_id varchar(255) not null,
+ period_end date not null,
+ simulation_time integer not null,
+ simulation_time_budget integer not null,
+ primary key (user_id)
+);
+
-- Workload traces available to the user.
create table traces
(
diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/runner/JobResourceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/runner/JobResourceTest.kt
index 9aca58e9..4a86c928 100644
--- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/runner/JobResourceTest.kt
+++ b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/runner/JobResourceTest.kt
@@ -63,7 +63,7 @@ class JobResourceTest {
private val dummyTopology = Topology(1, 1, "test", emptyList(), Instant.now(), Instant.now())
private val dummyTrace = Trace("bitbrains", "Bitbrains", "vm")
private val dummyScenario = Scenario(1, 1, dummyPortfolio, "test", Workload(dummyTrace, 1.0), dummyTopology, OperationalPhenomena(false, false), "test")
- private val dummyJob = Job(1, dummyScenario, JobState.PENDING, Instant.now(), Instant.now())
+ private val dummyJob = Job(1, dummyScenario, JobState.PENDING, Instant.now(), Instant.now(), 0)
@BeforeEach
fun setUp() {
@@ -151,10 +151,10 @@ class JobResourceTest {
@Test
@TestSecurity(user = "testUser", roles = ["runner"])
fun testUpdateNonExistent() {
- every { jobService.updateState(1, any(), any()) } returns null
+ every { jobService.updateState(1, any(), any(), any()) } returns null
Given {
- body(Job.Update(JobState.PENDING))
+ body(Job.Update(JobState.PENDING, 0))
contentType(ContentType.JSON)
} When {
post("/1")
@@ -170,10 +170,10 @@ class JobResourceTest {
@Test
@TestSecurity(user = "testUser", roles = ["runner"])
fun testUpdateState() {
- every { jobService.updateState(1, any(), any()) } returns dummyJob.copy(state = JobState.CLAIMED)
+ every { jobService.updateState(1, any(), any(), any()) } returns dummyJob.copy(state = JobState.CLAIMED)
Given {
- body(Job.Update(JobState.CLAIMED))
+ body(Job.Update(JobState.CLAIMED, 0))
contentType(ContentType.JSON)
} When {
post("/1")
diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/UserResourceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/UserResourceTest.kt
new file mode 100644
index 00000000..36af20f4
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/UserResourceTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.web.server.rest.user
+
+import io.quarkus.test.common.http.TestHTTPEndpoint
+import io.quarkus.test.junit.QuarkusTest
+import io.quarkus.test.security.TestSecurity
+import io.restassured.http.ContentType
+import io.restassured.module.kotlin.extensions.Then
+import io.restassured.module.kotlin.extensions.When
+import org.hamcrest.Matchers
+import org.junit.jupiter.api.Test
+
+/**
+ * Test suite for [UserResource].
+ */
+@QuarkusTest
+@TestHTTPEndpoint(UserResource::class)
+class UserResourceTest {
+ /**
+ * Test that tries to obtain the profile of the active user.
+ */
+ @Test
+ @TestSecurity(user = "testUser", roles = ["openid"])
+ fun testMe() {
+ When {
+ get("me")
+ } Then {
+ statusCode(200)
+ contentType(ContentType.JSON)
+
+ body("userId", Matchers.equalTo("testUser"))
+ body("accounting.simulationTime", Matchers.equalTo(0))
+ body("accounting.simulationTimeBudget", Matchers.greaterThan(0))
+ }
+ }
+
+ /**
+ * Test that tries to obtain the profile of the active user without authorization.
+ */
+ @Test
+ fun testMeUnauthorized() {
+ When {
+ get("me")
+ } Then {
+ statusCode(401)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/service/UserAccountingServiceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/service/UserAccountingServiceTest.kt
new file mode 100644
index 00000000..fdf04787
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/service/UserAccountingServiceTest.kt
@@ -0,0 +1,203 @@
+/*
+ * 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.web.server.service
+
+import io.mockk.every
+import io.mockk.mockk
+import io.quarkus.test.junit.QuarkusTest
+import org.junit.jupiter.api.Assertions.assertAll
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.opendc.web.server.model.UserAccounting
+import org.opendc.web.server.repository.UserAccountingRepository
+import java.time.Duration
+import java.time.LocalDate
+import javax.persistence.EntityExistsException
+
+/**
+ * Test suite for the [UserAccountingService].
+ */
+@QuarkusTest
+class UserAccountingServiceTest {
+ /**
+ * The [UserAccountingRepository] that is mocked.
+ */
+ private val repository: UserAccountingRepository = mockk()
+
+ /**
+ * The [UserAccountingService] instance under test.
+ */
+ private val service: UserAccountingService = UserAccountingService(repository, Duration.ofHours(1))
+
+ @Test
+ fun testGetUserDoesNotExist() {
+ val userId = "test"
+
+ every { repository.findForUser(userId) } returns null
+
+ val accounting = service.getAccounting(userId)
+
+ assertTrue(accounting.periodEnd.isAfter(LocalDate.now()))
+ assertEquals(0, accounting.simulationTime)
+ }
+
+ @Test
+ fun testGetUserDoesExist() {
+ val userId = "test"
+
+ val now = LocalDate.now()
+ val periodEnd = now.plusMonths(1)
+
+ every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600).also { it.simulationTime = 32 }
+
+ val accounting = service.getAccounting(userId)
+
+ assertAll(
+ { assertEquals(periodEnd, accounting.periodEnd) },
+ { assertEquals(32, accounting.simulationTime) },
+ { assertEquals(3600, accounting.simulationTimeBudget) }
+ )
+ }
+
+ @Test
+ fun testHasBudgetUserDoesNotExist() {
+ val userId = "test"
+
+ every { repository.findForUser(userId) } returns null
+
+ assertTrue(service.hasSimulationBudget(userId))
+ }
+
+ @Test
+ fun testHasBudget() {
+ val userId = "test"
+ val periodEnd = LocalDate.now().plusMonths(2)
+
+ every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600)
+
+ assertTrue(service.hasSimulationBudget(userId))
+ }
+
+ @Test
+ fun testHasBudgetExceededButPeriodExpired() {
+ val userId = "test"
+ val periodEnd = LocalDate.now().minusMonths(2)
+
+ every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600).also { it.simulationTime = 3900 }
+
+ assertTrue(service.hasSimulationBudget(userId))
+ }
+
+ @Test
+ fun testHasBudgetPeriodExpired() {
+ val userId = "test"
+ val periodEnd = LocalDate.now().minusMonths(2)
+
+ every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600)
+
+ assertTrue(service.hasSimulationBudget(userId))
+ }
+
+ @Test
+ fun testHasBudgetExceeded() {
+ val userId = "test"
+ val periodEnd = LocalDate.now().plusMonths(1)
+
+ every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600).also { it.simulationTime = 3900 }
+
+ assertFalse(service.hasSimulationBudget(userId))
+ }
+
+ @Test
+ fun testConsumeBudgetNewUser() {
+ val userId = "test"
+
+ every { repository.findForUser(userId) } returns null
+ every { repository.save(any()) } returns Unit
+
+ assertFalse(service.consumeSimulationBudget(userId, 10))
+ }
+
+ @Test
+ fun testConsumeBudgetNewUserExceeded() {
+ val userId = "test"
+
+ every { repository.findForUser(userId) } returns null
+ every { repository.save(any()) } returns Unit
+
+ assertTrue(service.consumeSimulationBudget(userId, 4000))
+ }
+
+ @Test
+ fun testConsumeBudgetNewUserConflict() {
+ val userId = "test"
+
+ val periodEnd = LocalDate.now().plusMonths(1)
+
+ every { repository.findForUser(userId) } returns null andThen UserAccounting(userId, periodEnd, 3600)
+ every { repository.save(any()) } throws EntityExistsException()
+ every { repository.consumeBudget(any(), any()) } answers {
+ val accounting = it.invocation.args[0] as UserAccounting
+ accounting.simulationTime -= it.invocation.args[1] as Int
+ true
+ }
+
+ assertFalse(service.consumeSimulationBudget(userId, 10))
+ }
+
+ @Test
+ fun testConsumeBudgetResetSuccess() {
+ val userId = "test"
+
+ val periodEnd = LocalDate.now().minusMonths(2)
+
+ every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600).also { it.simulationTime = 3900 }
+ every { repository.resetBudget(any(), any(), any()) } answers {
+ val accounting = it.invocation.args[0] as UserAccounting
+ accounting.periodEnd = it.invocation.args[1] as LocalDate
+ accounting.simulationTime = it.invocation.args[2] as Int
+ true
+ }
+
+ assertTrue(service.consumeSimulationBudget(userId, 4000))
+ }
+
+ @Test
+ fun testInfiniteConflict() {
+ val userId = "test"
+
+ val periodEnd = LocalDate.now().plusMonths(1)
+
+ every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600)
+ every { repository.consumeBudget(any(), any()) } answers {
+ val accounting = it.invocation.args[0] as UserAccounting
+ accounting.simulationTime -= it.invocation.args[1] as Int
+ false
+ }
+
+ assertThrows<IllegalStateException> { service.consumeSimulationBudget(userId, 10) }
+ }
+}
diff --git a/opendc-web/opendc-web-ui/src/api/users.js b/opendc-web/opendc-web-ui/src/api/users.js
new file mode 100644
index 00000000..12a9be05
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/api/users.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { request } from './index'
+
+/**
+ * Fetch information about the user from the web server.
+ *
+ * @param auth The authentication object.
+ */
+export function fetchUser(auth) {
+ return request(auth, `users/me`)
+}
diff --git a/opendc-web/opendc-web-ui/src/components/AppHeaderUser.js b/opendc-web/opendc-web-ui/src/components/AppHeaderUser.js
index e271accb..3a73d9ba 100644
--- a/opendc-web/opendc-web-ui/src/components/AppHeaderUser.js
+++ b/opendc-web/opendc-web-ui/src/components/AppHeaderUser.js
@@ -28,26 +28,44 @@ import {
DropdownItem,
DropdownGroup,
Avatar,
+ Progress,
+ ProgressSize,
+ DropdownSeparator,
} from '@patternfly/react-core'
import { useReducer } from 'react'
import { useAuth } from '../auth'
+import useUser from '../data/user'
export default function AppHeaderUser() {
const { logout, user, isAuthenticated, isLoading } = useAuth()
const username = isAuthenticated || isLoading ? user?.name : 'Anonymous'
const avatar = isAuthenticated || isLoading ? user?.picture : '/img/avatar.svg'
+ const { data } = useUser()
+ const simulationBudget = data?.accounting?.simulationTimeBudget ?? 3600
+ const simulationTime = data?.accounting?.simulationTime | 0
+
const [isDropdownOpen, toggleDropdown] = useReducer((t) => !t, false)
const userDropdownItems = [
- <DropdownGroup key="group 2">
- <DropdownItem
- key="group 2 logout"
- isDisabled={!isAuthenticated}
- onClick={() => logout({ returnTo: window.location.origin })}
- >
- Logout
+ <DropdownGroup key="budget" label="Monthly Simulation Budget">
+ <DropdownItem isDisabled>
+ <Progress
+ min={0}
+ max={simulationBudget}
+ value={simulationTime}
+ title={`${Math.ceil(simulationTime / 60)} of ${Math.ceil(simulationBudget / 60)} minutes`}
+ size={ProgressSize.sm}
+ />
</DropdownItem>
</DropdownGroup>,
+ <DropdownSeparator key="separator" />,
+ <DropdownItem
+ key="group 2 logout"
+ isDisabled={!isAuthenticated}
+ onClick={() => logout({ returnTo: window.location.origin })}
+ >
+ Logout
+ </DropdownItem>,
]
const avatarComponent = avatar ? (
diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js
index 8dc52f7a..64218a0a 100644
--- a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js
+++ b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js
@@ -64,7 +64,6 @@ function ScenarioTable({ portfolio, status }) {
) : (
'Unknown Topology'
)}
- ,
</Td>
<Td dataLabel="Workload">{`${scenario.workload.trace.name} (${
scenario.workload.samplingFraction * 100
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddContainer.js
index 6b136120..a0054ef6 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddContainer.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitAddContainer.js
@@ -25,6 +25,7 @@ import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import UnitAddComponent from './UnitAddComponent'
import { addUnit } from '../../../../redux/actions/topology/machine'
+import UnitType from './UnitType'
function UnitAddContainer({ machineId, unitType }) {
const units = useSelector((state) => Object.values(state.topology[unitType]))
@@ -37,7 +38,7 @@ function UnitAddContainer({ machineId, unitType }) {
UnitAddContainer.propTypes = {
machineId: PropTypes.string.isRequired,
- unitType: PropTypes.string.isRequired,
+ unitType: UnitType.isRequired,
}
export default UnitAddContainer
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListComponent.js
index daa3e7a7..75ab0ad7 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListComponent.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListComponent.js
@@ -20,9 +20,10 @@ import {
} from '@patternfly/react-core'
import { CubesIcon, InfoIcon, TrashIcon } from '@patternfly/react-icons'
import { ProcessingUnit, StorageUnit } from '../../../../shapes'
+import UnitType from './UnitType'
function UnitInfo({ unit, unitType }) {
- if (unitType === 'cpu' || unitType === 'gpu') {
+ if (unitType === 'cpus' || unitType === 'gpus') {
return (
<DescriptionList>
<DescriptionListGroup>
@@ -60,7 +61,7 @@ function UnitInfo({ unit, unitType }) {
}
UnitInfo.propTypes = {
- unitType: PropTypes.string.isRequired,
+ unitType: UnitType.isRequired,
unit: PropTypes.oneOfType([ProcessingUnit, StorageUnit]).isRequired,
}
@@ -104,7 +105,7 @@ function UnitListComponent({ unitType, units, onDelete }) {
}
UnitListComponent.propTypes = {
- unitType: PropTypes.string.isRequired,
+ unitType: UnitType.isRequired,
units: PropTypes.arrayOf(PropTypes.oneOfType([ProcessingUnit, StorageUnit])).isRequired,
onDelete: PropTypes.func,
}
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js
index 25e750c4..bcd4bdcc 100644
--- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitListContainer.js
@@ -25,6 +25,7 @@ import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import UnitListComponent from './UnitListComponent'
import { deleteUnit } from '../../../../redux/actions/topology/machine'
+import UnitType from './UnitType'
function UnitListContainer({ machineId, unitType }) {
const dispatch = useDispatch()
@@ -40,7 +41,7 @@ function UnitListContainer({ machineId, unitType }) {
UnitListContainer.propTypes = {
machineId: PropTypes.string.isRequired,
- unitType: PropTypes.string.isRequired,
+ unitType: UnitType.isRequired,
}
export default UnitListContainer
diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitType.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitType.js
new file mode 100644
index 00000000..b6d7bf8b
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/machine/UnitType.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+import PropTypes from 'prop-types'
+
+export default PropTypes.oneOf(['cpus', 'gpus', 'memories', 'storages'])
diff --git a/opendc-web/opendc-web-ui/src/data/query.js b/opendc-web/opendc-web-ui/src/data/query.js
index 59eaa684..3e5423b9 100644
--- a/opendc-web/opendc-web-ui/src/data/query.js
+++ b/opendc-web/opendc-web-ui/src/data/query.js
@@ -26,6 +26,7 @@ import { useAuth } from '../auth'
import { configureExperimentClient } from './experiments'
import { configureProjectClient } from './project'
import { configureTopologyClient } from './topology'
+import { configureUserClient } from './user'
let queryClient
@@ -34,6 +35,7 @@ function createQueryClient(auth) {
configureProjectClient(client, auth)
configureExperimentClient(client, auth)
configureTopologyClient(client, auth)
+ configureUserClient(client, auth)
return client
}
diff --git a/opendc-web/opendc-web-ui/src/data/user.js b/opendc-web/opendc-web-ui/src/data/user.js
new file mode 100644
index 00000000..97c0e1e2
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/data/user.js
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+import { useQuery } from 'react-query'
+import { fetchUser } from '../api/users'
+
+/**
+ * Configure the query defaults for the user client.
+ */
+export function configureUserClient(queryClient, auth) {
+ queryClient.setQueryDefaults('user', {
+ queryFn: () => fetchUser(auth),
+ })
+}
+
+/**
+ * Fetch the user data on the server.
+ */
+export default function useUser(options = {}) {
+ return useQuery('user', options)
+}
diff --git a/opendc-web/opendc-web-ui/src/util/available-metrics.js b/opendc-web/opendc-web-ui/src/util/available-metrics.js
index b21ab150..fda6cd4d 100644
--- a/opendc-web/opendc-web-ui/src/util/available-metrics.js
+++ b/opendc-web/opendc-web-ui/src/util/available-metrics.js
@@ -42,10 +42,10 @@ export const METRIC_NAMES_SHORT = {
mean_num_deployed_images: 'Mean Num. Deployed Images Per Host',
max_num_deployed_images: 'Max. Num. Deployed Images Per Host',
total_failure_vm_slices: 'Total Num. Failed VM Slices',
- total_vms_submitted: 'Total Num. VMs Submitted',
- total_vms_queued: 'Max. Num. VMs Queued',
- total_vms_finished: 'Max. Num. VMs Finished',
- total_vms_failed: 'Max. Num. VMs Failed',
+ total_vms_submitted: 'VMs Submitted',
+ total_vms_queued: 'VMs Queued',
+ total_vms_finished: 'VMs Finished',
+ total_vms_failed: 'VMs Failed',
}
export const METRIC_NAMES = {
@@ -58,11 +58,11 @@ export const METRIC_NAMES = {
mean_cpu_demand: 'Mean Host CPU Demand',
mean_num_deployed_images: 'Mean Number of Deployed Images Per Host',
max_num_deployed_images: 'Maximum Number Deployed Images Per Host',
- total_failure_vm_slices: 'Total Number Failed VM Slices',
- total_vms_submitted: 'Total Number VMs Submitted',
- total_vms_queued: 'Maximum Number VMs Queued',
- total_vms_finished: 'Maximum Number VMs Finished',
- total_vms_failed: 'Maximum Number VMs Failed',
+ total_failure_vm_slices: 'Failed VM Slices',
+ total_vms_submitted: 'VMs Submitted',
+ total_vms_queued: 'VMs Queued',
+ total_vms_finished: 'VMs Finished',
+ total_vms_failed: 'VMs Failed',
}
export const METRIC_UNITS = {
@@ -94,9 +94,8 @@ export const METRIC_DESCRIPTIONS = {
mean_num_deployed_images: 'The average number of virtual machines deployed on a host.',
max_num_deployed_images: 'The maximum number of virtual machines deployed at any time.',
total_failure_vm_slices: 'The total amount of CPU clock cycles lost due to failure.',
- total_vms_submitted: 'The total number of virtual machines scheduled by the compute service.',
- total_vms_queued:
- 'The maximum number of virtual machines waiting to be scheduled by the compute service at any point.',
- total_vms_finished: 'The total number of virtual machines that completed successfully.',
- total_vms_failed: 'The total number of virtual machines that failed during execution.',
+ total_vms_submitted: 'The number of virtual machines scheduled by the compute service.',
+ total_vms_queued: 'The number of virtual machines still waiting to be scheduled by the compute service.',
+ total_vms_finished: 'The number of virtual machines that completed.',
+ total_vms_failed: 'The number of virtual machines that could not be scheduled.',
}