Skip to content

Commit

Permalink
ENH: Implement API for slow running tasks. Add end-points for request…
Browse files Browse the repository at this point in the history
…ing status on a task
  • Loading branch information
davidkleiven committed Dec 2, 2023
1 parent 099a45e commit 6837dcc
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 43 deletions.
21 changes: 8 additions & 13 deletions src/main/ApiDataModels.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
package com.github.statnett.loadflowservice

import com.fasterxml.jackson.databind.ObjectMapper
import com.powsybl.iidm.network.Network
import com.powsybl.security.SecurityAnalysisResult
import com.powsybl.security.json.SecurityAnalysisJsonModule
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
* Class for holding properties from the PowsbyBl bus class that are
Expand All @@ -30,7 +22,7 @@ fun busPropertiesFromNetwork(network: Network): List<BusProperties> {
.map { bus ->
BusProperties(
id = bus.id,
voltage = bus.v,
voltage = if (bus.v.isNaN()) bus.voltageLevel.nominalV else bus.v,
angle = bus.angle,
activePower = bus.p,
reactivePower = bus.q,
Expand All @@ -50,21 +42,24 @@ data class LineProperties(
@Serializable
data class TerminalProperties(val activePower: Double, val reactivePower: Double)

@Serializable
sealed class ComputationResult

@Serializable
data class LoadFlowResultForApi(
val isOk: Boolean,
val buses: List<BusProperties>,
val branches: List<LineProperties>,
val report: String
)
) : ComputationResult()

fun branchPropertiesFromNetwork(network: Network): List<LineProperties> {
return network.lines.map { line ->
LineProperties(
id = line.id,
isOverloaded = line.isOverloaded,
terminal1 = TerminalProperties(line.terminal1.p, line.terminal1.q),
terminal2 = TerminalProperties(line.terminal2.p, line.terminal2.q)
terminal1 = TerminalProperties(line.terminal1.p, if (line.terminal1.q.isNaN()) 0.0 else line.terminal1.q),
terminal2 = TerminalProperties(line.terminal2.p, if (line.terminal2.q.isNaN()) 0.0 else line.terminal2.q)
)
}.toList()
}
Expand All @@ -74,4 +69,4 @@ data class LoadFlowServiceSecurityAnalysisResult(
@Serializable(with = SecurityAnalysisResultSerializer::class)
val securityAnalysisResult: SecurityAnalysisResult,
val report: String
)
) : ComputationResult()
33 changes: 23 additions & 10 deletions src/main/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import io.ktor.server.routing.*
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module() {
val taskManager = TaskManager()
install(ContentNegotiation) {
json()
}
Expand Down Expand Up @@ -64,7 +65,7 @@ fun Application.module() {
val paramContainer = LoadParameterContainer()
val files = multiPartDataHandler(call.receiveMultipart(), paramContainer::formItemHandler)
val network = networkFromFirstFile(files)
val result = solve(network, paramContainer.parameters)
val result = createTask(taskManager) { solve(network, paramContainer.parameters) }
call.respond(result)
}

Expand Down Expand Up @@ -123,17 +124,29 @@ fun Application.module() {
val actions: List<Action> = listOf()
val monitors: List<StateMonitor> = listOf()

val result = runSecurityAnalysis(
network,
securityParamsCnt.parameters,
contingencyCnt,
intersceptors,
operatorStrategies,
actions,
monitors
)
val result = createTask(taskManager) {
runSecurityAnalysis(
network,
securityParamsCnt.parameters,
contingencyCnt,
intersceptors,
operatorStrategies,
actions,
monitors
)
}
call.respond(result)
}

get("/status/{id}") {
val id = call.parameters["id"] ?: ""
call.respond(taskManager.status(id))
}

get("/result/{id}") {
val id = call.parameters["id"] ?: ""
taskManager.respondWithResult(call, id)
}
swaggerUI(path = "openapi", swaggerFile = "openapi/documentation.yaml")
}
}
9 changes: 8 additions & 1 deletion src/main/ExceptionHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,16 @@ class ExceptionHandler {
)
}

is TaskDoesNotExistException -> {
call.respondText(
"$cause",
status = HttpStatusCode.NotFound
)
}

else -> {
call.respondText(
"500: $cause.\nStack trace: ${cause.stackTraceToString()}",
"500: $cause. Stack trace: ${cause.stackTraceToString()}",
status = HttpStatusCode.InternalServerError
)
}
Expand Down
4 changes: 0 additions & 4 deletions src/main/Solver.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.statnett.loadflowservice

