diff --git a/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiClientTaskTemplateInfo.kt b/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiClientTaskTemplateInfo.kt index b03b3680..9b4c3bcf 100644 --- a/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiClientTaskTemplateInfo.kt +++ b/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiClientTaskTemplateInfo.kt @@ -12,7 +12,7 @@ data class ApiClientTaskTemplateInfo( val name: String, val taskGroup: String, val taskType: String, - val duration: Long + val duration: Long? ) { constructor(task: ApiTaskTemplate) : this( task.name, diff --git a/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiTaskOverview.kt b/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiTaskOverview.kt index 0099f8ce..ec053118 100644 --- a/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiTaskOverview.kt +++ b/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiTaskOverview.kt @@ -9,7 +9,7 @@ data class ApiTaskOverview( val name: String, val type: String, val group: String, - val duration: Long, + val duration: Long?, val taskId: String, val status: ApiTaskStatus, val started: Long?, @@ -24,4 +24,4 @@ data class ApiTaskOverview( task.status, task.started, task.ended) -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiTaskTemplateInfo.kt b/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiTaskTemplateInfo.kt index 11aa5f2a..1fc49721 100644 --- a/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiTaskTemplateInfo.kt +++ b/backend/src/main/kotlin/dev/dres/api/rest/types/evaluation/ApiTaskTemplateInfo.kt @@ -19,7 +19,7 @@ data class ApiTaskTemplateInfo( val comment: String?, val taskGroup: String, val taskType: String, - val duration: Long + val duration: Long? ) { constructor(task: ApiTaskTemplate) : this( task.id!!, diff --git a/backend/src/main/kotlin/dev/dres/api/rest/types/template/tasks/ApiTaskTemplate.kt b/backend/src/main/kotlin/dev/dres/api/rest/types/template/tasks/ApiTaskTemplate.kt index bcf61c0d..3c519f46 100644 --- a/backend/src/main/kotlin/dev/dres/api/rest/types/template/tasks/ApiTaskTemplate.kt +++ b/backend/src/main/kotlin/dev/dres/api/rest/types/template/tasks/ApiTaskTemplate.kt @@ -23,7 +23,7 @@ data class ApiTaskTemplate( val name: String, val taskGroup: String, val taskType: String, - val duration: Long, + val duration: Long?, val collectionId: CollectionId, val targets: List, val hints: List, diff --git a/backend/src/main/kotlin/dev/dres/api/rest/types/template/tasks/ApiTaskType.kt b/backend/src/main/kotlin/dev/dres/api/rest/types/template/tasks/ApiTaskType.kt index b0e075a7..74e09ae4 100644 --- a/backend/src/main/kotlin/dev/dres/api/rest/types/template/tasks/ApiTaskType.kt +++ b/backend/src/main/kotlin/dev/dres/api/rest/types/template/tasks/ApiTaskType.kt @@ -22,7 +22,7 @@ import java.nio.file.StandardOpenOption @Serializable data class ApiTaskType( val name: String, - val duration: Long, + val duration: Long?, val targetOption: ApiTargetOption, val hintOptions: List, val submissionOptions: List, diff --git a/backend/src/main/kotlin/dev/dres/data/model/run/AbstractInteractiveTask.kt b/backend/src/main/kotlin/dev/dres/data/model/run/AbstractInteractiveTask.kt index af1bfcf8..03426938 100644 --- a/backend/src/main/kotlin/dev/dres/data/model/run/AbstractInteractiveTask.kt +++ b/backend/src/main/kotlin/dev/dres/data/model/run/AbstractInteractiveTask.kt @@ -20,8 +20,8 @@ import kotlinx.dnq.query.* abstract class AbstractInteractiveTask(store: TransientEntityStore, task: DbTask) : AbstractTask(store, task) { - /** The total duration in milliseconds of this task. Usually determined by the [DbTaskTemplate] but can be adjusted! */ - abstract override var duration: Long + /** The total duration in seconds of this task. Usually determined by the [DbTaskTemplate] but can be adjusted! */ + abstract override var duration: Long? /** The [AnswerSetValidator] used to validate [DbSubmission]s. */ final override val validator: AnswerSetValidator diff --git a/backend/src/main/kotlin/dev/dres/data/model/run/InteractiveAsynchronousEvaluation.kt b/backend/src/main/kotlin/dev/dres/data/model/run/InteractiveAsynchronousEvaluation.kt index 0ea1eba6..a8cc798a 100644 --- a/backend/src/main/kotlin/dev/dres/data/model/run/InteractiveAsynchronousEvaluation.kt +++ b/backend/src/main/kotlin/dev/dres/data/model/run/InteractiveAsynchronousEvaluation.kt @@ -16,10 +16,7 @@ import dev.dres.run.filter.basics.SubmissionFilter import dev.dres.run.filter.basics.CombiningSubmissionFilter import dev.dres.run.score.scoreboard.MaxNormalizingScoreBoard import dev.dres.run.score.scoreboard.Scoreboard -import dev.dres.run.score.scorer.AvsTaskScorer -import dev.dres.run.score.scorer.CachingTaskScorer -import dev.dres.run.score.scorer.KisTaskScorer -import dev.dres.run.score.scorer.LegacyAvsTaskScorer +import dev.dres.run.score.scorer.* import dev.dres.run.transformer.MapToSegmentTransformer import dev.dres.run.transformer.SubmissionTaskMatchTransformer import dev.dres.run.transformer.basics.SubmissionTransformer @@ -161,8 +158,8 @@ class InteractiveAsynchronousEvaluation(store: TransientEntityStore, evaluation: /** The [CachingTaskScorer] instance used by this [InteractiveAsynchronousEvaluation].*/ override val scorer: CachingTaskScorer - /** The total duration in milliseconds of this task. Usually determined by the [DbTaskTemplate] but can be adjusted! */ - override var duration: Long = this.template.duration + /** The total duration in seconds of this task. Usually determined by the [DbTaskTemplate] but can be adjusted! NULL represents perpetual task*/ + override var duration: Long? = this.template.duration val teamId = this.dbTask.team!!.id @@ -220,6 +217,7 @@ class InteractiveAsynchronousEvaluation(store: TransientEntityStore, evaluation: DbScoreOption.AVS -> AvsTaskScorer(this, store) DbScoreOption.LEGACY_AVS -> LegacyAvsTaskScorer(this, store) + DbScoreOption.NOOP -> NoOpTaskScorer(this) else -> throw IllegalStateException("The task score option $scoreOption is currently not supported.") } ) diff --git a/backend/src/main/kotlin/dev/dres/data/model/run/InteractiveSynchronousEvaluation.kt b/backend/src/main/kotlin/dev/dres/data/model/run/InteractiveSynchronousEvaluation.kt index 331670b5..63747df6 100644 --- a/backend/src/main/kotlin/dev/dres/data/model/run/InteractiveSynchronousEvaluation.kt +++ b/backend/src/main/kotlin/dev/dres/data/model/run/InteractiveSynchronousEvaluation.kt @@ -16,10 +16,7 @@ import dev.dres.run.filter.basics.SubmissionFilter import dev.dres.run.filter.basics.CombiningSubmissionFilter import dev.dres.run.score.scoreboard.MaxNormalizingScoreBoard import dev.dres.run.score.scoreboard.Scoreboard -import dev.dres.run.score.scorer.AvsTaskScorer -import dev.dres.run.score.scorer.CachingTaskScorer -import dev.dres.run.score.scorer.KisTaskScorer -import dev.dres.run.score.scorer.LegacyAvsTaskScorer +import dev.dres.run.score.scorer.* import dev.dres.run.transformer.MapToSegmentTransformer import dev.dres.run.transformer.SubmissionTaskMatchTransformer import dev.dres.run.transformer.basics.SubmissionTransformer @@ -118,8 +115,8 @@ class InteractiveSynchronousEvaluation(store: TransientEntityStore, evaluation: /** The [CachingTaskScorer] instance used by this [ISTaskRun]. */ override val scorer: CachingTaskScorer - /** The total duration in milliseconds of this task. Usually determined by the [DbTaskTemplate] but can be adjusted! */ - override var duration: Long = this.template.duration + /** The total duration in seconds of this task. Usually determined by the [DbTaskTemplate] but can be adjusted! NULL represents perpetual task */ + override var duration: Long? = this.template.duration /** */ override val teams: List = @@ -176,6 +173,7 @@ class InteractiveSynchronousEvaluation(store: TransientEntityStore, evaluation: DbScoreOption.AVS -> AvsTaskScorer(this, store) DbScoreOption.LEGACY_AVS -> LegacyAvsTaskScorer(this, store) + DbScoreOption.NOOP -> NoOpTaskScorer(this) else -> throw IllegalStateException("The task score option $scoreOption is currently not supported.") } ) diff --git a/backend/src/main/kotlin/dev/dres/data/model/template/task/DbTaskTemplate.kt b/backend/src/main/kotlin/dev/dres/data/model/template/task/DbTaskTemplate.kt index 0e1a0bac..63782cbb 100644 --- a/backend/src/main/kotlin/dev/dres/data/model/template/task/DbTaskTemplate.kt +++ b/backend/src/main/kotlin/dev/dres/data/model/template/task/DbTaskTemplate.kt @@ -46,7 +46,7 @@ class DbTaskTemplate(entity: Entity) : PersistentEntity(entity), TaskTemplate { var collection by xdLink1(DbMediaCollection) /** The duration of the [DbTaskTemplate] in seconds. */ - var duration by xdRequiredLongProp { min(0L) } + var duration by xdNullableLongProp() /** The [DbTaskTemplateTarget]s that identify the target. Multiple entries indicate the existence of multiple targets. */ val targets by xdChildren1_N(DbTaskTemplateTarget::task) diff --git a/backend/src/main/kotlin/dev/dres/data/model/template/task/DbTaskType.kt b/backend/src/main/kotlin/dev/dres/data/model/template/task/DbTaskType.kt index 0090c404..3df748c2 100644 --- a/backend/src/main/kotlin/dev/dres/data/model/template/task/DbTaskType.kt +++ b/backend/src/main/kotlin/dev/dres/data/model/template/task/DbTaskType.kt @@ -29,8 +29,8 @@ class DbTaskType(entity: Entity) : XdEntity(entity) { /** The [DbEvaluationTemplate] this [DbTaskType] belongs to. */ var evaluation: DbEvaluationTemplate by xdParent(DbEvaluationTemplate::taskTypes) - /** The (default) duration of this [DbTaskType] in seconds. */ - var duration by xdRequiredLongProp() { min(0L) } + /** The (default) duration of this [DbTaskType] in seconds. Defaults to no duration, which means perpetually running task. */ + var duration by xdNullableLongProp() /** The [DbTargetOption] for this [DbTaskType]. Specifies the type of target. */ var target by xdLink1(DbTargetOption) diff --git a/backend/src/main/kotlin/dev/dres/data/model/template/task/options/DbScoreOption.kt b/backend/src/main/kotlin/dev/dres/data/model/template/task/options/DbScoreOption.kt index dfb9b09b..f21165de 100644 --- a/backend/src/main/kotlin/dev/dres/data/model/template/task/options/DbScoreOption.kt +++ b/backend/src/main/kotlin/dev/dres/data/model/template/task/options/DbScoreOption.kt @@ -17,6 +17,7 @@ class DbScoreOption(entity: Entity) : XdEnumEntity(entity) { val KIS by enumField { description = "KIS" } val AVS by enumField { description = "AVS" } val LEGACY_AVS by enumField {description = "LEGACY_AVS"} + val NOOP by enumField { description = "NOOP" } } /** Name / description of the [DbScoreOption]. */ diff --git a/backend/src/main/kotlin/dev/dres/run/InteractiveAsynchronousRunManager.kt b/backend/src/main/kotlin/dev/dres/run/InteractiveAsynchronousRunManager.kt index 9534475d..a658417b 100644 --- a/backend/src/main/kotlin/dev/dres/run/InteractiveAsynchronousRunManager.kt +++ b/backend/src/main/kotlin/dev/dres/run/InteractiveAsynchronousRunManager.kt @@ -370,8 +370,8 @@ class InteractiveAsynchronousRunManager( /** * Returns the time in milliseconds that is left until the end of the currently running task for the given team. - * Only works if the [InteractiveAsynchronousRunManager] is in state [RunManagerStatus.ACTIVE]. If no task is running, - * this method returns -1L. + * Only works if the [InteractiveAsynchronousRunManager] is in state [RunManagerStatus.ACTIVE]. + * If no task is running or is running perpetually, this method returns -1L. * * @param context The [RunActionContext] used for the invocation. * @return Time remaining until the task will end or -1, if no task is running. @@ -380,10 +380,10 @@ class InteractiveAsynchronousRunManager( val currentTaskRun = this.currentTask(context) - return if (currentTaskRun?.isRunning == true) { + return if (currentTaskRun?.isRunning == true && currentTaskRun.duration != null) { // TODO what is the semantic of a perpetual IA task? max( 0L, - currentTaskRun.duration * 1000L - (System.currentTimeMillis() - currentTaskRun.started!!) + InteractiveRunManager.COUNTDOWN_DURATION + currentTaskRun.duration!! * 1000L - (System.currentTimeMillis() - currentTaskRun.started!!) + InteractiveRunManager.COUNTDOWN_DURATION ) } else { -1L @@ -488,10 +488,11 @@ class InteractiveAsynchronousRunManager( val currentTaskRun = this.currentTask(context) ?: throw IllegalStateException("No active TaskRun found. This is a serious error!") - val newDuration = currentTaskRun.duration + s + check(currentTaskRun.duration != null){"The task '${currentTaskRun.template.name}' (${currentTaskRun.taskId}) runs perpetually."} // TODO what is the semantic of a perpetual IA task? + val newDuration = currentTaskRun.duration!! + s check((newDuration * 1000L - (System.currentTimeMillis() - currentTaskRun.started!!)) > 0) { "New duration $s can not be applied because too much time has already elapsed." } currentTaskRun.duration = newDuration - return (currentTaskRun.duration * 1000L - (System.currentTimeMillis() - currentTaskRun.started!!)) + return (currentTaskRun.duration!! * 1000L - (System.currentTimeMillis() - currentTaskRun.started!!)) } @@ -722,20 +723,24 @@ class InteractiveAsynchronousRunManager( this.stateLock.write { val task = this.evaluation.currentTaskForTeam(teamId) ?: throw IllegalStateException("Could not find active task for team $teamId despite status of the team being ${this.statusMap[teamId]}. This is a programmer's error!") - val timeLeft = max( - 0L, - task.duration * 1000L - (System.currentTimeMillis() - task.started!!) + InteractiveRunManager.COUNTDOWN_DURATION - ) - if (timeLeft <= 0) { - task.end() - AuditLogger.taskEnd(this.id, task.taskId, AuditLogSource.INTERNAL, null) + if(task.duration != null){ + // TODO what is the semantic of a perpetual IA task? + val timeLeft = max( + 0L, + task.duration!! * 1000L - (System.currentTimeMillis() - task.started!!) + InteractiveRunManager.COUNTDOWN_DURATION + ) + if (timeLeft <= 0) { + task.end() + AuditLogger.taskEnd(this.id, task.taskId, AuditLogSource.INTERNAL, null) // /* Enqueue WS message for sending */ // RunExecutor.broadcastWsMessage( // teamId, // ServerMessage(this.id, ServerMessageType.TASK_END, task.taskId) // ) + } } + } } else if (teamHasPreparingTask(teamId)) { this.stateLock.write { diff --git a/backend/src/main/kotlin/dev/dres/run/InteractiveSynchronousRunManager.kt b/backend/src/main/kotlin/dev/dres/run/InteractiveSynchronousRunManager.kt index 67f11f1e..052bb5de 100644 --- a/backend/src/main/kotlin/dev/dres/run/InteractiveSynchronousRunManager.kt +++ b/backend/src/main/kotlin/dev/dres/run/InteractiveSynchronousRunManager.kt @@ -353,30 +353,31 @@ class InteractiveSynchronousRunManager( val task = this.currentTask(context) ?: throw IllegalStateException("SynchronizedRunManager is in status ${this.status} but has no active TaskRun. This is a serious error!") check(task.isRunning) { "Task run '${this.name}.${task.position}' is currently not running. This is a programmer's error!" } + check(task.duration != null){"Task run '${this.name}.${task.position}' runs perpetually. This is a programmer's error!"} /* Adjust duration. */ - val newDuration = task.duration + s + val newDuration = task.duration!! + s if ((newDuration * 1000L - (System.currentTimeMillis() - task.started!!)) < 0) { throw IllegalArgumentException("New duration $s can not be applied because too much time has already elapsed.") } task.duration = newDuration - return (task.duration * 1000L - (System.currentTimeMillis() - task.started!!)) + return (task.duration!! * 1000L - (System.currentTimeMillis() - task.started!!)) } /** * Returns the time in milliseconds that is left until the end of the current [DbTask]. - * Only works if the [RunManager] is in wrong [RunManagerStatus]. If no task is running, - * this method returns -1L. + * Only works if the [RunManager] is in right [RunManagerStatus]. If no task is running, + * OR a perpetual task is running, this method returns -1L. * * @return Time remaining until the task will end or -1, if no task is running. */ override fun timeLeft(context: RunActionContext): Long { - return if (this.evaluation.currentTaskRun?.status == ApiTaskStatus.RUNNING) { + return if (this.evaluation.currentTaskRun?.status == ApiTaskStatus.RUNNING && this.evaluation.currentTaskRun?.duration != null) { val currentTaskRun = this.currentTask(context) ?: throw IllegalStateException("SynchronizedRunManager is in status ${this.status} but has no active TaskRun. This is a serious error!") max( 0L, - currentTaskRun.duration * 1000L - (System.currentTimeMillis() - currentTaskRun.started!!) + InteractiveRunManager.COUNTDOWN_DURATION + currentTaskRun.duration!! * 1000L - (System.currentTimeMillis() - currentTaskRun.started!!) + InteractiveRunManager.COUNTDOWN_DURATION ) } else { -1L @@ -677,13 +678,13 @@ class InteractiveSynchronousRunManager( // RunExecutor.broadcastWsMessage(ServerMessage(this.id, ServerMessageType.TASK_START, this.evaluation.currentTask?.taskId)) } - /** Case 2: Facilitates internal transition from RunManagerStatus.RUNNING_TASK to RunManagerStatus.TASK_ENDED due to timeout. */ - if (this.evaluation.currentTaskRun?.status == ApiTaskStatus.RUNNING) { + /** Case 2: Facilitates internal transition from RunManagerStatus.RUNNING_TASK to RunManagerStatus.TASK_ENDED due to timeout, if possible */ + if (this.evaluation.currentTaskRun?.status == ApiTaskStatus.RUNNING && this.evaluation.currentTaskRun!!.duration != null) { this.stateLock.write { val task = this.evaluation.currentTaskRun!! val timeLeft = max( 0L, - task.duration * 1000L - (System.currentTimeMillis() - task.started!!) + InteractiveRunManager.COUNTDOWN_DURATION + task.duration!! * 1000L - (System.currentTimeMillis() - task.started!!) + InteractiveRunManager.COUNTDOWN_DURATION ) if (timeLeft <= 0) { task.end() diff --git a/backend/src/main/kotlin/dev/dres/run/score/Scoreable.kt b/backend/src/main/kotlin/dev/dres/run/score/Scoreable.kt index 2f3d7ff5..79bba504 100644 --- a/backend/src/main/kotlin/dev/dres/run/score/Scoreable.kt +++ b/backend/src/main/kotlin/dev/dres/run/score/Scoreable.kt @@ -17,8 +17,8 @@ interface Scoreable { /** The [TeamId]s of teams that work on the task identified by this [Scoreable]. */ val teams: List - /** Duration of when the [Task] in seconds. */ - val duration: Long + /** Duration of when the [Task] in seconds. Null should be used to indicate perpetual duration */ + val duration: Long? /** Timestamp of when the [Task] identified by this [Scoreable] was started. */ val started: Long? diff --git a/backend/src/main/kotlin/dev/dres/run/score/scorer/KisTaskScorer.kt b/backend/src/main/kotlin/dev/dres/run/score/scorer/KisTaskScorer.kt index ab5bd534..47697ee4 100644 --- a/backend/src/main/kotlin/dev/dres/run/score/scorer/KisTaskScorer.kt +++ b/backend/src/main/kotlin/dev/dres/run/score/scorer/KisTaskScorer.kt @@ -16,6 +16,10 @@ class KisTaskScorer( store: TransientEntityStore? ) : AbstractTaskScorer(scoreable, store) { + init { + require(scoreable.duration != null){"Cannot create a KisTaskScorer for perpetual task '${scoreable.taskId}'"} + } + constructor(run: TaskRun, parameters: Map, store: TransientEntityStore?) : this( run, parameters.getOrDefault("maxPointsPerTask", "$defaultmaxPointsPerTask").toDoubleOrNull() ?: defaultmaxPointsPerTask, @@ -39,7 +43,7 @@ class KisTaskScorer( * @return A [Map] of [TeamId] to calculated task score. */ override fun calculateScores(submissions: Sequence): Map { - val taskDuration = this.scoreable.duration.toDouble() * 1000.0 + val taskDuration = this.scoreable.duration!!.toDouble() * 1000.0 val taskStartTime = this.scoreable.started ?: throw IllegalArgumentException("No task start time specified.") return this.scoreable.teams.associateWith { teamId -> val verdicts = submissions.filter { it.teamId == teamId }.sortedBy { it.timestamp }.flatMap { sub -> @@ -60,4 +64,4 @@ class KisTaskScorer( score } } -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/dev/dres/run/score/scorer/NoOpTaskScorer.kt b/backend/src/main/kotlin/dev/dres/run/score/scorer/NoOpTaskScorer.kt new file mode 100644 index 00000000..ec712653 --- /dev/null +++ b/backend/src/main/kotlin/dev/dres/run/score/scorer/NoOpTaskScorer.kt @@ -0,0 +1,11 @@ +package dev.dres.run.score.scorer + +import dev.dres.data.model.template.team.TeamId +import dev.dres.run.score.Scoreable + +/** + * Non-operational task scorer, which does not calculate a score. + */ +class NoOpTaskScorer(override val scoreable: Scoreable) : TaskScorer { + override fun scoreMap(): Map = emptyMap() +}