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 + } +}