import com.fasterxml.jackson.databind.ObjectMapper
import com.powsybl.commons.PowsyblException
import com.powsybl.commons.json.JsonUtil
import com.powsybl.commons.reporter.Reporter
Expand All @@ -15,12 +14,9 @@ import com.powsybl.loadflow.json.JsonLoadFlowParameters
import com.powsybl.security.LimitViolationFilter
import com.powsybl.security.SecurityAnalysis
import com.powsybl.security.SecurityAnalysisParameters
import com.powsybl.security.SecurityAnalysisReport
import com.powsybl.security.SecurityAnalysisResult
import com.powsybl.security.action.Action
import com.powsybl.security.detectors.DefaultLimitViolationDetector
import com.powsybl.security.interceptors.SecurityAnalysisInterceptor
import com.powsybl.security.json.SecurityAnalysisJsonModule
import com.powsybl.security.monitor.StateMonitor
import com.powsybl.security.strategy.OperatorStrategy
import com.powsybl.sensitivity.*
Expand Down
50 changes: 50 additions & 0 deletions src/main/TaskCreators.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.github.statnett.loadflowservice

import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable

fun statusUrl(id: String): String {
return "/status/$id"
}

fun resultUrl(id: String): String {
return "/result/$id"
}

@Serializable
data class TaskInfo(
val statusUrl: String,
val resultUrl: String,
val id: String
)

private val logger = KotlinLogging.logger { }

fun createTask(tm: TaskManager, calculate: () -> ComputationResult): TaskInfo {
val task = Task()
tm.register(task)
CoroutineScope(Dispatchers.Default).launch {
logger.info { "Running task ${task.id} on thread ${Thread.currentThread().name}" }
try {
task.status = TaskStatus.RUNNING
task.result = calculate()
task.status = TaskStatus.FINISHED
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
task.status = TaskStatus.FAILED
task.exception = e
} finally {
tm.scheduleTaskDeletion(task.id)
}
}
return TaskInfo(
statusUrl = statusUrl(task.id),
resultUrl = resultUrl(task.id),
id = task.id
)
}
124 changes: 124 additions & 0 deletions src/main/TaskManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.github.statnett.loadflowservice

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.*

enum class TaskStatus {
CREATED, RUNNING, FINISHED, FAILED
}

class Task {
var status = TaskStatus.CREATED
var result: ComputationResult? = null
val id = UUID.randomUUID().toString()
var exception: Exception? = null
private val createdAt = System.currentTimeMillis()

fun ageSeconds(): Int {
return ((System.currentTimeMillis() - createdAt) / 1000).toInt()
}
}

class TaskQueue {
private val tasks = mutableListOf<Task>()

fun get(id: String): Task? {
return tasks.firstOrNull { task -> task.id == id }
}

fun size(): Int {
return tasks.size
}

fun register(task: Task) {
tasks.add(task)
}

fun remove(id: String) {
tasks.removeIf { t -> t.id == id }
}

fun finished(id: String): Boolean {
val task = get(id) ?: return false
return task.status == TaskStatus.FINISHED
}

fun clearOlderThan(ageSeconds: Int) {
tasks.removeIf { t -> (t.ageSeconds() > ageSeconds) and (t.status != TaskStatus.RUNNING)}
}

fun numRunning(): Int {
return tasks.count { t -> t.status == TaskStatus.RUNNING }
}
}

class TaskDoesNotExistException(message: String) : Exception(message)
class FullBufferException(message: String) : Exception(message)

@Serializable
data class TaskStatusResponse(val status: String, val message: String)

class TaskManager(private val maxRunningTasks: Int = 100, private val retention: Int = 10 * 60) {
internal val queue = TaskQueue()

fun status(id: String): TaskStatusResponse {
val task = queue.get(id) ?: throw TaskDoesNotExistException("No task with id $id")
val message = if (task.exception != null) "${task.exception}" else ""
return TaskStatusResponse(
task.status.name,
message
)
}

fun size(): Int {
return queue.size()
}

fun numRunning(): Int {
return queue.numRunning()
}

private fun clearOld() {
queue.clearOlderThan(retention)
}

private fun raiseOnFull() {
if (numRunning() >= maxRunningTasks) {
throw FullBufferException("Could not add task because the buffer is full")
}
}

fun register(task: Task) {
raiseOnFull()
queue.register(task)
}


suspend fun respondWithResult(call: ApplicationCall, id: String) {
if (status(id).status != TaskStatus.FINISHED.name) {
call.respondText("Task not finished", status = HttpStatusCode.NotFound)
return
}

val result = queue.get(id)
if (result != null) {
val res = Json.encodeToString(result.result)
call.respondText(res, contentType = ContentType.Application.Json)
queue.remove(id)
}

throw TaskDoesNotExistException("No task with id $id")
}

suspend fun scheduleTaskDeletion(id: String) {
delay((retention*1000).toLong())
queue.remove(id)
}

}
2 changes: 1 addition & 1 deletion src/test/ApiDataModelsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import com.github.statnett.loadflowservice.LoadFlowServiceSecurityAnalysisResult
import com.github.statnett.loadflowservice.busPropertiesFromNetwork
import com.powsybl.ieeecdf.converter.IeeeCdfNetworkFactory
import com.powsybl.security.SecurityAnalysisResult
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals

Expand Down
Loading

0 comments on commit 6837dcc

Please sign in to comment.