diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43f21f91..d782a418 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,11 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Input
- Output
- Generation
-- Let `OsmoGridGuardian` initialize services
-- Spawn `LvCoordinator` and trigger it
-- Spawn worker pools of `LvRegionCoordinator`s and `LvGridGenerator`s
-- Forward results to `ResultEventListener`
+- Let `OsmoGridGuardian` handle multiple runs and spawn children accordingly
+- A `RunGuardian` takes care of a distinct simulation run and spawns all its needed services
+ - Spawn an `InputDataProvider` and a `ResultListener`(if required) per run
+ - Spawn `LvCoordinator` and trigger it
- Coordinated shut down phase
+ - Only terminate OSMoGrid internal result event listener and let additional listeners alive
+ - Post stop phase for terminated children (to shut down data connections, ...)
+ - Await response from terminated children
### Changed
- Rely on Java 17
@@ -24,6 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
- Legacy Java code
- - Jacoco gradle pluginfgc
+ - Jacoco gradle plugin
[Unreleased]: https://github.com/ie3-institute/OSMoGrid/compare/7e598e53e333c9c1a7b19906584f0357ddf07990...HEAD
diff --git a/build.gradle b/build.gradle
index 4f32ffd0..d569bb38 100644
--- a/build.gradle
+++ b/build.gradle
@@ -58,6 +58,8 @@ dependencies {
// akka
implementation "com.typesafe.akka:akka-actor-typed_${scalaMajorVersion}:${akkaVersion}"
+ implementation platform("com.typesafe.akka:akka-bom_$scalaMajorVersion:2.6.18")
+ testImplementation "com.typesafe.akka:akka-actor-testkit-typed_$scalaMajorVersion:2.6.18"
// logging
implementation "com.typesafe.scala-logging:scala-logging_${scalaMajorVersion}:3.9.4"
@@ -109,6 +111,8 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'cglib:cglib-nodep:3.3.0' // enables mocking of classes (in addition to interfaces)
testImplementation 'org.mockito:mockito-core:4.3.1' // mocking framework
+ testImplementation 'org.scalatestplus:mockito-3-12_3:3.2.10.0' // syntactic sugar
+
testImplementation 'org.spockframework:spock-core:2.0-groovy-3.0'
testImplementation 'org.objenesis:objenesis:3.2' // Mock creation with constructor parameters
diff --git a/docs/puml/protocol/protocol.puml b/docs/puml/protocol/protocol.puml
index 63898ac0..c321ca8c 100644
--- a/docs/puml/protocol/protocol.puml
+++ b/docs/puml/protocol/protocol.puml
@@ -8,6 +8,7 @@ boundary Main
database Input
participant InputDataProvider
participant OsmoGridGuardian
+participant RunGuardian
participant LvCoordinator
participant LvRegionCoordinator
participant MunicipalityCoordinator
@@ -17,15 +18,17 @@ participant LvGridGenerator
participant ResultListener
== Init ==
-Main -> OsmoGridGuardian: Run(cfg)
-OsmoGridGuardian --> InputDataProvider: //Spawn//
+Main -> OsmoGridGuardian: !Run(cfg, ...)
+OsmoGridGuardian --> RunGuardian: //Spawn//
+OsmoGridGuardian -> RunGuardian: !Run(cfg, ...)
+RunGuardian --> InputDataProvider: //Spawn//
-OsmoGridGuardian --> ResultListener: //Spawn//
+RunGuardian --> ResultListener: //Spawn//
note right: Death watch of\n""ResultListener""
== LV generation ==
-OsmoGridGuardian --> LvCoordinator: //Spawn//
-OsmoGridGuardian -> LvCoordinator: !ReqLvGrids(...)
+RunGuardian --> LvCoordinator: //Spawn//
+RunGuardian -> LvCoordinator: !ReqLvGrids(...)
LvCoordinator -> InputDataProvider: !ReqOsm(...)
InputDataProvider <--> Input: //Read//
LvCoordinator -> InputDataProvider: !ReqAssetTypes(...)
@@ -68,21 +71,22 @@ LvRegionCoordinator -> LvRegionCoordinator: !Done
LvRegionCoordinator -> LvCoordinator: !Done
note left: All results are apparent;\nassign sub grid numbers
-LvCoordinator -> OsmoGridGuardian: !RepLvGrids(...)
+LvCoordinator -> RunGuardian: !RepLvGrids(...)
== MV generation ==
... **To be defined in a later stage** ...
== Result handling ==
-OsmoGridGuardian -> ResultListener: !GridResult(...)
+RunGuardian -> ResultListener: !GridResult(...)
activate ResultListener
... ...
-ResultListener -> OsmoGridGuardian: !ResultListenerDied
+ResultListener -> RunGuardian: !ResultListenerDied
deactivate ResultListener
-OsmoGridGuardian -> InputDataProvider: !Terminate(...)
+RunGuardian -> InputDataProvider: !Terminate(...)
InputDataProvider <--> Input: //Close//
-InputDataProvider -> OsmoGridGuardian: !InputDataProviderDied
+InputDataProvider -> RunGuardian: !InputDataProviderDied
+RunGuardian -> OsmoGridGuardian: !Done
OsmoGridGuardian -> Main: !Done
'TODO: Don't forget to spawn and initialize the ResultListener
diff --git a/src/main/resources/config/config-template.conf b/src/main/resources/config/config-template.conf
index 21e5f74f..55ffa25f 100644
--- a/src/main/resources/config/config-template.conf
+++ b/src/main/resources/config/config-template.conf
@@ -13,8 +13,9 @@ input.asset: {
}
output: {
#@optional
- file: {
+ csv: {
directory: String
+ separator: String | ";"
hierarchic: Boolean | false
}
}
diff --git a/src/main/scala/edu/ie3/osmogrid/cfg/ConfigFailFast.scala b/src/main/scala/edu/ie3/osmogrid/cfg/ConfigFailFast.scala
index 5c422184..fb11b7f0 100644
--- a/src/main/scala/edu/ie3/osmogrid/cfg/ConfigFailFast.scala
+++ b/src/main/scala/edu/ie3/osmogrid/cfg/ConfigFailFast.scala
@@ -6,19 +6,30 @@
package edu.ie3.osmogrid.cfg
+import akka.actor.typed.ActorRef
+import com.typesafe.scalalogging.LazyLogging
import edu.ie3.osmogrid.cfg.OsmoGridConfig.{Input, Output}
import edu.ie3.osmogrid.cfg.OsmoGridConfig.Input.{Asset, Osm}
import edu.ie3.osmogrid.cfg.OsmoGridConfig.Generation
import edu.ie3.osmogrid.cfg.OsmoGridConfig.Generation.Lv
import edu.ie3.osmogrid.cfg.OsmoGridConfig.Input.Asset.File
import edu.ie3.osmogrid.exception.IllegalConfigException
+import edu.ie3.osmogrid.io.output.ResultListener
-object ConfigFailFast {
- def check(cfg: OsmoGridConfig): Unit = cfg match {
- case OsmoGridConfig(generation, input, output) =>
- checkInputConfig(input)
- checkOutputConfig(output)
- checkGenerationConfig(generation)
+import scala.util.Try
+
+object ConfigFailFast extends LazyLogging {
+ def check(
+ cfg: OsmoGridConfig,
+ additionalListener: Seq[ActorRef[ResultListener.ResultEvent]] = Seq.empty
+ ): Try[OsmoGridConfig] = Try {
+ cfg match {
+ case OsmoGridConfig(generation, input, output) =>
+ checkInputConfig(input)
+ checkOutputConfig(output, additionalListener)
+ checkGenerationConfig(generation)
+ }
+ cfg
}
private def checkGenerationConfig(generation: Generation): Unit =
@@ -67,11 +78,8 @@ object ConfigFailFast {
}
private def checkAssetInputFile(file: OsmoGridConfig.Input.Asset.File): Unit =
- file match {
- case File(directory, _) if directory.isEmpty =>
- throw IllegalConfigException("Asset input directory may be set!")
- case _ => /* I don't care. Everything is fine. */
- }
+ if (file.directory.isEmpty)
+ throw IllegalConfigException("Asset input directory may be set!")
private def checkOsmInputConfig(osm: OsmoGridConfig.Input.Osm): Unit =
osm match {
@@ -83,26 +91,29 @@ object ConfigFailFast {
}
private def checkPbfFileDefinition(pbf: OsmoGridConfig.Input.Osm.Pbf): Unit =
- pbf match {
- case OsmoGridConfig.Input.Osm.Pbf(file) if file.isEmpty =>
- throw IllegalConfigException("Pbf file may be set!")
- case _ => /* I don't care. Everything is fine. */
- }
+ if (pbf.file.isEmpty) throw IllegalConfigException("Pbf file may be set!")
- private def checkOutputConfig(output: OsmoGridConfig.Output): Unit =
+ private def checkOutputConfig(
+ output: OsmoGridConfig.Output,
+ additionalListener: Seq[ActorRef[ResultListener.ResultEvent]]
+ ): Unit =
output match {
case Output(Some(file)) =>
checkOutputFile(file)
+ case Output(None) if additionalListener.nonEmpty =>
+ logger.info(
+ "No output data type defined, but other listener provided. Will use them accordingly!"
+ )
case Output(None) =>
throw IllegalConfigException(
- "You have to provide at least one output data type!"
+ "You have to provide at least one output data sink, e.g. to .csv-files!"
)
}
- private def checkOutputFile(file: OsmoGridConfig.Output.File): Unit =
- file match {
- case OsmoGridConfig.Output.File(directory, _) if directory.isEmpty =>
- throw IllegalConfigException("Output directory may be set!")
- case _ => /* I don't care. Everything is fine. */
- }
+ private def checkOutputFile(file: OsmoGridConfig.Output.Csv): Unit = if (
+ file.directory.isEmpty || file.separator.isEmpty
+ )
+ throw IllegalConfigException(
+ "Output directory and separator must be set when using .csv file sink!"
+ )
}
diff --git a/src/main/scala/edu/ie3/osmogrid/cfg/OsmoGridConfig.scala b/src/main/scala/edu/ie3/osmogrid/cfg/OsmoGridConfig.scala
index 650e6c53..5dcd3a1f 100644
--- a/src/main/scala/edu/ie3/osmogrid/cfg/OsmoGridConfig.scala
+++ b/src/main/scala/edu/ie3/osmogrid/cfg/OsmoGridConfig.scala
@@ -1,5 +1,5 @@
/*
- * © 2021. TU Dortmund University,
+ * © 2022. TU Dortmund University,
* Institute of Energy Systems, Energy Efficiency and Energy Economics,
* Research group Distribution grid planning and operation
*/
@@ -35,7 +35,7 @@ object OsmoGridConfig {
amountOfRegionCoordinators =
if (c.hasPathOrNull("amountOfRegionCoordinators"))
c.getInt("amountOfRegionCoordinators")
- else 50,
+ else 5,
distinctHouseConnections = c.hasPathOrNull(
"distinctHouseConnections"
) && c.getBoolean("distinctHouseConnections")
@@ -198,23 +198,26 @@ object OsmoGridConfig {
}
final case class Output(
- file: scala.Option[OsmoGridConfig.Output.File]
+ csv: scala.Option[OsmoGridConfig.Output.Csv]
)
object Output {
- final case class File(
+ final case class Csv(
directory: java.lang.String,
- hierarchic: scala.Boolean
+ hierarchic: scala.Boolean,
+ separator: java.lang.String
)
- object File {
+ object Csv {
def apply(
c: com.typesafe.config.Config,
parentPath: java.lang.String,
$tsCfgValidator: $TsCfgValidator
- ): OsmoGridConfig.Output.File = {
- OsmoGridConfig.Output.File(
+ ): OsmoGridConfig.Output.Csv = {
+ OsmoGridConfig.Output.Csv(
directory = $_reqStr(parentPath, c, "directory", $tsCfgValidator),
hierarchic =
- c.hasPathOrNull("hierarchic") && c.getBoolean("hierarchic")
+ c.hasPathOrNull("hierarchic") && c.getBoolean("hierarchic"),
+ separator =
+ if (c.hasPathOrNull("separator")) c.getString("separator") else ";"
)
}
private def $_reqStr(
@@ -241,14 +244,11 @@ object OsmoGridConfig {
$tsCfgValidator: $TsCfgValidator
): OsmoGridConfig.Output = {
OsmoGridConfig.Output(
- file =
- if (c.hasPathOrNull("file"))
+ csv =
+ if (c.hasPathOrNull("csv"))
scala.Some(
- OsmoGridConfig.Output.File(
- c.getConfig("file"),
- parentPath + "file.",
- $tsCfgValidator
- )
+ OsmoGridConfig.Output
+ .Csv(c.getConfig("csv"), parentPath + "csv.", $tsCfgValidator)
)
else None
)
diff --git a/src/main/scala/edu/ie3/osmogrid/exception/UnsupportedRequestException.scala b/src/main/scala/edu/ie3/osmogrid/exception/UnsupportedRequestException.scala
new file mode 100644
index 00000000..c142dcd4
--- /dev/null
+++ b/src/main/scala/edu/ie3/osmogrid/exception/UnsupportedRequestException.scala
@@ -0,0 +1,12 @@
+/*
+ * © 2021. TU Dortmund University,
+ * Institute of Energy Systems, Energy Efficiency and Energy Economics,
+ * Research group Distribution grid planning and operation
+ */
+
+package edu.ie3.osmogrid.exception
+
+case class UnsupportedRequestException(
+ msg: String = "",
+ cause: Throwable = None.orNull
+) extends Exception(msg, cause)
diff --git a/src/main/scala/edu/ie3/osmogrid/guardian/OsmoGridGuardian.scala b/src/main/scala/edu/ie3/osmogrid/guardian/OsmoGridGuardian.scala
index 30657f75..4a1011b8 100644
--- a/src/main/scala/edu/ie3/osmogrid/guardian/OsmoGridGuardian.scala
+++ b/src/main/scala/edu/ie3/osmogrid/guardian/OsmoGridGuardian.scala
@@ -7,131 +7,90 @@
package edu.ie3.osmogrid.guardian
import akka.actor.typed.Behavior
-import akka.actor.typed.scaladsl.Behaviors
+import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import akka.actor.typed.ActorRef
import edu.ie3.datamodel.models.input.container.{
JointGridContainer,
SubGridContainer
}
import edu.ie3.datamodel.utils.ContainerUtils
-import edu.ie3.osmogrid.cfg.OsmoGridConfig
-import edu.ie3.osmogrid.cfg.OsmoGridConfig.Generation
+import edu.ie3.osmogrid.cfg.{ConfigFailFast, OsmoGridConfig}
+import edu.ie3.osmogrid.cfg.OsmoGridConfig.{Generation, Output}
+import edu.ie3.osmogrid.guardian.run.Run
+import edu.ie3.osmogrid.guardian.run.RunGuardian
import edu.ie3.osmogrid.io.input.InputDataProvider
-import edu.ie3.osmogrid.io.input.InputDataProvider.InputDataEvent
import edu.ie3.osmogrid.io.output.ResultListener
-import edu.ie3.osmogrid.io.output.ResultListener.{GridResult, ResultEvent}
+import edu.ie3.osmogrid.io.output.ResultListener.{GridResult, Request}
import edu.ie3.osmogrid.lv.LvCoordinator
import edu.ie3.osmogrid.lv.LvCoordinator.ReqLvGrids
+import org.slf4j.Logger
+import java.util.UUID
import scala.jdk.CollectionConverters.*
import scala.util.{Failure, Success, Try}
object OsmoGridGuardian {
- sealed trait OsmoGridGuardianEvent
- final case class Run(cfg: OsmoGridConfig) extends OsmoGridGuardianEvent
- object InputDataProviderDied extends OsmoGridGuardianEvent
- object ResultEventListenerDied extends OsmoGridGuardianEvent
- object LvCoordinatorDied extends OsmoGridGuardianEvent
- final case class RepLvGrids(grids: Vector[SubGridContainer])
- extends OsmoGridGuardianEvent
+ sealed trait Request
- def apply(): Behavior[OsmoGridGuardianEvent] = idle()
+ /** Message to initiate a grid generation run
+ *
+ * @param cfg
+ * Configuration for the tool
+ * @param additionalListener
+ * Addresses of additional listeners to be informed about results
+ * @param runId
+ * Unique identifier for that generation run
+ */
+ final case class Run(
+ cfg: OsmoGridConfig,
+ additionalListener: Seq[ActorRef[ResultListener.ResultEvent]] = Seq.empty,
+ runId: UUID = UUID.randomUUID()
+ ) extends Request
- private def idle(): Behavior[OsmoGridGuardianEvent] =
- Behaviors.receive { (ctx, msg) =>
- msg match {
- case Run(cfg) =>
- ctx.log.info("Initializing grid generation!")
+ /* dead watch events */
+ sealed trait Watch extends Request {
+ val runId: UUID
+ }
- ctx.log.info("Starting input data provider")
- val inputProvider =
- ctx.spawn(InputDataProvider(cfg.input), "InputDataProvider")
- ctx.watchWith(inputProvider, InputDataProviderDied)
- ctx.log.debug("Starting output data listener")
- val resultEventListener =
- ctx.spawn(ResultListener(cfg.output), "ResultListener")
- ctx.watchWith(resultEventListener, ResultEventListenerDied)
+ private[guardian] final case class RunGuardianDied(override val runId: UUID)
+ extends Watch
- /* Check, which voltage level configs are given. Start with lv level, if this is desired for. */
- cfg.generation match {
- case Generation(Some(lvConfig)) =>
- ctx.log.debug("Starting low voltage grid coordinator.")
- val lvCoordinator = ctx.spawn(LvCoordinator(), "LvCoordinator")
- ctx.watchWith(lvCoordinator, LvCoordinatorDied)
- lvCoordinator ! ReqLvGrids(lvConfig, ctx.self)
- awaitLvGrids(inputProvider, resultEventListener)
- case unsupported =>
- ctx.log.error(
- "Received unsupported grid generation config. Bye, bye."
- )
- Behaviors.stopped
- }
- case InputDataProviderDied =>
- ctx.log.error("Input data provider died. That's bad...")
- Behaviors.stopped
- case ResultEventListenerDied =>
- ctx.log.error("Result event listener died. That's bad...")
- Behaviors.stopped
- case LvCoordinatorDied =>
- ctx.log.error("Lv coordinator died. That's bad...")
- Behaviors.stopped
- case unsupported =>
- ctx.log.error(s"Received unsupported message '$unsupported'.")
- Behaviors.stopped
- }
- }
+ /** Relevant, state-independent data, the the actor needs to know
+ *
+ * @param runs
+ * Currently active conversion runs
+ */
+ private[guardian] final case class GuardianData(
+ runs: Seq[UUID]
+ ) {
+ def append(run: UUID): GuardianData = this.copy(runs = runs :+ run)
- private def awaitLvGrids(
- inputDataProvider: ActorRef[InputDataEvent],
- resultListener: ActorRef[ResultEvent]
- ): Behaviors.Receive[OsmoGridGuardianEvent] =
- Behaviors.receive { (ctx, msg) =>
- msg match {
- case RepLvGrids(lvGrids) =>
- ctx.log.info(s"Received ${lvGrids.length} lv grids. Join them.")
- Try(ContainerUtils.combineToJointGrid(lvGrids.asJava)) match {
- case Success(jointGrid) =>
- resultListener ! GridResult(jointGrid, ctx.self)
- awaitShutDown(inputDataProvider)
- case Failure(exception) =>
- ctx.log.error(
- "Combination of received sub-grids failed. Shutting down."
- )
- Behaviors.stopped
- }
- case unsupported =>
- ctx.log.error(
- s"Received unsupported message while waiting for lv grids. Unsupported: $unsupported"
- )
- Behaviors.stopped
- }
- }
-
- private def awaitShutDown(
- inputDataProvider: ActorRef[InputDataEvent],
- resultListenerTerminated: Boolean = false,
- inputDataProviderTerminated: Boolean = false
- ): Behaviors.Receive[OsmoGridGuardianEvent] = Behaviors.receive {
- (ctx, msg) =>
- msg match {
- case ResultEventListenerDied =>
- ctx.log.info("Result listener finished handling the result.")
- ctx.log.debug("Shut down input data provider.")
- awaitShutDown(inputDataProvider, resultListenerTerminated = true)
- case InputDataProviderDied if resultListenerTerminated =>
- /* That's the fine case */
- ctx.log.info("Input data provider shut down.")
- Behaviors.stopped
- case InputDataProviderDied =>
- /* That's the malicious case */
- ctx.log.error(
- "Input data provider unexpectedly died during shutdown was initiated."
- )
- Behaviors.stopped
- case unsupported =>
- ctx.log.error(s"Received an unsupported message $unsupported.")
- Behaviors.stopped
- }
+ def remove(run: UUID): GuardianData =
+ this.copy(runs = runs.filterNot(_ == run))
+ }
+ object GuardianData {
+ def empty = new GuardianData(Seq.empty[UUID])
}
+
+ def apply(): Behavior[Request] = idle(GuardianData.empty)
+
+ private[guardian] def idle(guardianData: GuardianData): Behavior[Request] =
+ Behaviors.receive {
+ case (ctx, Run(cfg, additionalListener, runId)) =>
+ val runGuardian = ctx.spawn(
+ RunGuardian(cfg, additionalListener, runId),
+ s"RunGuardian_$runId"
+ )
+ ctx.watchWith(runGuardian, RunGuardianDied(runId))
+ runGuardian ! run.Run
+ idle(guardianData.append(runId))
+
+ case (ctx, watch: Watch) =>
+ watch match {
+ case RunGuardianDied(runId) =>
+ ctx.log.info(s"Run $runId terminated.")
+ idle(guardianData.remove(runId))
+ }
+ }
}
diff --git a/src/main/scala/edu/ie3/osmogrid/guardian/run/RunGuardian.scala b/src/main/scala/edu/ie3/osmogrid/guardian/run/RunGuardian.scala
new file mode 100644
index 00000000..7e21085d
--- /dev/null
+++ b/src/main/scala/edu/ie3/osmogrid/guardian/run/RunGuardian.scala
@@ -0,0 +1,181 @@
+/*
+ * © 2022. TU Dortmund University,
+ * Institute of Energy Systems, Energy Efficiency and Energy Economics,
+ * Research group Distribution grid planning and operation
+ */
+
+package edu.ie3.osmogrid.guardian.run
+
+import akka.actor.typed.scaladsl.Behaviors
+import akka.actor.typed.{ActorRef, Behavior}
+import edu.ie3.osmogrid.cfg.OsmoGridConfig
+import edu.ie3.osmogrid.guardian.run.MessageAdapters
+import edu.ie3.osmogrid.guardian.run.MessageAdapters.{
+ WrappedListenerResponse,
+ WrappedLvCoordinatorResponse
+}
+import edu.ie3.osmogrid.guardian.run.{RunSupport, StopSupport, SubGridHandling}
+import edu.ie3.osmogrid.io.input.InputDataProvider
+import edu.ie3.osmogrid.io.output.ResultListener
+import edu.ie3.osmogrid.lv.LvCoordinator
+
+import java.util.UUID
+import scala.util.{Failure, Success}
+
+/** Actor to take care of a specific simulation run
+ */
+object RunGuardian extends RunSupport with StopSupport with SubGridHandling {
+
+ /** Instantiate the actor
+ *
+ * @param cfg
+ * Configuration for the tool
+ * @param additionalListener
+ * Addresses of additional listeners to be informed about results
+ * @param runId
+ * Unique identifier for that generation run
+ */
+ def apply(
+ cfg: OsmoGridConfig,
+ additionalListener: Seq[ActorRef[ResultListener.ResultEvent]] = Seq.empty,
+ runId: UUID
+ ): Behavior[Request] = Behaviors.setup { ctx =>
+ idle(
+ RunGuardianData(
+ runId,
+ cfg,
+ additionalListener,
+ MessageAdapters(
+ ctx.messageAdapter(msg => WrappedLvCoordinatorResponse(msg)),
+ ctx.messageAdapter(msg => WrappedListenerResponse(msg))
+ )
+ )
+ )
+ }
+
+ /** This actor is in idle state and waits for any kind of request
+ *
+ * @param runGuardianData
+ * Meta information describing the current actor's state
+ * @return
+ * the next state
+ */
+ private def idle(runGuardianData: RunGuardianData): Behavior[Request] =
+ Behaviors.receive {
+ case (ctx, Run) =>
+ /* Start a run */
+ initRun(
+ runGuardianData,
+ ctx
+ ) match {
+ case Success(childReferences) =>
+ running(
+ runGuardianData,
+ childReferences
+ )
+ case Failure(exception) =>
+ ctx.log.error(
+ s"Unable to start run ${runGuardianData.runId}.",
+ exception
+ )
+ Behaviors.stopped
+ }
+ case (ctx, notUnderstood) =>
+ ctx.log.error(
+ s"Received a message, that I don't understand during idle phase of run ${runGuardianData.runId}.\n\tMessage: $notUnderstood"
+ )
+ Behaviors.same
+ }
+
+ /** Behavior to indicate, that a simulation run is currently active
+ *
+ * @param runGuardianData
+ * Meta information describing the current actor's state
+ * @param childReferences
+ * References to child actors
+ * @return
+ * The next state
+ */
+ private def running(
+ runGuardianData: RunGuardianData,
+ childReferences: ChildReferences
+ ): Behavior[Request] = Behaviors.receive {
+ case (
+ ctx,
+ WrappedLvCoordinatorResponse(
+ LvCoordinator.RepLvGrids(subGridContainers)
+ )
+ ) =>
+ /* Handle the grid results and wait for the listener to report back */
+ handleLvResults(
+ subGridContainers,
+ runGuardianData.cfg.generation,
+ childReferences.resultListeners,
+ runGuardianData.msgAdapters
+ )(ctx.log)
+ Behaviors.same
+ case (
+ ctx,
+ WrappedListenerResponse(ResultListener.ResultHandled(_, sender))
+ ) =>
+ ctx.log.debug(
+ s"The listener $sender has successfully handled the result event of run ${runGuardianData.runId}."
+ )
+ if (
+ childReferences.resultListener.contains(
+ sender
+ ) || childReferences.resultListener.isEmpty
+ ) {
+ /* Start coordinated shutdown */
+ ctx.log.info(
+ s"Run ${runGuardianData.runId} successfully finished. Stop all run-related processes."
+ )
+ stopping(stopChildren(runGuardianData.runId, childReferences, ctx))
+ } else {
+ /* Somebody did something great, but nothing, that affects us */
+ Behaviors.same
+ }
+ case (ctx, watch: Watch) =>
+ /* Somebody died unexpectedly. Start coordinated shutdown */
+ stopping(
+ handleUnexpectedShutDown(
+ runGuardianData.runId,
+ childReferences,
+ watch,
+ ctx
+ )
+ )
+ case (ctx, notUnderstood) =>
+ ctx.log.error(
+ s"Received a message, that I don't understand during active run ${runGuardianData.runId}.\n\tMessage: $notUnderstood"
+ )
+ Behaviors.same
+ }
+
+ /** Behavior, that indicates, that a coordinate shutdown of the children takes
+ * place
+ *
+ * @param stoppingData
+ * Information about who already has terminated
+ * @return
+ * The next state
+ */
+ private def stopping(
+ stoppingData: StoppingData
+ ): Behavior[Request] = Behaviors.receive {
+ case (ctx, watch: Watch) =>
+ val updatedStoppingData = registerCoordinatedShutDown(watch, stoppingData)
+ if (updatedStoppingData.allChildrenTerminated) {
+ ctx.log.info(
+ s"All child processes of run ${stoppingData.runId} successfully terminated. Finally terminating the whole run process."
+ )
+ /* The overall guardian is automatically informed via death watch */
+ Behaviors.stopped
+ } else stopping(updatedStoppingData)
+ case (ctx, notUnderstood) =>
+ ctx.log.error(
+ s"Received a message, that I don't understand during coordinated shutdown phase of run ${stoppingData.runId}.\n\tMessage: $notUnderstood"
+ )
+ Behaviors.same
+ }
+}
diff --git a/src/main/scala/edu/ie3/osmogrid/guardian/run/RunSupport.scala b/src/main/scala/edu/ie3/osmogrid/guardian/run/RunSupport.scala
new file mode 100644
index 00000000..5426832e
--- /dev/null
+++ b/src/main/scala/edu/ie3/osmogrid/guardian/run/RunSupport.scala
@@ -0,0 +1,201 @@
+/*
+ * © 2022. TU Dortmund University,
+ * Institute of Energy Systems, Energy Efficiency and Energy Economics,
+ * Research group Distribution grid planning and operation
+ */
+
+package edu.ie3.osmogrid.guardian.run
+
+import akka.actor.typed.ActorRef
+import akka.actor.typed.scaladsl.ActorContext
+import edu.ie3.osmogrid.cfg.OsmoGridConfig.{Generation, Output}
+import edu.ie3.osmogrid.cfg.{ConfigFailFast, OsmoGridConfig}
+import edu.ie3.osmogrid.exception.UnsupportedRequestException
+import edu.ie3.osmogrid.guardian.run.RunGuardian
+import edu.ie3.osmogrid.io.input.InputDataProvider
+import edu.ie3.osmogrid.io.output.ResultListener
+import edu.ie3.osmogrid.lv.LvCoordinator
+import edu.ie3.osmogrid.lv.LvCoordinator.ReqLvGrids
+
+import java.util.UUID
+import scala.util.{Failure, Success, Try}
+
+private trait RunSupport {
+
+ /** Initiate a generation run and return the updated run meta data
+ *
+ * @param runGuardianData
+ * Meta information describing the current actor's state
+ * @param ctx
+ * Current actor context
+ * @return
+ * Updated run meta data
+ */
+ protected def initRun(
+ runGuardianData: RunGuardianData,
+ ctx: ActorContext[Request]
+ ): Try[ChildReferences] = {
+ val log = ctx.log
+ ConfigFailFast
+ .check(
+ runGuardianData.cfg,
+ runGuardianData.additionalListener
+ )
+ .flatMap { validConfig =>
+ log.info(
+ s"Initializing grid generation for run with id '${runGuardianData.runId}'!"
+ )
+
+ /* Check, which voltage level configs are given. Start with lv level, if this is desired for. */
+ validConfig.generation match {
+ case Generation(Some(lvConfig)) =>
+ ctx.log.info("Starting low voltage grid coordinator ...")
+ val (inputProvider, resultEventListener) =
+ spawnIoActors(
+ runGuardianData.runId,
+ validConfig.input,
+ validConfig.output,
+ ctx
+ )
+ val lvCoordinator = startLvGridGeneration(
+ runGuardianData.runId,
+ lvConfig,
+ runGuardianData.msgAdapters.lvCoordinator,
+ ctx
+ )
+ Success(
+ ChildReferences(
+ inputProvider,
+ resultEventListener,
+ runGuardianData.additionalListener,
+ Some(lvCoordinator)
+ )
+ )
+ case unsupported =>
+ ctx.log.error(
+ s"Received unsupported grid generation config '$unsupported'. Stopping run with id '${runGuardianData.runId}'!"
+ )
+ Failure(
+ UnsupportedRequestException(
+ s"Unable to issue a generation run with the given parameters: '${validConfig.generation}'"
+ )
+ )
+ }
+ }
+ }
+
+ /** Spawns both the input and the output actor for the given specific run
+ *
+ * @param runId
+ * Identifier for the targeted run
+ * @param inputConfig
+ * Configuration for the input behavior
+ * @param outputConfig
+ * Configuration of the output behavior
+ * @param ctx
+ * Current actor context
+ * @return
+ * Reference to an [[InputDataProvider]] as well as [[ResultListener]]
+ */
+ private def spawnIoActors(
+ runId: UUID,
+ inputConfig: OsmoGridConfig.Input,
+ outputConfig: OsmoGridConfig.Output,
+ ctx: ActorContext[Request]
+ ): (
+ ActorRef[InputDataProvider.Request],
+ Option[ActorRef[ResultListener.ResultEvent]]
+ ) = (
+ spawnInputDataProvider(runId, inputConfig, ctx),
+ spawnResultListener(runId, outputConfig, ctx)
+ )
+
+ /** Spawn an input data provider for this run
+ *
+ * @param runId
+ * Identifier for the targeted run
+ * @param inputConfig
+ * Configuration for the input behavior
+ * @param ctx
+ * Current actor context
+ * @return
+ * Reference to an [[InputDataProvider]]
+ */
+ private def spawnInputDataProvider(
+ runId: UUID,
+ inputConfig: OsmoGridConfig.Input,
+ ctx: ActorContext[Request]
+ ): ActorRef[InputDataProvider.Request] = {
+ ctx.log.info("Starting input data provider ...")
+ val inputProvider =
+ ctx.spawn(
+ InputDataProvider(inputConfig),
+ s"InputDataProvider_${runId.toString}"
+ )
+ ctx.watchWith(inputProvider, InputDataProviderDied)
+ inputProvider
+ }
+
+ /** Spawn a result listener for the specified run
+ *
+ * @param runId
+ * Identifier for the targeted run
+ * @param outputConfig
+ * Configuration of the output behavior
+ * @param ctx
+ * Current actor context
+ * @return
+ * References to [[ResultListener]]
+ */
+ private def spawnResultListener(
+ runId: UUID,
+ outputConfig: OsmoGridConfig.Output,
+ ctx: ActorContext[Request]
+ ): Option[ActorRef[ResultListener.ResultEvent]] = {
+ val resultListener = outputConfig match {
+ case Output(Some(_)) =>
+ ctx.log.info("Starting output data listener ...")
+ Some(
+ ctx.spawn(
+ ResultListener(runId, outputConfig),
+ s"PersistenceResultListener_${runId.toString}"
+ )
+ )
+ case Output(None) =>
+ ctx.log.warn(s"No result listener configured for run $runId.")
+ None
+ }
+ resultListener.foreach(
+ ctx.watchWith(_, ResultEventListenerDied)
+ )
+ resultListener
+ }
+
+ /** Spawn a [[LvCoordinator]] for the targeted run and ask it to start
+ * conversion
+ *
+ * @param runId
+ * Identifier for the targeted run
+ * @param lvConfig
+ * Configuration for low voltage grid generation
+ * @param lvCoordinatorAdapter
+ * Message adapter to understand responses from [[LvCoordinator]]
+ * @param ctx
+ * Current actor context
+ */
+ private def startLvGridGeneration(
+ runId: UUID,
+ lvConfig: OsmoGridConfig.Generation.Lv,
+ lvCoordinatorAdapter: ActorRef[LvCoordinator.Response],
+ ctx: ActorContext[Request]
+ ): ActorRef[LvCoordinator.Request] = {
+ val lvCoordinator =
+ ctx.spawn(LvCoordinator(), s"LvCoordinator_${runId.toString}")
+ ctx.watchWith(lvCoordinator, LvCoordinatorDied)
+
+ ctx.log.info("Starting voltage level grid generation ...")
+ lvCoordinator ! ReqLvGrids(lvConfig, lvCoordinatorAdapter)
+
+ lvCoordinator
+ }
+}
diff --git a/src/main/scala/edu/ie3/osmogrid/guardian/run/StopSupport.scala b/src/main/scala/edu/ie3/osmogrid/guardian/run/StopSupport.scala
new file mode 100644
index 00000000..a3ac72ff
--- /dev/null
+++ b/src/main/scala/edu/ie3/osmogrid/guardian/run/StopSupport.scala
@@ -0,0 +1,110 @@
+/*
+ * © 2022. TU Dortmund University,
+ * Institute of Energy Systems, Energy Efficiency and Energy Economics,
+ * Research group Distribution grid planning and operation
+ */
+
+package edu.ie3.osmogrid.guardian.run
+
+import akka.actor.typed.SupervisorStrategy.Stop
+import akka.actor.typed.scaladsl.ActorContext
+import edu.ie3.osmogrid.guardian.run.RunGuardian
+import edu.ie3.osmogrid.io.input.InputDataProvider
+import edu.ie3.osmogrid.io.output.ResultListener
+import edu.ie3.osmogrid.lv.LvCoordinator
+import edu.ie3.osmogrid.lv.LvCoordinator.Terminate
+
+import java.util.UUID
+
+trait StopSupport {
+
+ /** Stop all children for the given run. The additional listeners are not
+ * asked to be stopped!
+ *
+ * @param runId
+ * Identifier of the current run
+ * @param childReferences
+ * References to children
+ * @param ctx
+ * Current actor context
+ */
+ protected def stopChildren(
+ runId: UUID,
+ childReferences: ChildReferences,
+ ctx: ActorContext[Request]
+ ): StoppingData = {
+ childReferences.lvCoordinator.foreach(_ ! LvCoordinator.Terminate)
+ childReferences.inputDataProvider ! InputDataProvider.Terminate
+ childReferences.resultListener.foreach(_ ! ResultListener.Terminate)
+
+ StoppingData(
+ runId,
+ false,
+ false,
+ childReferences.lvCoordinator.map(_ => false)
+ )
+ }
+
+ /** Register [[Watch]] messages within the coordinated shutdown phase of a run
+ *
+ * @param watchMsg
+ * Received [[Watch]] message
+ * @param stoppingData
+ * State data for the stopping run
+ * @return
+ * Next state with updated [[GuardianData]]
+ */
+ protected def registerCoordinatedShutDown(
+ watchMsg: Watch,
+ stoppingData: StoppingData
+ ): StoppingData = watchMsg match {
+ case InputDataProviderDied =>
+ stoppingData.copy(inputDataProviderTerminated = true)
+ case ResultEventListenerDied =>
+ stoppingData.copy(resultListenerTerminated = true)
+ case LvCoordinatorDied =>
+ stoppingData.copy(lvCoordinatorTerminated =
+ stoppingData.lvCoordinatorTerminated.map(_ => true)
+ )
+ }
+
+ /** Handle an unexpected shutdown of children and start coordinated shutdown
+ * phase for that run
+ *
+ * @param runId
+ * Identifier of the current run
+ * @param childReferences
+ * References to child actors
+ * @param watchMsg
+ * Received [[Watch]] message
+ * @param ctx
+ * Current Actor context
+ * @return
+ * Next state with updated [[GuardianData]]
+ */
+ protected def handleUnexpectedShutDown(
+ runId: UUID,
+ childReferences: ChildReferences,
+ watchMsg: Watch,
+ ctx: ActorContext[Request]
+ ): StoppingData = {
+ (stopChildren(runId, childReferences, ctx), watchMsg) match {
+ case (stoppingData, InputDataProviderDied) =>
+ ctx.log.warn(
+ s"Input data provider for run $runId unexpectedly died. Start coordinated shut down phase for this run."
+ )
+ stoppingData.copy(inputDataProviderTerminated = true)
+ case (stoppingData, ResultEventListenerDied) =>
+ ctx.log.warn(
+ s"One of the result listener for run $runId unexpectedly died. Start coordinated shut down phase for this run."
+ )
+ stoppingData.copy(resultListenerTerminated = true)
+ case (stoppingData, LvCoordinatorDied) =>
+ ctx.log.warn(
+ s"Lv coordinator for run $runId unexpectedly died. Start coordinated shut down phase for this run."
+ )
+ stoppingData.copy(resultListenerTerminated = true)
+ }
+
+ }
+}
diff --git a/src/main/scala/edu/ie3/osmogrid/guardian/run/SubGridHandling.scala b/src/main/scala/edu/ie3/osmogrid/guardian/run/SubGridHandling.scala
new file mode 100644
index 00000000..1c120bda
--- /dev/null
+++ b/src/main/scala/edu/ie3/osmogrid/guardian/run/SubGridHandling.scala
@@ -0,0 +1,68 @@
+/*
+ * © 2022. TU Dortmund University,
+ * Institute of Energy Systems, Energy Efficiency and Energy Economics,
+ * Research group Distribution grid planning and operation
+ */
+
+package edu.ie3.osmogrid.guardian.run
+
+import akka.actor.typed.ActorRef
+import edu.ie3.datamodel.models.input.container.SubGridContainer
+import edu.ie3.datamodel.utils.ContainerUtils
+import edu.ie3.osmogrid.cfg.OsmoGridConfig
+import edu.ie3.osmogrid.guardian.run.RunGuardian
+import edu.ie3.osmogrid.guardian.run.SubGridHandling.assignSubnetNumbers
+import edu.ie3.osmogrid.io.output.ResultListener
+import org.slf4j.Logger
+
+import java.util.UUID
+import scala.jdk.CollectionConverters.*
+
+trait SubGridHandling {
+
+ /** Handle incoming low voltage grid results
+ *
+ * @param grids
+ * Received grids
+ * @param cfg
+ * Grid generation config
+ * @param resultListener
+ * References to the responsible result listener
+ * @param msgAdapters
+ * Collection of all message adapters
+ */
+ protected def handleLvResults(
+ grids: Seq[SubGridContainer],
+ cfg: OsmoGridConfig.Generation,
+ resultListener: Seq[ActorRef[ResultListener.ResultEvent]],
+ msgAdapters: MessageAdapters
+ )(implicit log: Logger): Unit = {
+ log.info("All lv grids successfully generated.")
+ val updatedSubGrids = assignSubnetNumbers(grids)
+
+ log.debug(
+ "No further generation steps intended. Hand over results to result handler."
+ )
+ /* Bundle grid result and inform interested listeners */
+ val jointGrid =
+ ContainerUtils.combineToJointGrid(updatedSubGrids.asJava)
+ resultListener.foreach { listener =>
+ listener ! ResultListener.GridResult(
+ jointGrid,
+ msgAdapters.resultListener
+ )
+ }
+ }
+}
+
+object SubGridHandling {
+ private def assignSubnetNumbers(
+ subnets: Seq[SubGridContainer]
+ ): Seq[SubGridContainer] = subnets.zipWithIndex.map {
+ case (subGrid, subnetNumber) =>
+ assignSubnetNumber(subGrid, subnetNumber + 1)
+ }
+
+ private def assignSubnetNumber(subGrid: SubGridContainer, subnetNumber: Int) =
+ subGrid
+}
diff --git a/src/main/scala/edu/ie3/osmogrid/guardian/run/run.scala b/src/main/scala/edu/ie3/osmogrid/guardian/run/run.scala
new file mode 100644
index 00000000..1ad53252
--- /dev/null
+++ b/src/main/scala/edu/ie3/osmogrid/guardian/run/run.scala
@@ -0,0 +1,101 @@
+/*
+ * © 2022. TU Dortmund University,
+ * Institute of Energy Systems, Energy Efficiency and Energy Economics,
+ * Research group Distribution grid planning and operation
+ */
+
+package edu.ie3.osmogrid.guardian.run
+
+import akka.actor.typed.ActorRef
+import edu.ie3.osmogrid.cfg.OsmoGridConfig
+import edu.ie3.osmogrid.io.input.InputDataProvider
+import edu.ie3.osmogrid.io.output.ResultListener
+import edu.ie3.osmogrid.lv.LvCoordinator
+
+import java.util.UUID
+
+/* This file only contains package-level definitions */
+
+/* Received requests */
+sealed trait Request
+
+object Run extends Request
+
+/** Container object with all available adapters for outside protocol messages
+ *
+ * @param lvCoordinator
+ * Adapter for messages from [[LvCoordinator]]
+ * @param resultListener
+ * Adapter for messages from [[ResultEventListener]]
+ */
+private final case class MessageAdapters(
+ lvCoordinator: ActorRef[LvCoordinator.Response],
+ resultListener: ActorRef[ResultListener.Response]
+)
+
+private object MessageAdapters {
+ final case class WrappedLvCoordinatorResponse(
+ response: LvCoordinator.Response
+ ) extends Request
+
+ final case class WrappedListenerResponse(
+ response: ResultListener.Response
+ ) extends Request
+}
+
+/* Death watch messages */
+sealed trait Watch extends Request
+
+object InputDataProviderDied extends Watch
+
+object ResultEventListenerDied extends Watch
+
+object LvCoordinatorDied extends Watch
+
+/* Sent out responses */
+sealed trait Response
+
+final case class Done(runId: UUID) extends Response
+
+private final case class ChildReferences(
+ inputDataProvider: ActorRef[InputDataProvider.Request],
+ resultListener: Option[ActorRef[ResultListener.ResultEvent]],
+ additionalResultListeners: Seq[ActorRef[ResultListener.ResultEvent]],
+ lvCoordinator: Option[ActorRef[LvCoordinator.Request]]
+) {
+ def resultListeners: Seq[ActorRef[ResultListener.ResultEvent]] =
+ resultListener
+ .map(Seq(_))
+ .getOrElse(Seq.empty) ++ additionalResultListeners
+}
+
+private sealed trait StateData
+private final case class RunGuardianData(
+ runId: UUID,
+ cfg: OsmoGridConfig,
+ additionalListener: Seq[ActorRef[ResultListener.ResultEvent]],
+ msgAdapters: MessageAdapters
+) extends StateData
+
+/** Meta data to keep track of which children already terminated during the
+ * coordinated shutdown phase
+ *
+ * @param runId
+ * Identifier of the run
+ * @param inputDataProviderTerminated
+ * If the [[InputDataProvider]] has stopped
+ * @param resultListenerTerminated
+ * If the [[ResultListener]] has stopped
+ * @param lvCoordinatorTerminated
+ * Optional information, if the [[LvCoordinator]] has stopped
+ */
+private final case class StoppingData(
+ runId: UUID,
+ inputDataProviderTerminated: Boolean,
+ resultListenerTerminated: Boolean,
+ lvCoordinatorTerminated: Option[Boolean]
+) extends StateData {
+ def allChildrenTerminated: Boolean =
+ inputDataProviderTerminated && resultListenerTerminated && lvCoordinatorTerminated
+ .forall(terminated => terminated)
+}
diff --git a/src/main/scala/edu/ie3/osmogrid/io/input/InputDataProvider.scala b/src/main/scala/edu/ie3/osmogrid/io/input/InputDataProvider.scala
index fafbd873..1a967ca5 100644
--- a/src/main/scala/edu/ie3/osmogrid/io/input/InputDataProvider.scala
+++ b/src/main/scala/edu/ie3/osmogrid/io/input/InputDataProvider.scala
@@ -8,29 +8,35 @@ package edu.ie3.osmogrid.io.input
import akka.actor.typed.Behavior
import akka.actor.typed.ActorRef
+import akka.actor.typed.PostStop
import akka.actor.typed.scaladsl.Behaviors
import edu.ie3.osmogrid.cfg.OsmoGridConfig
-import edu.ie3.osmogrid.guardian.OsmoGridGuardian.OsmoGridGuardianEvent
+import edu.ie3.osmogrid.guardian.OsmoGridGuardian
object InputDataProvider {
- sealed trait InputDataEvent
+ sealed trait Request
final case class Read()
- extends InputDataEvent // todo this read method should contain configuration parameters for the actual source + potential filter options
- final case class Terminate(replyTo: ActorRef[OsmoGridGuardianEvent])
- extends InputDataEvent
+ extends Request // todo this read method should contain configuration parameters for the actual source + potential filter options
+ object Terminate extends Request
- def apply(cfg: OsmoGridConfig.Input): Behavior[InputDataEvent] =
- Behaviors.receive { (ctx, msg) =>
- msg match {
- case _: Read =>
+ sealed trait Response
+
+ def apply(cfg: OsmoGridConfig.Input): Behavior[Request] =
+ Behaviors
+ .receive[Request] {
+ case (ctx, _: Read) =>
ctx.log.warn("Reading of data not yet implemented.")
Behaviors.same
- case Terminate(_) =>
+ case (ctx, Terminate) =>
ctx.log.info("Stopping input data provider")
- // TODO: Any closing of sources and stuff
- Behaviors.stopped
+ Behaviors.stopped { () => cleanUp() }
+ }
+ .receiveSignal { case (ctx, PostStop) =>
+ ctx.log.info("Requested to stop.")
+ cleanUp()
+ Behaviors.same
}
- }
+ private def cleanUp(): Unit = ???
}
diff --git a/src/main/scala/edu/ie3/osmogrid/io/output/ResultListener.scala b/src/main/scala/edu/ie3/osmogrid/io/output/ResultListener.scala
index 064ff7f9..07b7671f 100644
--- a/src/main/scala/edu/ie3/osmogrid/io/output/ResultListener.scala
+++ b/src/main/scala/edu/ie3/osmogrid/io/output/ResultListener.scala
@@ -6,33 +6,50 @@
package edu.ie3.osmogrid.io.output
-import akka.actor.typed.Behavior
-import akka.actor.typed.ActorRef
+import akka.actor.typed.{ActorRef, Behavior, PostStop}
import akka.actor.typed.scaladsl.Behaviors
import edu.ie3.datamodel.models.input.container.{
GridContainer,
JointGridContainer
}
import edu.ie3.osmogrid.cfg.OsmoGridConfig
-import edu.ie3.osmogrid.guardian.OsmoGridGuardian.OsmoGridGuardianEvent
+import edu.ie3.osmogrid.guardian.OsmoGridGuardian
+
+import java.util.UUID
object ResultListener {
+ sealed trait Request
+ final case class GridResult(
+ grid: GridContainer,
+ replyTo: ActorRef[Response]
+ ) extends Request
+ with ResultEvent
+ object Terminate extends Request with ResultEvent
+
+ sealed trait Response
+ final case class ResultHandled(
+ runId: UUID,
+ replyTo: ActorRef[ResultListener.ResultEvent]
+ ) extends Response
+ /* internal API */
sealed trait ResultEvent
- final case class GridResult(
- grid: JointGridContainer,
- replyTo: ActorRef[OsmoGridGuardianEvent]
- ) extends ResultEvent
-
- def apply(cfg: OsmoGridConfig.Output): Behavior[ResultEvent] =
- Behaviors.receive { (ctx, msg) =>
- msg match {
- case GridResult(grid, _) =>
- ctx.log.info(s"Received grid result for grid '${grid.getGridName}'")
- // TODO: Actual persistence and stuff, closing sinks, ...
- Behaviors.stopped
+ def apply(runId: UUID, cfg: OsmoGridConfig.Output): Behavior[ResultEvent] =
+ Behaviors
+ .receive[ResultEvent] {
+ case (ctx, GridResult(grid, replyTo)) =>
+ ctx.log.info(s"Received grid result for run id '${runId.toString}'")
+ // TODO: Actual persistence and stuff, ...
+ replyTo ! ResultHandled(runId, ctx.self)
+ Behaviors.stopped { () => cleanUp() }
+ case (ctx, Terminate) => Behaviors.stopped { () => cleanUp() }
+ }
+ .receiveSignal { case (ctx, PostStop) =>
+ ctx.log.info("Requested to stop.")
+ cleanUp()
+ Behaviors.same
}
- }
+ private def cleanUp(): Unit = ???
}
diff --git a/src/main/scala/edu/ie3/osmogrid/lv/LvCoordinator.scala b/src/main/scala/edu/ie3/osmogrid/lv/LvCoordinator.scala
index 1889207e..af6096bf 100644
--- a/src/main/scala/edu/ie3/osmogrid/lv/LvCoordinator.scala
+++ b/src/main/scala/edu/ie3/osmogrid/lv/LvCoordinator.scala
@@ -6,28 +6,31 @@
package edu.ie3.osmogrid.lv
-import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
+import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import akka.actor.typed.scaladsl.{Behaviors, Routers}
import edu.ie3.datamodel.models.input.container.SubGridContainer
import edu.ie3.osmogrid.cfg.OsmoGridConfig
import edu.ie3.osmogrid.cfg.OsmoGridConfig.Generation.Lv
-import edu.ie3.osmogrid.guardian.OsmoGridGuardian.{
- OsmoGridGuardianEvent,
- RepLvGrids
-}
import edu.ie3.osmogrid.lv.LvGenerator
+import java.util.UUID
+
object LvCoordinator {
- sealed trait LvCoordinatorEvent
+ sealed trait Request
final case class ReqLvGrids(
cfg: OsmoGridConfig.Generation.Lv,
- replyTo: ActorRef[OsmoGridGuardianEvent]
- ) extends LvCoordinatorEvent
+ replyTo: ActorRef[Response]
+ ) extends Request
+
+ object Terminate extends Request
- def apply(): Behavior[LvCoordinatorEvent] = idle
+ sealed trait Response
+ final case class RepLvGrids(grids: Seq[SubGridContainer]) extends Response
- private def idle: Behavior[LvCoordinatorEvent] = Behaviors.receive {
- (ctx, msg) =>
+ def apply(): Behavior[Request] = idle
+
+ private def idle: Behavior[Request] = Behaviors
+ .receive[Request] { (ctx, msg) =>
msg match {
case ReqLvGrids(
Lv(
@@ -65,11 +68,18 @@ object LvCoordinator {
val lvRegionCoordinatorProxy =
ctx.spawn(lvRegionCoordinatorPool, "LvRegionCoordinatorPool")
- replyTo ! RepLvGrids(Vector.empty[SubGridContainer])
- Behaviors.stopped
+ replyTo ! RepLvGrids(Seq.empty[SubGridContainer])
+ Behaviors.stopped(cleanUp())
case unsupported =>
ctx.log.error(s"Received unsupported message: $unsupported")
- Behaviors.stopped
+ Behaviors.stopped(cleanUp())
}
- }
+ }
+ .receiveSignal { case (ctx, PostStop) =>
+ ctx.log.info("Got terminated by ActorSystem.")
+ cleanUp()
+ Behaviors.same
+ }
+
+ private def cleanUp(): () => Unit = ???
}
diff --git a/src/main/scala/edu/ie3/osmogrid/lv/LvGenerator.scala b/src/main/scala/edu/ie3/osmogrid/lv/LvGenerator.scala
index 56f3bb16..3d5d9abd 100644
--- a/src/main/scala/edu/ie3/osmogrid/lv/LvGenerator.scala
+++ b/src/main/scala/edu/ie3/osmogrid/lv/LvGenerator.scala
@@ -9,11 +9,11 @@ package edu.ie3.osmogrid.lv
import akka.actor.typed.scaladsl.Behaviors
object LvGenerator {
- sealed trait LvGeneratorEvent
+ sealed trait Request
- def apply(): Behaviors.Receive[LvGeneratorEvent] = idle
+ def apply(): Behaviors.Receive[Request] = idle
- private def idle: Behaviors.Receive[LvGeneratorEvent] = Behaviors.receive {
+ private def idle: Behaviors.Receive[Request] = Behaviors.receive {
case (ctx, unsupported) =>
ctx.log.warn(s"Received unsupported message '$unsupported'.")
Behaviors.stopped
diff --git a/src/main/scala/edu/ie3/osmogrid/lv/LvRegionCoordinator.scala b/src/main/scala/edu/ie3/osmogrid/lv/LvRegionCoordinator.scala
index 55da4bed..0494ab69 100644
--- a/src/main/scala/edu/ie3/osmogrid/lv/LvRegionCoordinator.scala
+++ b/src/main/scala/edu/ie3/osmogrid/lv/LvRegionCoordinator.scala
@@ -9,20 +9,11 @@ package edu.ie3.osmogrid.lv
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.ActorRef
-import edu.ie3.osmogrid.lv.LvGenerator.LvGeneratorEvent
object LvRegionCoordinator {
- sealed trait LvRegionCoordinatorEvent
+ sealed trait Request
def apply(
- lvGeneratorPool: ActorRef[LvGeneratorEvent]
- ): Behaviors.Receive[LvRegionCoordinatorEvent] = idle(lvGeneratorPool)
-
- private def idle(
- lvGeneratorPool: ActorRef[LvGeneratorEvent]
- ): Behaviors.Receive[LvRegionCoordinatorEvent] = Behaviors.receive {
- case (ctx, unsupported) =>
- ctx.log.warn(s"Received unsupported message '$unsupported'.")
- Behaviors.stopped
- }
+ lvGeneratorPool: ActorRef[LvGenerator.Request]
+ ): Behaviors.Receive[Request] = ???
}
diff --git a/src/main/scala/edu/ie3/osmogrid/main/RunOsmoGridStandalone.scala b/src/main/scala/edu/ie3/osmogrid/main/RunOsmoGridStandalone.scala
index 71b23f97..17dca710 100644
--- a/src/main/scala/edu/ie3/osmogrid/main/RunOsmoGridStandalone.scala
+++ b/src/main/scala/edu/ie3/osmogrid/main/RunOsmoGridStandalone.scala
@@ -8,7 +8,7 @@ package edu.ie3.osmogrid.main
import akka.actor.typed.ActorSystem
import edu.ie3.osmogrid.guardian.OsmoGridGuardian
-import edu.ie3.osmogrid.guardian.OsmoGridGuardian.{OsmoGridGuardianEvent, Run}
+import edu.ie3.osmogrid.guardian.OsmoGridGuardian.Run
import edu.ie3.osmogrid.cfg.{ArgsParser, ConfigFailFast, OsmoGridConfig}
object RunOsmoGridStandalone {
@@ -17,7 +17,7 @@ object RunOsmoGridStandalone {
val cfg: OsmoGridConfig = ArgsParser.prepare(args)
ConfigFailFast.check(cfg)
- val actorSystem: ActorSystem[OsmoGridGuardianEvent] =
+ val actorSystem: ActorSystem[OsmoGridGuardian.Request] =
ActorSystem(OsmoGridGuardian(), "OSMoGridGuardian")
actorSystem ! Run(cfg)
}
diff --git a/src/test/resources/testConfig.conf b/src/test/resources/testConfig.conf
index 7f5c378e..f265eaa1 100644
--- a/src/test/resources/testConfig.conf
+++ b/src/test/resources/testConfig.conf
@@ -1,5 +1,5 @@
input.osm.pbf.file = "input_file_path"
input.asset.file.directory = "asset_directory"
input.asset.file.hierarchic = false
-output.file.directory = "output_file_path"
+output.csv.directory = "output_file_path"
generation.lv.distinctHouseConnections = true
diff --git a/src/test/scala/edu/ie3/osmogrid/cfg/ConfigFailFastSpec.scala b/src/test/scala/edu/ie3/osmogrid/cfg/ConfigFailFastSpec.scala
index 2c26b19a..76141007 100644
--- a/src/test/scala/edu/ie3/osmogrid/cfg/ConfigFailFastSpec.scala
+++ b/src/test/scala/edu/ie3/osmogrid/cfg/ConfigFailFastSpec.scala
@@ -24,9 +24,12 @@ class ConfigFailFastSpec extends UnitSpec {
|generation.lv.distinctHouseConnections = true""".stripMargin
} match {
case Success(cfg) =>
- val exc =
- intercept[IllegalConfigException](ConfigFailFast.check(cfg))
- exc.msg shouldBe "You have to provide at least one input data type for open street map information!"
+ ConfigFailFast.check(cfg) match {
+ case Failure(exception) =>
+ exception.getMessage shouldBe "You have to provide at least one input data type for open street map information!"
+ case Success(_) =>
+ fail("Config check succeeded, but was meant to fail.")
+ }
case Failure(exception) =>
fail(s"Config generation failed with an exception: '$exception'")
}
@@ -41,9 +44,12 @@ class ConfigFailFastSpec extends UnitSpec {
|generation.lv.distinctHouseConnections = true""".stripMargin
} match {
case Success(cfg) =>
- val exc =
- intercept[IllegalConfigException](ConfigFailFast.check(cfg))
- exc.msg shouldBe "Pbf file may be set!"
+ ConfigFailFast.check(cfg) match {
+ case Failure(exception) =>
+ exception.getMessage shouldBe "Pbf file may be set!"
+ case Success(_) =>
+ fail("Config check succeeded, but was meant to fail.")
+ }
case Failure(exception) =>
fail(s"Config generation failed with an exception: '$exception'")
}
@@ -56,9 +62,12 @@ class ConfigFailFastSpec extends UnitSpec {
|generation.lv.distinctHouseConnections = true""".stripMargin
} match {
case Success(cfg) =>
- val exc =
- intercept[IllegalConfigException](ConfigFailFast.check(cfg))
- exc.msg shouldBe "You have to provide at least one input data type for asset information!"
+ ConfigFailFast.check(cfg) match {
+ case Failure(exception) =>
+ exception.getMessage shouldBe "You have to provide at least one input data type for asset information!"
+ case Success(_) =>
+ fail("Config check succeeded, but was meant to fail.")
+ }
case Failure(exception) =>
fail(s"Config generation failed with an exception: '$exception'")
}
@@ -73,9 +82,12 @@ class ConfigFailFastSpec extends UnitSpec {
|generation.lv.distinctHouseConnections = true""".stripMargin
} match {
case Success(cfg) =>
- val exc =
- intercept[IllegalConfigException](ConfigFailFast.check(cfg))
- exc.msg shouldBe "Asset input directory may be set!"
+ ConfigFailFast.check(cfg) match {
+ case Failure(exception) =>
+ exception.getMessage shouldBe "Asset input directory may be set!"
+ case Success(_) =>
+ fail("Config check succeeded, but was meant to fail.")
+ }
case Failure(exception) =>
fail(s"Config generation failed with an exception: '$exception'")
}
@@ -88,12 +100,15 @@ class ConfigFailFastSpec extends UnitSpec {
"""input.osm.pbf.file = "input_file_path"
|input.asset.file.directory = "asset_input_dir"
|input.asset.file.hierarchic = false
- |output.file.directory = "output_file_path"""".stripMargin
+ |output.csv.directory = "output_file_path"""".stripMargin
} match {
case Success(cfg) =>
- val exc =
- intercept[IllegalConfigException](ConfigFailFast.check(cfg))
- exc.msg shouldBe "At least one voltage level generation config has to be defined."
+ ConfigFailFast.check(cfg) match {
+ case Failure(exception) =>
+ exception.getMessage shouldBe "At least one voltage level generation config has to be defined."
+ case Success(_) =>
+ fail("Config check succeeded, but was meant to fail.")
+ }
case Failure(exception) =>
fail(s"Config generation failed with an exception: '$exception'")
}
@@ -106,15 +121,18 @@ class ConfigFailFastSpec extends UnitSpec {
"""input.osm.pbf.file = "input_file_path"
|input.asset.file.directory = "asset_input_dir"
|input.asset.file.hierarchic = false
- |output.file.directory = "output_file_path"
+ |output.csv.directory = "output_file_path"
|generation.lv.amountOfGridGenerators = 0
|generation.lv.amountOfRegionCoordinators = 5
|generation.lv.distinctHouseConnections = false""".stripMargin
} match {
case Success(cfg) =>
- val exc =
- intercept[IllegalConfigException](ConfigFailFast.check(cfg))
- exc.msg shouldBe "The amount of lv grid generation actors needs to be at least 1 (provided: 0)."
+ ConfigFailFast.check(cfg) match {
+ case Failure(exception) =>
+ exception.getMessage shouldBe "The amount of lv grid generation actors needs to be at least 1 (provided: 0)."
+ case Success(_) =>
+ fail("Config check succeeded, but was meant to fail.")
+ }
case Failure(exception) =>
fail(s"Config generation failed with an exception: '$exception'")
}
@@ -125,15 +143,18 @@ class ConfigFailFastSpec extends UnitSpec {
"""input.osm.pbf.file = "input_file_path"
|input.asset.file.directory = "asset_input_dir"
|input.asset.file.hierarchic = false
- |output.file.directory = "output_file_path"
+ |output.csv.directory = "output_file_path"
|generation.lv.amountOfGridGenerators = -42
|generation.lv.amountOfRegionCoordinators = 5
|generation.lv.distinctHouseConnections = false""".stripMargin
} match {
case Success(cfg) =>
- val exc =
- intercept[IllegalConfigException](ConfigFailFast.check(cfg))
- exc.msg shouldBe "The amount of lv grid generation actors needs to be at least 1 (provided: -42)."
+ ConfigFailFast.check(cfg) match {
+ case Failure(exception) =>
+ exception.getMessage shouldBe "The amount of lv grid generation actors needs to be at least 1 (provided: -42)."
+ case Success(_) =>
+ fail("Config check succeeded, but was meant to fail.")
+ }
case Failure(exception) =>
fail(s"Config generation failed with an exception: '$exception'")
}
@@ -144,15 +165,18 @@ class ConfigFailFastSpec extends UnitSpec {
"""input.osm.pbf.file = "input_file_path"
|input.asset.file.directory = "asset_input_dir"
|input.asset.file.hierarchic = false
- |output.file.directory = "output_file_path"
+ |output.csv.directory = "output_file_path"
|generation.lv.amountOfGridGenerators = 10
|generation.lv.amountOfRegionCoordinators = 0
|generation.lv.distinctHouseConnections = false""".stripMargin
} match {
case Success(cfg) =>
- val exc =
- intercept[IllegalConfigException](ConfigFailFast.check(cfg))
- exc.msg shouldBe "The amount of lv region coordination actors needs to be at least 1 (provided: 0)."
+ ConfigFailFast.check(cfg) match {
+ case Failure(exception) =>
+ exception.getMessage shouldBe "The amount of lv region coordination actors needs to be at least 1 (provided: 0)."
+ case Success(_) =>
+ fail("Config check succeeded, but was meant to fail.")
+ }
case Failure(exception) =>
fail(s"Config generation failed with an exception: '$exception'")
}
@@ -163,15 +187,18 @@ class ConfigFailFastSpec extends UnitSpec {
"""input.osm.pbf.file = "input_file_path"
|input.asset.file.directory = "asset_input_dir"
|input.asset.file.hierarchic = false
- |output.file.directory = "output_file_path"
+ |output.csv.directory = "output_file_path"
|generation.lv.amountOfGridGenerators = 10
|generation.lv.amountOfRegionCoordinators = -42
|generation.lv.distinctHouseConnections = false""".stripMargin
} match {
case Success(cfg) =>
- val exc =
- intercept[IllegalConfigException](ConfigFailFast.check(cfg))
- exc.msg shouldBe "The amount of lv region coordination actors needs to be at least 1 (provided: -42)."
+ ConfigFailFast.check(cfg) match {
+ case Failure(exception) =>
+ exception.getMessage shouldBe "The amount of lv region coordination actors needs to be at least 1 (provided: -42)."
+ case Success(_) =>
+ fail("Config check succeeded, but was meant to fail.")
+ }
case Failure(exception) =>
fail(s"Config generation failed with an exception: '$exception'")
}
@@ -184,13 +211,10 @@ class ConfigFailFastSpec extends UnitSpec {
"""input.osm.pbf.file = "input_file_path"
|input.asset.file.directory = "asset_input_dir"
|input.asset.file.hierarchic = false
- |output.file.directory = "output_file_path"
+ |output.csv.directory = "output_file_path"
|generation.lv.distinctHouseConnections = true""".stripMargin
} match {
- case Success(cfg) =>
- noException shouldBe thrownBy {
- ConfigFailFast.check(cfg)
- }
+ case Success(cfg) => ConfigFailFast.check(cfg) shouldBe Success(cfg)
case Failure(exception) =>
fail(s"Config generation failed with an exception: '$exception'")
}
diff --git a/src/test/scala/edu/ie3/osmogrid/cfg/OsmoGridConfigFactory.scala b/src/test/scala/edu/ie3/osmogrid/cfg/OsmoGridConfigFactory.scala
index 78616642..6f1d437d 100644
--- a/src/test/scala/edu/ie3/osmogrid/cfg/OsmoGridConfigFactory.scala
+++ b/src/test/scala/edu/ie3/osmogrid/cfg/OsmoGridConfigFactory.scala
@@ -12,6 +12,11 @@ import java.io.File
import scala.util.Try
object OsmoGridConfigFactory {
+ lazy val defaultTestConfig: OsmoGridConfig =
+ OsmoGridConfig(
+ ConfigFactory.parseFile(new File("src/test/resources/testConfig.conf"))
+ )
+
def parse(config: String): Try[OsmoGridConfig] = Try {
OsmoGridConfig(
ConfigFactory
diff --git a/src/test/scala/edu/ie3/osmogrid/guardian/OsmoGridGuardianSpec.scala b/src/test/scala/edu/ie3/osmogrid/guardian/OsmoGridGuardianSpec.scala
new file mode 100644
index 00000000..fc4b57aa
--- /dev/null
+++ b/src/test/scala/edu/ie3/osmogrid/guardian/OsmoGridGuardianSpec.scala
@@ -0,0 +1,80 @@
+/*
+ * © 2022. TU Dortmund University,
+ * Institute of Energy Systems, Energy Efficiency and Energy Economics,
+ * Research group Distribution grid planning and operation
+ */
+
+package edu.ie3.osmogrid.guardian
+
+import akka.actor.testkit.typed.CapturedLogEvent
+import akka.actor.testkit.typed.Effect.{MessageAdapter, Spawned}
+import akka.actor.testkit.typed.scaladsl.BehaviorTestKit
+import akka.actor.typed.{ActorRef, Behavior}
+import edu.ie3.osmogrid.cfg.OsmoGridConfigFactory
+import edu.ie3.osmogrid.guardian.OsmoGridGuardian.{
+ GuardianData,
+ RunGuardianDied
+}
+import edu.ie3.osmogrid.guardian.run.RunGuardian
+import edu.ie3.osmogrid.guardian.run.Request
+import edu.ie3.osmogrid.io.output.ResultListener.ResultEvent
+import edu.ie3.test.common.UnitSpec
+import org.slf4j.event.Level
+
+import java.util.UUID
+
+class OsmoGridGuardianSpec extends UnitSpec {
+ "Having an overall OsmoGridGuardian" when {
+ "being idle" should {
+ val guardianData = OsmoGridGuardian.GuardianData(Seq.empty[UUID])
+ val config = OsmoGridConfigFactory.defaultTestConfig
+ val additionalListeners = Seq.empty[ActorRef[ResultEvent]]
+ val runId = UUID.randomUUID()
+
+ val idleTestKit = BehaviorTestKit(OsmoGridGuardian.idle(guardianData))
+
+ "spawn a new RunGuardian on request" in {
+ idleTestKit.run(
+ OsmoGridGuardian.Run(config, additionalListeners, runId)
+ )
+
+ /* Check if the right child is spawned */
+ idleTestKit.expectEffectPF {
+ case Spawned(childBehav: Behavior[Request], name, props) =>
+ name shouldBe s"RunGuardian_$runId"
+ case Spawned(childBehav, _, _) =>
+ fail(s"Spawned a child with wrong behavior '$childBehav'.")
+ }
+ }
+
+ "report a dead RunGuardian" in {
+ idleTestKit.run(RunGuardianDied(runId))
+
+ idleTestKit.logEntries() should contain only CapturedLogEvent(
+ Level.INFO,
+ s"Run $runId terminated."
+ )
+ }
+ }
+
+ "checking for state data" should {
+ "bring up empty data" in {
+ GuardianData.empty shouldBe GuardianData(Seq.empty[UUID])
+ }
+
+ val run0 = UUID.randomUUID()
+ val run1 = UUID.randomUUID()
+ "properly add a new run to existing data" in {
+ GuardianData(Seq(run0))
+ .append(run1)
+ .runs should contain theSameElementsAs Seq(run0, run1)
+ }
+
+ "properly remove a run from existing data" in {
+ GuardianData(Seq(run0, run1))
+ .remove(run0)
+ .runs should contain theSameElementsAs Seq(run1)
+ }
+ }
+ }
+}
diff --git a/src/test/scala/edu/ie3/osmogrid/guardian/run/RunGuardianSpec.scala b/src/test/scala/edu/ie3/osmogrid/guardian/run/RunGuardianSpec.scala
new file mode 100644
index 00000000..d163be14
--- /dev/null
+++ b/src/test/scala/edu/ie3/osmogrid/guardian/run/RunGuardianSpec.scala
@@ -0,0 +1,247 @@
+/*
+ * © 2022. TU Dortmund University,
+ * Institute of Energy Systems, Energy Efficiency and Energy Economics,
+ * Research group Distribution grid planning and operation
+ */
+
+package edu.ie3.osmogrid.guardian.run
+
+import akka.actor.testkit.typed.CapturedLogEvent
+import akka.actor.testkit.typed.Effect.{MessageAdapter, Spawned, WatchedWith}
+import akka.actor.testkit.typed.scaladsl.{
+ BehaviorTestKit,
+ ScalaTestWithActorTestKit
+}
+import akka.actor.typed.{ActorRef, Behavior}
+import edu.ie3.datamodel.models.input.container.SubGridContainer
+import edu.ie3.osmogrid.cfg.OsmoGridConfigFactory
+import edu.ie3.osmogrid.exception.IllegalConfigException
+import edu.ie3.osmogrid.io.input.InputDataProvider
+import edu.ie3.osmogrid.io.output.ResultListener
+import edu.ie3.osmogrid.io.output.ResultListener.{GridResult, ResultEvent}
+import edu.ie3.osmogrid.lv.LvCoordinator
+import edu.ie3.osmogrid.lv.LvCoordinator.ReqLvGrids
+import edu.ie3.test.common.{GridSupport, UnitSpec}
+import org.slf4j.event.Level
+
+import java.util.UUID
+
+class RunGuardianSpec extends ScalaTestWithActorTestKit with UnitSpec {
+ "Having a run guardian" when {
+ val runId = UUID.randomUUID()
+ val validConfig = OsmoGridConfigFactory.defaultTestConfig
+
+ "being idle state" should {
+ val idleTestKit = BehaviorTestKit(
+ RunGuardian(validConfig, Seq.empty[ActorRef[ResultEvent]], runId)
+ )
+
+ "log an error if faced to not supported request" in {
+ idleTestKit.run(ResultEventListenerDied)
+
+ idleTestKit.logEntries() should contain only CapturedLogEvent(
+ Level.ERROR,
+ s"Received a message, that I don't understand during idle phase of run $runId.\n\tMessage: $ResultEventListenerDied"
+ )
+ }
+
+ "log an error, if initiation of a run is impossible" in {
+ val maliciousConfig = OsmoGridConfigFactory
+ .parseWithoutFallback("")
+ .getOrElse(fail("Unable to parse malicious config"))
+ val maliciousIdleTestKit = BehaviorTestKit(
+ RunGuardian(maliciousConfig, Seq.empty[ActorRef[ResultEvent]], runId)
+ )
+
+ maliciousIdleTestKit.run(Run)
+ maliciousIdleTestKit.logEntries() should contain only CapturedLogEvent(
+ Level.ERROR,
+ s"Unable to start run $runId.",
+ Some(
+ IllegalConfigException(
+ "You have to provide at least one input data type for asset information!"
+ )
+ ),
+ None
+ )
+ }
+
+ "spawns children, if input is fine" in {
+ idleTestKit.run(Run)
+
+ /* Two message adapters are registered */
+ idleTestKit
+ .expectEffectType[MessageAdapter[LvCoordinator.Response, Request]]
+ idleTestKit.expectEffectType[MessageAdapter[ResultEvent, Request]]
+
+ /* Check if I/O actors and LvCoordinator are spawned and watched correctly */
+ idleTestKit.expectEffectPF {
+ case Spawned(_: Behavior[InputDataProvider.Request], name, props) =>
+ name shouldBe s"InputDataProvider_$runId"
+ }
+ idleTestKit
+ .expectEffectType[WatchedWith[InputDataProvider.Request, Watch]]
+ idleTestKit.expectEffectPF {
+ case Spawned(_: Behavior[ResultEvent], name, props) =>
+ name shouldBe s"PersistenceResultListener_$runId"
+ }
+ idleTestKit.expectEffectType[WatchedWith[ResultEvent, Watch]]
+ idleTestKit.expectEffectPF {
+ case Spawned(_: Behavior[LvCoordinator.Request], name, props) =>
+ name shouldBe s"LvCoordinator_$runId"
+ }
+ idleTestKit.expectEffectType[WatchedWith[LvCoordinator.Request, Watch]]
+
+ /* Check for child messages */
+ idleTestKit
+ .childInbox[LvCoordinator.Request](s"LvCoordinator_$runId")
+ .receiveAll()
+ .exists {
+ case ReqLvGrids(cfg, replyTo) =>
+ validConfig.generation.lv.contains(cfg)
+ case _ => false
+ } shouldBe true
+ }
+ }
+
+ "being in running state" should {
+ val running = PrivateMethod[Behavior[Request]](Symbol("running"))
+
+ /* Test probes */
+ val lvCoordinatorAdapter =
+ testKit.createTestProbe[LvCoordinator.Response]()
+ val resultListenerAdapter =
+ testKit.createTestProbe[ResultListener.Response]()
+ val inputDataProvider =
+ testKit.createTestProbe[InputDataProvider.Request]()
+ val resultListener = testKit.createTestProbe[ResultEvent]()
+ val lvCoordinator = testKit.createTestProbe[LvCoordinator.Request]()
+
+ /* State data */
+ val runGuardianData = RunGuardianData(
+ runId,
+ validConfig,
+ Seq.empty[ActorRef[ResultEvent]],
+ MessageAdapters(lvCoordinatorAdapter.ref, resultListenerAdapter.ref)
+ )
+ val childReferences = ChildReferences(
+ inputDataProvider.ref,
+ Some(resultListener.ref),
+ Seq.empty,
+ Some(lvCoordinator.ref)
+ )
+
+ val runningTestKit = BehaviorTestKit(
+ RunGuardian invokePrivate running(runGuardianData, childReferences)
+ )
+
+ "log an error if faced to not supported request" in {
+ runningTestKit.run(Run)
+
+ runningTestKit.logEntries() should contain only CapturedLogEvent(
+ Level.ERROR,
+ s"Received a message, that I don't understand during active run $runId.\n\tMessage: $Run"
+ )
+ }
+
+ "handles an incoming result" in new GridSupport {
+ runningTestKit.run(
+ MessageAdapters.WrappedLvCoordinatorResponse(
+ LvCoordinator.RepLvGrids(Seq(mockSubGrid(1)))
+ )
+ )
+
+ /* Event is logged */
+ runningTestKit.logEntries() should contain allOf (CapturedLogEvent(
+ Level.INFO,
+ "All lv grids successfully generated."
+ ), CapturedLogEvent(
+ Level.DEBUG,
+ "No further generation steps intended. Hand over results to result handler."
+ ))
+
+ /* Result is forwarded to listener */
+ resultListener.expectMessageType[GridResult]
+ }
+
+ "initiate coordinated shutdown, if somebody unexpectedly dies" in {
+ runningTestKit.run(LvCoordinatorDied)
+
+ /* Event is logged */
+ runningTestKit.logEntries() should contain(
+ CapturedLogEvent(
+ Level.WARN,
+ s"Lv coordinator for run $runId unexpectedly died. Start coordinated shut down phase for this run."
+ )
+ )
+ /* All children are sent a termination request */
+ lvCoordinator.expectMessage(LvCoordinator.Terminate)
+ inputDataProvider.expectMessage(InputDataProvider.Terminate)
+ resultListener.expectMessage(ResultListener.Terminate)
+ }
+ }
+
+ "being in stopping state without a LvCoordinator" should {
+ val stopping = PrivateMethod[Behavior[Request]](Symbol("stopping"))
+
+ val stoppingData = StoppingData(
+ runId,
+ inputDataProviderTerminated = false,
+ resultListenerTerminated = false,
+ lvCoordinatorTerminated = None
+ )
+
+ val stoppingTestKit = BehaviorTestKit(
+ RunGuardian invokePrivate stopping(stoppingData)
+ )
+
+ "log an error if faced to not supported request" in {
+ stoppingTestKit.run(Run)
+
+ stoppingTestKit.logEntries() should contain only CapturedLogEvent(
+ Level.ERROR,
+ s"Received a message, that I don't understand during coordinated shutdown phase of run $runId.\n\tMessage: $Run"
+ )
+ }
+
+ "stop itself only once all awaited termination messages have been received" in {
+ stoppingTestKit.run(ResultEventListenerDied)
+
+ stoppingTestKit.isAlive shouldBe true
+
+ stoppingTestKit.run(InputDataProviderDied)
+
+ stoppingTestKit.isAlive shouldBe false
+ }
+ }
+
+ "being in stopping state with a LvCoordinator" should {
+ val stopping = PrivateMethod[Behavior[Request]](Symbol("stopping"))
+
+ val stoppingData = StoppingData(
+ runId,
+ inputDataProviderTerminated = false,
+ resultListenerTerminated = false,
+ lvCoordinatorTerminated = Some(false)
+ )
+
+ val stoppingTestKit = BehaviorTestKit(
+ RunGuardian invokePrivate stopping(stoppingData)
+ )
+
+ "stop itself only once all awaited termination messages have been received" in {
+ stoppingTestKit.run(InputDataProviderDied)
+
+ stoppingTestKit.isAlive shouldBe true
+
+ stoppingTestKit.run(ResultEventListenerDied)
+
+ stoppingTestKit.isAlive shouldBe true
+
+ stoppingTestKit.run(LvCoordinatorDied)
+
+ stoppingTestKit.isAlive shouldBe false
+ }
+ }
+ }
+}
diff --git a/src/test/scala/edu/ie3/osmogrid/guardian/run/SubGridHandlingSpec.scala b/src/test/scala/edu/ie3/osmogrid/guardian/run/SubGridHandlingSpec.scala
new file mode 100644
index 00000000..e6e8b3d7
--- /dev/null
+++ b/src/test/scala/edu/ie3/osmogrid/guardian/run/SubGridHandlingSpec.scala
@@ -0,0 +1,125 @@
+/*
+ * © 2022. TU Dortmund University,
+ * Institute of Energy Systems, Energy Efficiency and Energy Economics,
+ * Research group Distribution grid planning and operation
+ */
+
+package edu.ie3.osmogrid.guardian.run
+
+import akka.actor.testkit.typed.Effect.MessageAdapter
+import akka.actor.testkit.typed.scaladsl.{ActorTestKit, TestProbe}
+import edu.ie3.datamodel.models.input.container.SubGridContainer
+import edu.ie3.osmogrid.cfg.{OsmoGridConfig, OsmoGridConfigFactory}
+import edu.ie3.osmogrid.guardian.run.{RunGuardian, SubGridHandling}
+import edu.ie3.osmogrid.io.input.InputDataProvider
+import edu.ie3.osmogrid.io.output.ResultListener
+import edu.ie3.osmogrid.lv.LvCoordinator
+import edu.ie3.test.common.{GridSupport, UnitSpec}
+import org.mockito.Mockito.when
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.PrivateMethodTester.PrivateMethod
+import org.scalatestplus.mockito.MockitoSugar.mock
+import org.slf4j.{Logger, LoggerFactory}
+
+import java.util.UUID
+import scala.jdk.CollectionConverters.*
+
+class SubGridHandlingSpec
+ extends UnitSpec
+ with GridSupport
+ with BeforeAndAfterAll {
+ private val testKit: ActorTestKit = ActorTestKit()
+
+ "Supporting sub grid handling" when {
+ "assigning sub grid numbers to a single sub grid container" should {
+ val assignSubnetNumber =
+ PrivateMethod[SubGridContainer](Symbol("assignSubnetNumber"))
+
+ "return the same container" in {
+ /* ATTENTION: This is a dummy test until the concrete logic is implemented */
+ val subGridContainer = mock[SubGridContainer]
+
+ val actual =
+ SubGridHandling invokePrivate assignSubnetNumber(subGridContainer, 42)
+
+ actual shouldBe subGridContainer
+ }
+ }
+
+ "assigning sub grid numbers to a series of sub grid containers" should {
+ val assignSubnetNumbers =
+ PrivateMethod[Seq[SubGridContainer]](Symbol("assignSubnetNumbers"))
+
+ "return the same containers" in {
+ /* ATTENTION: This is a dummy test until the concrete logic is implemented */
+ val containers = Range(1, 10).map(_ => mock[SubGridContainer])
+
+ val actual =
+ SubGridHandling invokePrivate assignSubnetNumbers(containers)
+
+ actual should contain theSameElementsAs containers
+ }
+ }
+
+ "handling incoming results" when {
+ implicit val log: Logger =
+ LoggerFactory.getLogger("SubGridHandlingTestLogger")
+
+ val inputDataProvider =
+ testKit.createTestProbe[InputDataProvider.Request]("InputDataProvider")
+ val lvCoordinatorAdapter =
+ testKit.createTestProbe[LvCoordinator.Response]("LvCoordinatorAdapter")
+ val resultListener =
+ testKit.createTestProbe[ResultListener.ResultEvent]("ResultListener")
+ val resultListenerAdapter =
+ testKit.createTestProbe[ResultListener.Response](
+ "ResultListenerAdapter"
+ )
+ val additionalResultListener =
+ testKit.createTestProbe[ResultListener.ResultEvent](
+ "AdditionalResultListener"
+ )
+
+ val runId = UUID.randomUUID()
+ val grids = Range(1, 10).map(mockSubGrid)
+ val messageAdapters = new MessageAdapters(
+ lvCoordinatorAdapter.ref,
+ resultListenerAdapter.ref
+ )
+ val cfg = OsmoGridConfigFactory.parse {
+ """
+ |input.osm.file.pbf=test.pbf
+ |input.asset.file.directory=assets/
+ |output.csv.directory=output/
+ |generation.lv.distinctHouseConnections=true""".stripMargin
+ }.get
+
+ "having an active run" should {
+ "inform the right parties about correct information" in new SubGridHandling {
+ handleLvResults(
+ grids,
+ cfg.generation,
+ Seq(resultListener.ref, additionalResultListener.ref),
+ messageAdapters
+ )
+
+ resultListener.receiveMessage() match {
+ case ResultListener.GridResult(grid, _) =>
+ grid.getGridName shouldBe "DummyGrid"
+ grid.getRawGrid.getNodes.size() shouldBe 0
+ }
+ additionalResultListener.receiveMessage() match {
+ case ResultListener.GridResult(grid, _) =>
+ grid.getGridName shouldBe "DummyGrid"
+ grid.getRawGrid.getNodes.size() shouldBe 0
+ }
+ inputDataProvider.expectNoMessage()
+ lvCoordinatorAdapter.expectNoMessage()
+ resultListenerAdapter.expectNoMessage()
+ }
+ }
+ }
+ }
+
+ override protected def afterAll(): Unit = testKit.shutdownTestKit()
+}
diff --git a/src/test/scala/edu/ie3/test/common/GridSupport.scala b/src/test/scala/edu/ie3/test/common/GridSupport.scala
new file mode 100644
index 00000000..00317988
--- /dev/null
+++ b/src/test/scala/edu/ie3/test/common/GridSupport.scala
@@ -0,0 +1,87 @@
+/*
+ * © 2022. TU Dortmund University,
+ * Institute of Energy Systems, Energy Efficiency and Energy Economics,
+ * Research group Distribution grid planning and operation
+ */
+
+package edu.ie3.test.common
+
+import edu.ie3.datamodel.models.input.{MeasurementUnitInput, NodeInput}
+import edu.ie3.datamodel.models.input.connector.{
+ LineInput,
+ SwitchInput,
+ Transformer2WInput,
+ Transformer3WInput
+}
+import edu.ie3.datamodel.models.input.container.{
+ GraphicElements,
+ RawGridElements,
+ SubGridContainer,
+ SystemParticipants
+}
+import edu.ie3.datamodel.models.input.graphics.{
+ LineGraphicInput,
+ NodeGraphicInput
+}
+import edu.ie3.datamodel.models.input.system.{
+ BmInput,
+ ChpInput,
+ EvInput,
+ EvcsInput,
+ FixedFeedInInput,
+ HpInput,
+ LoadInput,
+ PvInput,
+ StorageInput,
+ WecInput
+}
+import org.mockito.Mockito.when
+import org.mockito.stubbing.OngoingStubbing
+import org.scalatestplus.mockito.MockitoSugar.mock
+
+import scala.jdk.CollectionConverters._
+
+trait GridSupport {
+ protected def mockSubGrid(subnetNo: Int): SubGridContainer = {
+ val mockedRawGrid = mock[RawGridElements]
+ when(mockedRawGrid.getNodes).thenReturn(Set.empty[NodeInput].asJava)
+ when(mockedRawGrid.getLines).thenReturn(Set.empty[LineInput].asJava)
+ when(mockedRawGrid.getTransformer2Ws).thenReturn(
+ Set.empty[Transformer2WInput].asJava
+ )
+ when(mockedRawGrid.getTransformer3Ws).thenReturn(
+ Set.empty[Transformer3WInput].asJava
+ )
+ when(mockedRawGrid.getSwitches).thenReturn(Set.empty[SwitchInput].asJava)
+ when(mockedRawGrid.getMeasurementUnits).thenReturn(
+ Set.empty[MeasurementUnitInput].asJava
+ )
+
+ val mockedParticipants = new SystemParticipants(
+ Set.empty[BmInput].asJava,
+ Set.empty[ChpInput].asJava,
+ Set.empty[EvcsInput].asJava,
+ Set.empty[EvInput].asJava,
+ Set.empty[FixedFeedInInput].asJava,
+ Set.empty[HpInput].asJava,
+ Set.empty[LoadInput].asJava,
+ Set.empty[PvInput].asJava,
+ Set.empty[StorageInput].asJava,
+ Set.empty[WecInput].asJava
+ )
+
+ val mockedGraphics = new GraphicElements(
+ Set.empty[NodeGraphicInput].asJava,
+ Set.empty[LineGraphicInput].asJava
+ )
+
+ val mockedSubGrid = mock[SubGridContainer]
+ when(mockedSubGrid.getGridName).thenReturn(s"DummyGrid")
+ when(mockedSubGrid.getSubnet).thenReturn(subnetNo)
+ when(mockedSubGrid.getRawGrid).thenReturn(mockedRawGrid)
+ when(mockedSubGrid.getSystemParticipants).thenReturn(mockedParticipants)
+ when(mockedSubGrid.getGraphics).thenReturn(mockedGraphics)
+
+ mockedSubGrid
+ }
+}