Skip to content

Commit

Permalink
#467 Backend support for perpetual tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
sauterl committed Jul 15, 2024
1 parent 9480356 commit 966f9d3
Show file tree
Hide file tree
Showing 16 changed files with 67 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand All @@ -24,4 +24,4 @@ data class ApiTaskOverview(
task.status,
task.started,
task.ended)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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!!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApiTarget>,
val hints: List<ApiHint>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApiHintOption>,
val submissionOptions: List<ApiSubmissionOption>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.")
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<TeamId> =
Expand Down Expand Up @@ -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.")
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DbTaskTemplate, DbTaskTemplateTarget>(DbTaskTemplateTarget::task)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class DbTaskType(entity: Entity) : XdEntity(entity) {
/** The [DbEvaluationTemplate] this [DbTaskType] belongs to. */
var evaluation: DbEvaluationTemplate by xdParent<DbTaskType,DbEvaluationTemplate>(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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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!!))

}

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions backend/src/main/kotlin/dev/dres/run/score/Scoreable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ interface Scoreable {
/** The [TeamId]s of teams that work on the task identified by this [Scoreable]. */
val teams: List<TeamId>

/** 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?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>, store: TransientEntityStore?) : this(
run,
parameters.getOrDefault("maxPointsPerTask", "$defaultmaxPointsPerTask").toDoubleOrNull() ?: defaultmaxPointsPerTask,
Expand All @@ -39,7 +43,7 @@ class KisTaskScorer(
* @return A [Map] of [TeamId] to calculated task score.
*/
override fun calculateScores(submissions: Sequence<Submission>): Map<TeamId, Double> {
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 ->
Expand All @@ -60,4 +64,4 @@ class KisTaskScorer(
score
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<TeamId, Double> = emptyMap()
}

0 comments on commit 966f9d3

Please sign in to comment.