From e0f92f753ea57ccdcc58bafca6ca9c0c6586a9d0 Mon Sep 17 00:00:00 2001 From: Piotr Rudnicki Date: Thu, 16 Jan 2025 12:57:52 +0100 Subject: [PATCH 01/11] [Nu-7393] Allow copying alert message when it is of an error type (#7464) Co-authored-by: Piotr Rudnicki --- designer/client/package-lock.json | 27 +++++++ designer/client/package.json | 1 + .../components/notifications/Notification.tsx | 14 +++- .../components/notifications/copyTooltip.tsx | 78 +++++++++++++++++++ 4 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 designer/client/src/components/notifications/copyTooltip.tsx diff --git a/designer/client/package-lock.json b/designer/client/package-lock.json index 97f763865a8..9054fa1411b 100644 --- a/designer/client/package-lock.json +++ b/designer/client/package-lock.json @@ -26,6 +26,7 @@ "@touk/window-manager": "1.9.1", "ace-builds": "1.34.2", "axios": "1.7.5", + "copy-to-clipboard": "3.3.1", "d3-transition": "3.0.1", "d3-zoom": "3.0.0", "dagre": "0.8.5", @@ -10357,6 +10358,14 @@ "node": ">=0.10.0" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", + "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/copy-webpack-plugin": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", @@ -26188,6 +26197,11 @@ "node": ">= 0.10" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -36096,6 +36110,14 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, + "copy-to-clipboard": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", + "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, "copy-webpack-plugin": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", @@ -47973,6 +47995,11 @@ "through2": "^2.0.3" } }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/designer/client/package.json b/designer/client/package.json index 0330944e1be..fb37fe878d4 100644 --- a/designer/client/package.json +++ b/designer/client/package.json @@ -19,6 +19,7 @@ "@touk/window-manager": "1.9.1", "ace-builds": "1.34.2", "axios": "1.7.5", + "copy-to-clipboard": "3.3.1", "d3-transition": "3.0.1", "d3-zoom": "3.0.0", "dagre": "0.8.5", diff --git a/designer/client/src/components/notifications/Notification.tsx b/designer/client/src/components/notifications/Notification.tsx index b9a4d9e5ce4..988536b5e1a 100644 --- a/designer/client/src/components/notifications/Notification.tsx +++ b/designer/client/src/components/notifications/Notification.tsx @@ -1,6 +1,8 @@ import React, { ReactElement } from "react"; import { Alert, AlertColor } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; +import { CopyTooltip } from "./copyTooltip"; +import { useTranslation } from "react-i18next"; interface Props { icon: ReactElement; @@ -10,9 +12,19 @@ interface Props { } export default function Notification({ icon, message, type }: Props): JSX.Element { - return ( + const { t } = useTranslation(); + + const alertContent = ( }> {message} ); + + return type === "error" ? ( + + {alertContent} + + ) : ( + alertContent + ); } diff --git a/designer/client/src/components/notifications/copyTooltip.tsx b/designer/client/src/components/notifications/copyTooltip.tsx new file mode 100644 index 00000000000..f06532ed38d --- /dev/null +++ b/designer/client/src/components/notifications/copyTooltip.tsx @@ -0,0 +1,78 @@ +import React, { PropsWithChildren, useEffect, useState } from "react"; +import copy from "copy-to-clipboard"; +import { Button, Tooltip } from "@mui/material"; +import { CopyAll, Done } from "@mui/icons-material"; + +export function useCopyClipboard(): [boolean, (value: string) => void] { + const [isCopied, setIsCopied] = useState(); + const [text, setText] = useState(); + + useEffect(() => { + if (isCopied) { + const id = setTimeout(() => { + setIsCopied(false); + }, 1000); + + return () => { + clearTimeout(id); + }; + } + }, [isCopied, text]); + + return [ + isCopied, + (value: string) => { + setText(value); + setIsCopied(copy(value)); + }, + ]; +} + +export function CopyTooltip({ + children, + text, + title, +}: PropsWithChildren<{ + text: string; + title: string; +}>): JSX.Element { + const [isCopied, copy] = useCopyClipboard(); + return ( + : } + onClick={(e) => { + copy(text); + e.stopPropagation(); + }} + > + {title} + + } + componentsProps={{ + popper: { + sx: { + opacity: 0.8, + }, + }, + tooltip: { + sx: { + bgcolor: (t) => (t.palette.mode === "dark" ? t.palette.common.white : t.palette.common.black), + color: (t) => (t.palette.mode === "dark" ? t.palette.common.black : t.palette.common.white), + }, + }, + arrow: { sx: { color: (t) => (t.palette.mode === "dark" ? t.palette.common.white : t.palette.common.black) } }, + }} + placement="bottom-start" + arrow + > + {children} + + ); +} From 9ed7326d0841f6b0d980c742d0693c7f6124a9dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82abek?= Date: Thu, 16 Jan 2025 14:08:53 +0100 Subject: [PATCH 02/11] [NU-1974] Initialize model classloaders only once (#7447) initialize model classloaders only once --- .../ui/factory/NussknackerAppFactory.scala | 19 ++- .../ModelClassLoaderProvider.scala | 69 ++++++++++ .../LocalProcessingTypeDataLoader.scala | 9 +- .../loader/ProcessingTypeDataLoader.scala | 2 + ...sConfigBasedProcessingTypeDataLoader.scala | 18 ++- .../server/AkkaHttpBasedRouteProvider.scala | 12 +- .../test/base/it/NuResourcesTest.scala | 17 ++- .../test/mock/MockDeploymentManager.scala | 9 +- .../api/AppApiHttpServiceBusinessSpec.scala | 120 +++++++++++++----- .../ui/config/ConfigurationTest.scala | 13 +- .../ProcessingTypeDataProviderSpec.scala | 3 +- .../ScenarioParametersServiceTest.scala | 6 + ...amingDeploymentManagerProviderHelper.scala | 9 +- .../FlinkStreamingDeploymentManagerSpec.scala | 4 +- .../touk/nussknacker/engine/ModelData.scala | 7 +- 15 files changed, 255 insertions(+), 62 deletions(-) create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/ModelClassLoaderProvider.scala diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/factory/NussknackerAppFactory.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/factory/NussknackerAppFactory.scala index a340b48ba6c..44787d0477d 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/factory/NussknackerAppFactory.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/factory/NussknackerAppFactory.scala @@ -7,9 +7,10 @@ import cats.effect.{IO, Resource} import com.typesafe.scalalogging.LazyLogging import io.dropwizard.metrics5.MetricRegistry import io.dropwizard.metrics5.jmx.JmxReporter -import pl.touk.nussknacker.engine.ConfigWithUnresolvedVersion +import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap import pl.touk.nussknacker.engine.util.loader.ScalaServiceLoader import pl.touk.nussknacker.engine.util.{JavaClassVersionChecker, SLF4JBridgeHandlerRegistrar} +import pl.touk.nussknacker.engine.{ConfigWithUnresolvedVersion, ProcessingTypeConfig} import pl.touk.nussknacker.ui.config.{DesignerConfig, DesignerConfigLoader} import pl.touk.nussknacker.ui.configloader.{ProcessingTypeConfigsLoader, ProcessingTypeConfigsLoaderFactory} import pl.touk.nussknacker.ui.db.DbRef @@ -18,6 +19,7 @@ import pl.touk.nussknacker.ui.process.processingtype.loader.{ ProcessingTypeDataLoader, ProcessingTypesConfigBasedProcessingTypeDataLoader } +import pl.touk.nussknacker.ui.process.processingtype.{ModelClassLoaderDependencies, ModelClassLoaderProvider} import pl.touk.nussknacker.ui.server.{AkkaHttpBasedRouteProvider, NussknackerHttpServer} import pl.touk.nussknacker.ui.util.{ActorSystemBasedExecutionContextWithIORuntime, IOToFutureSttpBackendConverter} import sttp.client3.SttpBackend @@ -40,6 +42,9 @@ class NussknackerAppFactory( designerConfig, ioSttpBackend )(executionContextWithIORuntime.ioRuntime) + modelClassLoaderProvider = createModelClassLoaderProvider( + designerConfig.processingTypeConfigs.configByProcessingType + ) processingTypeDataLoader = createProcessingTypeDataLoader(processingTypeConfigsLoader) materializer = Materializer(system) _ <- Resource.eval(IO(JavaClassVersionChecker.check())) @@ -54,7 +59,8 @@ class NussknackerAppFactory( IOToFutureSttpBackendConverter.convert(ioSttpBackend)(executionContextWithIORuntime), processingTypeDataLoader, feStatisticsRepository, - clock + clock, + modelClassLoaderProvider )( system, materializer, @@ -116,6 +122,15 @@ class NussknackerAppFactory( ) } + private def createModelClassLoaderProvider( + processingTypeConfigs: Map[String, ProcessingTypeConfig] + ): ModelClassLoaderProvider = { + val defaultWorkingDirOpt = None + ModelClassLoaderProvider( + processingTypeConfigs.mapValuesNow(c => ModelClassLoaderDependencies(c.classPath, defaultWorkingDirOpt)) + ) + } + } object NussknackerAppFactory { diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/ModelClassLoaderProvider.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/ModelClassLoaderProvider.scala new file mode 100644 index 00000000000..b3404eb0ac1 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/ModelClassLoaderProvider.scala @@ -0,0 +1,69 @@ +package pl.touk.nussknacker.ui.process.processingtype + +import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap +import pl.touk.nussknacker.engine.util.loader.ModelClassLoader + +import java.nio.file.Path + +final case class ModelClassLoaderDependencies(classpath: List[String], workingDirectoryOpt: Option[Path]) { + + def show(): String = { + val workingDirectoryReadable = workingDirectoryOpt match { + case Some(value) => value.toString + case None => "None (default)" + } + s"classpath: ${classpath.mkString(", ")}, workingDirectoryOpt: $workingDirectoryReadable" + } + +} + +class ModelClassLoaderProvider private ( + processingTypeClassLoaders: Map[String, (ModelClassLoader, ModelClassLoaderDependencies)] +) { + + def forProcessingTypeUnsafe(processingTypeName: String): ModelClassLoader = { + processingTypeClassLoaders + .getOrElse( + processingTypeName, + throw new IllegalArgumentException( + s"Unknown ProcessingType: $processingTypeName, known ProcessingTypes are: ${processingTypeName.mkString(", ")}" + ) + ) + ._1 + } + + def validateReloadConsistency( + dependenciesFromReload: Map[String, ModelClassLoaderDependencies] + ): Unit = { + if (dependenciesFromReload.keySet != processingTypeClassLoaders.keySet) { + throw new IllegalStateException( + s"""Processing types cannot be added, removed, or renamed during processing type reload. + |Reloaded processing types: [${dependenciesFromReload.keySet.toList.sorted.mkString(", ")}] + |Current processing types: [${processingTypeClassLoaders.keySet.toList.sorted.mkString(", ")}] + |If you need to modify this, please restart the application with desired config.""".stripMargin + ) + } + dependenciesFromReload.foreach { case (processingType, reloadedConfig) => + val currentConfig = processingTypeClassLoaders.mapValuesNow(_._2)(processingType) + if (reloadedConfig != currentConfig) { + throw new IllegalStateException( + s"Error during processing types reload. Model ClassLoader dependencies such as classpath cannot be modified during reload. " + + s"For processing type [$processingType], reloaded ClassLoader dependencies: [${reloadedConfig.show()}] " + + s"do not match current dependencies: [${currentConfig.show()}]" + ) + } + } + } + +} + +object ModelClassLoaderProvider { + + def apply(processingTypeConfig: Map[String, ModelClassLoaderDependencies]): ModelClassLoaderProvider = { + val processingTypesClassloaders = processingTypeConfig.map { case (name, deps) => + name -> (ModelClassLoader(deps.classpath, deps.workingDirectoryOpt) -> deps) + } + new ModelClassLoaderProvider(processingTypesClassloaders) + } + +} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/LocalProcessingTypeDataLoader.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/LocalProcessingTypeDataLoader.scala index 2fae13dc5c2..c5676f87cd3 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/LocalProcessingTypeDataLoader.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/LocalProcessingTypeDataLoader.scala @@ -7,7 +7,11 @@ import pl.touk.nussknacker.engine.api.process.ProcessingType import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap import pl.touk.nussknacker.ui.process.processingtype.loader.ProcessingTypeDataLoader.toValueWithRestriction import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataState -import pl.touk.nussknacker.ui.process.processingtype.{CombinedProcessingTypeData, ProcessingTypeData} +import pl.touk.nussknacker.ui.process.processingtype.{ + CombinedProcessingTypeData, + ModelClassLoaderProvider, + ProcessingTypeData +} class LocalProcessingTypeDataLoader( modelData: Map[ProcessingType, (String, ModelData)], @@ -16,7 +20,8 @@ class LocalProcessingTypeDataLoader( override def loadProcessingTypeData( getModelDependencies: ProcessingType => ModelDependencies, - getDeploymentManagerDependencies: ProcessingType => DeploymentManagerDependencies + getDeploymentManagerDependencies: ProcessingType => DeploymentManagerDependencies, + modelClassLoaderProvider: ModelClassLoaderProvider ): IO[ProcessingTypeDataState[ProcessingTypeData, CombinedProcessingTypeData]] = IO { val processingTypes = modelData.map { case (processingType, (category, model)) => val deploymentManagerDependencies = getDeploymentManagerDependencies(processingType) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypeDataLoader.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypeDataLoader.scala index b2b526fc02f..28fa2bb20bc 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypeDataLoader.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypeDataLoader.scala @@ -6,6 +6,7 @@ import pl.touk.nussknacker.engine.{DeploymentManagerDependencies, ModelDependenc import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataState import pl.touk.nussknacker.ui.process.processingtype.{ CombinedProcessingTypeData, + ModelClassLoaderProvider, ProcessingTypeData, ValueWithRestriction } @@ -15,6 +16,7 @@ trait ProcessingTypeDataLoader { def loadProcessingTypeData( getModelDependencies: ProcessingType => ModelDependencies, getDeploymentManagerDependencies: ProcessingType => DeploymentManagerDependencies, + modelClassLoaderProvider: ModelClassLoaderProvider ): IO[ProcessingTypeDataState[ProcessingTypeData, CombinedProcessingTypeData]] } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypesConfigBasedProcessingTypeDataLoader.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypesConfigBasedProcessingTypeDataLoader.scala index de70b5c0ba2..c1d31b5e5c2 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypesConfigBasedProcessingTypeDataLoader.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypesConfigBasedProcessingTypeDataLoader.scala @@ -18,16 +18,20 @@ class ProcessingTypesConfigBasedProcessingTypeDataLoader(processingTypeConfigsLo override def loadProcessingTypeData( getModelDependencies: ProcessingType => ModelDependencies, getDeploymentManagerDependencies: ProcessingType => DeploymentManagerDependencies, + modelClassLoaderProvider: ModelClassLoaderProvider ): IO[ProcessingTypeDataState[ProcessingTypeData, CombinedProcessingTypeData]] = { processingTypeConfigsLoader .loadProcessingTypeConfigs() - .map(createProcessingTypeData(_, getModelDependencies, getDeploymentManagerDependencies)) + .map( + createProcessingTypeData(_, getModelDependencies, getDeploymentManagerDependencies, modelClassLoaderProvider) + ) } private def createProcessingTypeData( processingTypesConfig: ProcessingTypeConfigs, getModelDependencies: ProcessingType => ModelDependencies, - getDeploymentManagerDependencies: ProcessingType => DeploymentManagerDependencies + getDeploymentManagerDependencies: ProcessingType => DeploymentManagerDependencies, + modelClassLoaderProvider: ModelClassLoaderProvider ): ProcessingTypeDataState[ProcessingTypeData, CombinedProcessingTypeData] = { // This step with splitting DeploymentManagerProvider loading for all processing types // and after that creating ProcessingTypeData is done because of the deduplication of deployments @@ -41,15 +45,23 @@ class ProcessingTypesConfigBasedProcessingTypeDataLoader(processingTypeConfigsLo ) (processingTypeConfig, provider, nameInputData) } + modelClassLoaderProvider.validateReloadConsistency(providerWithNameInputData.map { case (processingType, data) => + processingType -> ModelClassLoaderDependencies( + classpath = data._1.classPath, + workingDirectoryOpt = getModelDependencies(processingType).workingDirectoryOpt + ) + }) + val engineSetupNames = ScenarioParametersDeterminer.determineEngineSetupNames(providerWithNameInputData.mapValuesNow(_._3)) val processingTypesData = providerWithNameInputData .map { case (processingType, (processingTypeConfig, deploymentManagerProvider, _)) => logger.debug(s"Creating Processing Type: $processingType with config: $processingTypeConfig") val modelDependencies = getModelDependencies(processingType) + val modelClassLoader = modelClassLoaderProvider.forProcessingTypeUnsafe(processingType) val processingTypeData = ProcessingTypeData.createProcessingTypeData( processingType, - ModelData(processingTypeConfig, modelDependencies), + ModelData(processingTypeConfig, modelDependencies, modelClassLoader), deploymentManagerProvider, getDeploymentManagerDependencies(processingType), engineSetupNames(processingType), diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala index ec0da71149b..4faadc146f0 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala @@ -65,7 +65,7 @@ import pl.touk.nussknacker.ui.process.newdeployment.synchronize.{ DeploymentsStatusesSynchronizer } import pl.touk.nussknacker.ui.process.newdeployment.{DeploymentRepository, DeploymentService} -import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeData +import pl.touk.nussknacker.ui.process.processingtype.{ModelClassLoaderProvider, ProcessingTypeData} import pl.touk.nussknacker.ui.process.processingtype.loader.ProcessingTypeDataLoader import pl.touk.nussknacker.ui.process.processingtype.provider.ReloadableProcessingTypeDataProvider import pl.touk.nussknacker.ui.process.repository._ @@ -109,7 +109,8 @@ class AkkaHttpBasedRouteProvider( sttpBackend: SttpBackend[Future, Any], processingTypeDataLoader: ProcessingTypeDataLoader, feStatisticsRepository: FEStatisticsRepository[Future], - designerClock: Clock + designerClock: Clock, + modelClassLoaderProvider: ModelClassLoaderProvider )( implicit system: ActorSystem, materializer: Materializer, @@ -140,7 +141,8 @@ class AkkaHttpBasedRouteProvider( dbioRunner, sttpBackend, featureTogglesConfig, - globalNotificationRepository + globalNotificationRepository, + modelClassLoaderProvider ) deploymentsStatusesSynchronizer = new DeploymentsStatusesSynchronizer( @@ -716,7 +718,8 @@ class AkkaHttpBasedRouteProvider( dbioActionRunner: DBIOActionRunner, sttpBackend: SttpBackend[Future, Any], featureTogglesConfig: FeatureTogglesConfig, - globalNotificationRepository: InMemoryTimeseriesRepository[Notification] + globalNotificationRepository: InMemoryTimeseriesRepository[Notification], + modelClassLoaderProvider: ModelClassLoaderProvider ): Resource[IO, ReloadableProcessingTypeDataProvider] = { Resource .make( @@ -735,6 +738,7 @@ class AkkaHttpBasedRouteProvider( sttpBackend, _ ), + modelClassLoaderProvider ) val loadAndNotifyIO = laodProcessingTypeDataIO .map { state => diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/NuResourcesTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/NuResourcesTest.scala index c2674f278f4..11600e5b9ed 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/NuResourcesTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/NuResourcesTest.scala @@ -124,10 +124,18 @@ trait NuResourcesTest protected val processingTypeConfig: ProcessingTypeConfig = ProcessingTypeConfig.read(ConfigWithScalaVersion.StreamingProcessTypeConfig) - protected val deploymentManagerProvider: DeploymentManagerProvider = - new MockManagerProvider(deploymentManager) + protected val deploymentManagerProvider: DeploymentManagerProvider = new MockManagerProvider(deploymentManager) - private val modelData = ModelData(processingTypeConfig, modelDependencies) + private val modelClassLoaderProvider = ModelClassLoaderProvider( + Map(Streaming.stringify -> ModelClassLoaderDependencies(processingTypeConfig.classPath, None)) + ) + + private val modelData = + ModelData( + processingTypeConfig, + modelDependencies, + modelClassLoaderProvider.forProcessingTypeUnsafe(Streaming.stringify) + ) protected val testProcessingTypeDataProvider: ProcessingTypeDataProvider[ProcessingTypeData, _] = mapProcessingTypeDataProvider( @@ -151,7 +159,8 @@ trait NuResourcesTest new ProcessingTypesConfigBasedProcessingTypeDataLoader(() => IO.pure(designerConfig.processingTypeConfigs)) .loadProcessingTypeData( _ => modelDependencies, - _ => deploymentManagerDependencies + _ => deploymentManagerDependencies, + modelClassLoaderProvider ) .unsafeRunSync() ) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/mock/MockDeploymentManager.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/mock/MockDeploymentManager.scala index e7ff26f3da9..4cf5180d00e 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/mock/MockDeploymentManager.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/mock/MockDeploymentManager.scala @@ -7,11 +7,6 @@ import cats.data.ValidatedNel import com.google.common.collect.LinkedHashMultimap import com.typesafe.config.Config import pl.touk.nussknacker.engine._ -import pl.touk.nussknacker.engine.api.definition.{ - NotBlankParameterValidator, - NotNullParameterValidator, - StringParameterEditor -} import pl.touk.nussknacker.engine.api.deployment._ import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus import pl.touk.nussknacker.engine.api.process.ProcessName @@ -19,6 +14,7 @@ import pl.touk.nussknacker.engine.api.{ProcessVersion, StreamMetaData} import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.engine.deployment._ import pl.touk.nussknacker.engine.management.{FlinkDeploymentManager, FlinkStreamingDeploymentManagerProvider} +import pl.touk.nussknacker.engine.util.loader.ModelClassLoader import pl.touk.nussknacker.test.config.ConfigWithScalaVersion import pl.touk.nussknacker.test.utils.domain.TestFactory import shapeless.syntax.typeable.typeableOps @@ -47,7 +43,8 @@ class MockDeploymentManager( ) extends FlinkDeploymentManager( ModelData( ProcessingTypeConfig.read(ConfigWithScalaVersion.StreamingProcessTypeConfig), - TestFactory.modelDependencies + TestFactory.modelDependencies, + ModelClassLoader(ProcessingTypeConfig.read(ConfigWithScalaVersion.StreamingProcessTypeConfig).classPath, None) ), DeploymentManagerDependencies( deployedScenariosProvider, diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/AppApiHttpServiceBusinessSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/AppApiHttpServiceBusinessSpec.scala index 1f90931f0a6..d9f0245c76e 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/AppApiHttpServiceBusinessSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/AppApiHttpServiceBusinessSpec.scala @@ -2,6 +2,7 @@ package pl.touk.nussknacker.ui.api import com.typesafe.config.ConfigValueFactory.fromAnyRef import com.typesafe.config.{Config, ConfigFactory} +import com.typesafe.scalalogging.LazyLogging import io.restassured.RestAssured._ import io.restassured.module.scala.RestAssuredSupport.AddThenToResponse import org.hamcrest.Matchers._ @@ -29,9 +30,10 @@ class AppApiHttpServiceBusinessSpec with WithBusinessCaseRestAssuredUsersExtensions with NuRestAssureMatchers with RestAssuredVerboseLoggingIfValidationFails - with PatientScalaFutures { + with PatientScalaFutures + with LazyLogging { - private var simulateChangeInApplicationConfig: Boolean = false + private var simulatedChangeInApplicationConfig: Option[Config] = None "The app health check endpoint should" - { "return simple designer health check (with no scenario statuses check)" in { @@ -279,11 +281,11 @@ class AppApiHttpServiceBusinessSpec "The processing type data reload endpoint should" - { "reload processing types-related model data when" - { "'scenarioTypes' configuration is changed" in { - val componentNamesBeforeReload = fetchSortedComponentNames() + val componentNamesBeforeReload = fetchComponentGroupNamesWithOccurencesCount() given() .applicationState { - simulateChangeInApplicationConfig = true + simulatedChangeInApplicationConfig = Some(additionalProcessingTypeCustomization) } .when() .basicAuthAdmin() @@ -291,27 +293,86 @@ class AppApiHttpServiceBusinessSpec .Then() .statusCode(204) - val componentNamesAfterReload = fetchSortedComponentNames() + val componentNamesAfterReload = fetchComponentGroupNamesWithOccurencesCount() componentNamesAfterReload shouldNot be(componentNamesBeforeReload) - componentNamesAfterReload.length should be > (componentNamesBeforeReload.length) + componentNamesAfterReload("someComponentGroup") shouldBe 2 + } + } + "return error when" - { + "scenario type is added" in { + given() + .applicationState { + simulatedChangeInApplicationConfig = Some( + ConfigFactory.parseString( + s""" + |scenarioTypes { + | streaming2 { + | deploymentConfig { + | type: "development-tests" + | } + | modelConfig { + | classPath: [] + | } + | category: "Default" + | } + |} + |""".stripMargin + ) + ) + } + .when() + .basicAuthAdmin() + .post(s"$nuDesignerHttpAddress/api/app/processingtype/reload") + .Then() + .statusCode(500) + .body( + startsWith("Processing types cannot be added, removed, or renamed during processing type reload.") + ) + } + "classpath of a model is changed" in { + given() + .applicationState { + simulatedChangeInApplicationConfig = Some( + ConfigFactory.parseString( + s""" + |scenarioTypes { + | streaming { + | modelConfig { + | classPath: ["changed.jar"] + | } + | } + |} + |""".stripMargin + ) + ) + } + .when() + .basicAuthAdmin() + .post(s"$nuDesignerHttpAddress/api/app/processingtype/reload") + .Then() + .statusCode(500) + .body( + startsWith( + "Error during processing types reload. Model ClassLoader dependencies such as classpath cannot be modified during reload." + ) + ) } } } override def beforeEach(): Unit = { super.beforeEach() - if (simulateChangeInApplicationConfig) { - simulateChangeInApplicationConfig = false + if (simulatedChangeInApplicationConfig.isDefined) { + simulatedChangeInApplicationConfig = None forceReloadProcessingTypes() } } override def designerConfig: Config = { - if (simulateChangeInApplicationConfig) { - additionalProcessingTypeCustomization.withFallback(originDesignerConfig) - } else { - originDesignerConfig + simulatedChangeInApplicationConfig match { + case Some(customization) => customization.withFallback(originDesignerConfig) + case None => originDesignerConfig } } @@ -332,34 +393,33 @@ class AppApiHttpServiceBusinessSpec ConfigFactory.parseString( s""" |scenarioTypes { - | streaming3 { - | deploymentConfig { - | type: "mockable" - | id: "3" - | engineSetupName: "Mockable" - | } - | modelConfig: { - | classPath: [ - | "engine/flink/management/dev-model/target/scala-"$${scala.major.version}"/devModel.jar", - | "engine/flink/executor/target/scala-"$${scala.major.version}"/flinkExecutor.jar" - | ] - | } - | category: "Category1" - | } + | streaming { + | modelConfig { + | componentsUiConfig { + | sendCommunication { + | componentGroup: "someComponentGroup" + | } + | } + | } + | } |} |""".stripMargin ) } - private def fetchSortedComponentNames(): List[String] = { - given() + private def fetchComponentGroupNamesWithOccurencesCount(): Map[String, Int] = { + val body = given() .when() .basicAuthAdmin() .get(s"$nuDesignerHttpAddress/api/components") .Then() .statusCode(200) - .extractList("name") - .sorted + body + .extractList("componentGroupName") + .groupBy(identity) + .view + .map { case (name, occurences) => name -> occurences.length } + .toMap } private def forceReloadProcessingTypes(): Unit = { diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/config/ConfigurationTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/config/ConfigurationTest.scala index ac1e56a3885..afcb0dfe60d 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/config/ConfigurationTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/config/ConfigurationTest.scala @@ -3,6 +3,7 @@ package pl.touk.nussknacker.ui.config import cats.effect.unsafe.implicits.global import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import pl.touk.nussknacker.engine.util.loader.ModelClassLoader import pl.touk.nussknacker.engine.{ModelData, ProcessingTypeConfig} import pl.touk.nussknacker.test.config.ConfigWithScalaVersion import pl.touk.nussknacker.test.utils.domain.TestFactory @@ -17,10 +18,14 @@ class ConfigurationTest extends AnyFunSuite with Matchers { // warning: can't be val - uses ConfigFactory.load which breaks "should preserve config overrides" test private def globalConfig = ConfigWithScalaVersion.TestsConfig - private def modelData: ModelData = ModelData( - ProcessingTypeConfig.read(ConfigWithScalaVersion.StreamingProcessTypeConfig), - TestFactory.modelDependencies - ) + private def modelData: ModelData = { + val config = ProcessingTypeConfig.read(ConfigWithScalaVersion.StreamingProcessTypeConfig) + ModelData( + config, + TestFactory.modelDependencies, + ModelClassLoader(config.classPath, None) + ) + } private lazy val modelDataConfig = modelData.modelConfig diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeDataProviderSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeDataProviderSpec.scala index f4a8e88d82d..64aa228791e 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeDataProviderSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeDataProviderSpec.scala @@ -57,7 +57,8 @@ class ProcessingTypeDataProviderSpec extends AnyFunSuite with Matchers { loader .loadProcessingTypeData( _ => modelDependencies, - _ => TestFactory.deploymentManagerDependencies + _ => TestFactory.deploymentManagerDependencies, + ModelClassLoaderProvider(allProcessingTypes.map(_ -> ModelClassLoaderDependencies(List.empty, None)).toMap) ) .unsafeRunSync() } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ScenarioParametersServiceTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ScenarioParametersServiceTest.scala index fe079bdeec1..792c9141fa1 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ScenarioParametersServiceTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ScenarioParametersServiceTest.scala @@ -14,6 +14,7 @@ import pl.touk.nussknacker.engine.api.component.{ComponentProvider, DesignerWide import pl.touk.nussknacker.engine.api.process.ProcessingType import pl.touk.nussknacker.engine.definition.component.Components.ComponentDefinitionExtractionMode import pl.touk.nussknacker.engine.deployment.EngineSetupName +import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap import pl.touk.nussknacker.restmodel.scenariodetails.ScenarioParameters import pl.touk.nussknacker.security.Permission import pl.touk.nussknacker.test.ValidatedValuesDetailedMessage @@ -296,6 +297,11 @@ class ScenarioParametersServiceTest ComponentDefinitionExtractionMode.FinalDefinition ), _ => TestFactory.deploymentManagerDependencies, + ModelClassLoaderProvider( + designerConfig.processingTypeConfigs.configByProcessingType.mapValuesNow(conf => + ModelClassLoaderDependencies(conf.classPath, None) + ) + ) ) .unsafeRunSync() val parametersService = processingTypeData.getCombined().parametersService diff --git a/engine/flink/management/src/it/scala/pl/touk/nussknacker/engine/management/streaming/FlinkStreamingDeploymentManagerProviderHelper.scala b/engine/flink/management/src/it/scala/pl/touk/nussknacker/engine/management/streaming/FlinkStreamingDeploymentManagerProviderHelper.scala index 931b35d5794..aaa2604efe2 100644 --- a/engine/flink/management/src/it/scala/pl/touk/nussknacker/engine/management/streaming/FlinkStreamingDeploymentManagerProviderHelper.scala +++ b/engine/flink/management/src/it/scala/pl/touk/nussknacker/engine/management/streaming/FlinkStreamingDeploymentManagerProviderHelper.scala @@ -12,6 +12,7 @@ import pl.touk.nussknacker.engine.api.deployment.{ } import pl.touk.nussknacker.engine.definition.component.Components.ComponentDefinitionExtractionMode import pl.touk.nussknacker.engine.management.FlinkStreamingDeploymentManagerProvider +import pl.touk.nussknacker.engine.util.loader.ModelClassLoader import pl.touk.nussknacker.engine.{ ConfigWithUnresolvedVersion, DeploymentManagerDependencies, @@ -26,7 +27,8 @@ object FlinkStreamingDeploymentManagerProviderHelper { def createDeploymentManager( processingTypeConfig: ConfigWithUnresolvedVersion, ): DeploymentManager = { - val typeConfig = ProcessingTypeConfig.read(processingTypeConfig) + val typeConfig = ProcessingTypeConfig.read(processingTypeConfig) + val modelClassLoader = ModelClassLoader(typeConfig.classPath, None) val modelData = ModelData( processingTypeConfig = typeConfig, ModelDependencies( @@ -34,8 +36,9 @@ object FlinkStreamingDeploymentManagerProviderHelper { determineDesignerWideId = id => DesignerWideComponentId(id.toString), workingDirectoryOpt = None, _ => true, - ComponentDefinitionExtractionMode.FinalDefinition - ) + ComponentDefinitionExtractionMode.FinalDefinition, + ), + modelClassLoader ) val actorSystem = ActorSystem("FlinkStreamingDeploymentManagerProviderHelper") val backend = AsyncHttpClientFutureBackend.usingConfig(new DefaultAsyncHttpClientConfig.Builder().build()) diff --git a/engine/flink/management/src/it/scala/pl/touk/nussknacker/engine/management/streaming/FlinkStreamingDeploymentManagerSpec.scala b/engine/flink/management/src/it/scala/pl/touk/nussknacker/engine/management/streaming/FlinkStreamingDeploymentManagerSpec.scala index 5872b331cb7..0916e833329 100644 --- a/engine/flink/management/src/it/scala/pl/touk/nussknacker/engine/management/streaming/FlinkStreamingDeploymentManagerSpec.scala +++ b/engine/flink/management/src/it/scala/pl/touk/nussknacker/engine/management/streaming/FlinkStreamingDeploymentManagerSpec.scala @@ -18,6 +18,7 @@ import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId} import pl.touk.nussknacker.engine.definition.component.Components.ComponentDefinitionExtractionMode import pl.touk.nussknacker.engine.deployment.DeploymentData +import pl.touk.nussknacker.engine.util.loader.ModelClassLoader import java.net.URI import java.nio.file.{Files, Paths} @@ -271,7 +272,8 @@ class FlinkStreamingDeploymentManagerSpec extends AnyFunSuite with Matchers with workingDirectoryOpt = None, _ => true, ComponentDefinitionExtractionMode.FinalDefinition - ) + ), + ModelClassLoader(processingTypeConfig.classPath, None) ) val definition = modelData.modelDefinition definition.components.components.map(_.id) should contain(ComponentId(ComponentType.Service, "accountService")) diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/ModelData.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/ModelData.scala index 43f00f94bd9..d87f651729f 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/ModelData.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/ModelData.scala @@ -40,8 +40,11 @@ object ModelData extends LazyLogging { Map[DesignerWideComponentId, ComponentAdditionalConfig] ) => ModelDefinition - def apply(processingTypeConfig: ProcessingTypeConfig, dependencies: ModelDependencies): ModelData = { - val modelClassLoader = ModelClassLoader(processingTypeConfig.classPath, dependencies.workingDirectoryOpt) + def apply( + processingTypeConfig: ProcessingTypeConfig, + dependencies: ModelDependencies, + modelClassLoader: ModelClassLoader + ): ModelData = { ClassLoaderModelData( _.resolveInputConfigDuringExecution(processingTypeConfig.modelConfig, modelClassLoader.classLoader), modelClassLoader, From fd82abe6bc38ed8f61d805fb0dd9e8e0ae1d67f2 Mon Sep 17 00:00:00 2001 From: mgoworko <37329559+mgoworko@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:50:09 +0100 Subject: [PATCH 03/11] PeriodicDM as decorator for any DM with data stored in the main Nu DB (#7364) --- build.sbt | 48 +- designer/client/src/types/node.ts | 2 +- .../api/deployment/DeploymentManager.scala | 28 +- ...CachingProcessStateDeploymentManager.scala | 4 +- .../model/DeploymentWithRuntimeParams.scala | 12 + .../scheduler/model/ScheduleProperty.scala | 16 + .../model/ScheduledDeploymentDetails.scala | 28 + .../model/ScheduledProcessDetails.scala | 9 + .../AdditionalDeploymentDataProvider.scala | 9 + .../services}/ProcessConfigEnricher.scala | 26 +- .../SchedulePropertyExtractorFactory.scala | 13 + .../ScheduledExecutionPerformer.scala | 31 ++ .../services/ScheduledProcessListener.scala | 60 +++ .../testing/DeploymentManagerStub.scala | 2 + ...ingProcessStateDeploymentManagerSpec.scala | 9 +- .../common/V1_103__drop_buildinfo.sql | 0 .../migration/common/V1_105__add_indexes.sql | 0 ...etries_to_periodic_process_deployments.sql | 0 ...9__add_action_id_to_periodic_processes.sql | 0 .../V1_101__create_batch_periodic_tables.sql | 0 .../V1_102__add_program_args_and_jar_id.sql | 0 .../hsql/V1_104__multiple_schedules.sql | 0 .../hsql/V1_106__rename_model_config.sql | 0 .../hsql/V1_108__add_processing_type.sql | 0 .../V1_101__create_batch_periodic_tables.sql | 0 .../V1_102__add_program_args_and_jar_id.sql | 0 .../postgres/V1_104__multiple_schedules.sql | 0 .../postgres/V1_106__rename_model_config.sql | 0 .../postgres/V1_108__add_processing_type.sql | 0 .../web/static/assets/states/scheduled.svg | 0 .../static/assets/states/wait-reschedule.svg | 0 ...dicDeploymentManagerTablesDefinition.scala | 153 ++++++ ...061__PeriodicDeploymentManagerTables.scala | 8 + ...061__PeriodicDeploymentManagerTables.scala | 8 + .../PeriodicProcessDeploymentsTable.scala | 13 +- .../ui/db/entity/PeriodicProcessesTable.scala | 160 ++++++ ...aultAdditionalDeploymentDataProvider.scala | 18 + .../process}/periodic/DeploymentActor.scala | 26 +- .../periodic/PeriodicDeploymentManager.scala | 89 ++-- .../PeriodicDeploymentManagerDecorator.scala | 117 ++++ .../periodic/PeriodicProcessException.scala | 2 +- .../periodic/PeriodicProcessService.scala | 346 +++++++----- ...eriodicProcessStateDefinitionManager.scala | 13 +- .../periodic/PeriodicStateStatus.scala | 2 +- .../periodic/RescheduleFinishedActor.scala | 4 +- .../process/periodic/ScheduleProperty.scala | 4 +- .../process/periodic/SchedulingConfig.scala | 13 +- .../ui/process}/periodic/Utils.scala | 2 +- .../cron/CronParameterValidator.scala | 24 +- .../cron/CronSchedulePropertyExtractor.scala | 34 ++ .../legacy/db/LegacyDbInitializer.scala | 4 +- ...riodicProcessDeploymentsTableFactory.scala | 76 +++ .../LegacyPeriodicProcessesRepository.scala | 498 ++++++++++++++++++ .../LegacyPeriodicProcessesTableFactory.scala | 20 +- .../periodic/model/PeriodicProcess.scala | 18 + .../model/PeriodicProcessDeployment.scala | 71 +++ .../periodic/model/SchedulesState.scala | 46 +- .../utils}/DeterministicUUIDFromLong.scala | 2 +- .../SchedulePropertyExtractorUtils.scala | 27 +- .../InvalidDeploymentManagerStub.scala | 2 + .../processingtype/ProcessingTypeData.scala | 55 +- .../LocalProcessingTypeDataLoader.scala | 8 +- .../loader/ProcessingTypeDataLoader.scala | 6 +- ...sConfigBasedProcessingTypeDataLoader.scala | 31 +- .../DBFetchingProcessRepository.scala | 27 +- .../FetchingProcessRepository.scala | 11 +- .../PeriodicProcessesRepository.scala | 314 +++++------ .../repository/ScenarioActionRepository.scala | 68 ++- .../server/AkkaHttpBasedRouteProvider.scala | 5 +- ...tionsAndCommentsToScenarioActivities.scala | 1 - .../nussknacker/test/base/db/DbTesting.scala | 2 + .../test/base/it/NuResourcesTest.scala | 5 +- .../test/mock/MockDeploymentManager.scala | 3 +- .../mock/MockFetchingProcessRepository.scala | 19 +- .../DefaultComponentServiceSpec.scala | 2 + .../ProcessStateDefinitionServiceSpec.scala | 2 + ...eriodicProcessServiceIntegrationTest.scala | 264 +++++++--- .../PeriodicProcessesFetchingTest.scala | 42 +- .../CronSchedulePropertyExtractorTest.scala | 14 +- .../flink}/CronSchedulePropertyTest.scala | 6 +- .../periodic/flink}/DeploymentActorTest.scala | 27 +- .../flink}/DeploymentManagerStub.scala | 6 +- .../periodic/flink}/FlinkClientStub.scala | 2 +- .../PeriodicDeploymentManagerTest.scala | 35 +- .../flink}/PeriodicProcessDeploymentGen.scala | 14 +- .../periodic/flink/PeriodicProcessGen.scala | 39 ++ .../flink}/PeriodicProcessServiceTest.scala | 127 +++-- ...dicProcessStateDefinitionManagerTest.scala | 17 +- .../flink}/RescheduleFinishedActorTest.scala | 3 +- .../ScheduledExecutionPerformerStub.scala | 49 ++ .../ScheduledExecutionPerformerTest.scala | 53 +- .../process/periodic/flink}/UtilsSpec.scala | 5 +- .../db/InMemPeriodicProcessesRepository.scala | 297 ++++++++--- .../ProcessingTypeDataProviderSpec.scala | 4 +- .../ScenarioParametersServiceTest.scala | 4 +- docs/Changelog.md | 3 + docs/MigrationGuide.md | 34 ++ ...DevelopmentDeploymentManagerProvider.scala | 2 + .../MockableDeploymentManagerProvider.scala | 2 + .../aggregate/AggregatesSpec.scala | 126 +++-- .../transformer/aggregate/aggregates.scala | 11 +- .../aggregate/median/MedianHelper.scala | 8 +- engine/flink/management/periodic/README.md | 34 -- ...ssknacker.engine.DeploymentManagerProvider | 1 - ...ne.api.definition.CustomParameterValidator | 1 - ...inkPeriodicDeploymentManagerProvider.scala | 79 --- .../management/periodic/JarManager.scala | 23 - .../SchedulePropertyExtractorFactory.scala | 7 - .../periodic/flink/FlinkJarManager.scala | 118 ----- .../model/DeploymentWithJarData.scala | 25 - .../periodic/model/PeriodicProcess.scala | 21 - .../model/PeriodicProcessDeployment.scala | 52 -- .../AdditionalDeploymentDataProvider.scala | 27 - .../service/PeriodicProcessListener.scala | 61 --- .../management/periodic/JarManagerStub.scala | 38 -- .../periodic/PeriodicProcessGen.scala | 38 -- ...amingDeploymentManagerProviderHelper.scala | 6 +- .../engine/management/FlinkRestManager.scala | 29 +- .../FlinkScheduledExecutionPerformer.scala | 140 +++++ ...nkStreamingDeploymentManagerProvider.scala | 9 - .../management/FlinkRestManagerSpec.scala | 8 +- .../embedded/EmbeddedDeploymentManager.scala | 2 + ...esponseEmbeddedDeploymentManagerTest.scala | 13 +- .../k8s/manager/K8sDeploymentManager.scala | 1 + .../BaseK8sDeploymentManagerTest.scala | 2 +- .../K8sDeploymentManagerOnMocksTest.scala | 4 +- .../src/universal/conf/dev-application.conf | 13 +- 127 files changed, 3065 insertions(+), 1537 deletions(-) create mode 100644 designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/DeploymentWithRuntimeParams.scala create mode 100644 designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/ScheduleProperty.scala create mode 100644 designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/ScheduledDeploymentDetails.scala create mode 100644 designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/ScheduledProcessDetails.scala create mode 100644 designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/AdditionalDeploymentDataProvider.scala rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/service => designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services}/ProcessConfigEnricher.scala (77%) create mode 100644 designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/SchedulePropertyExtractorFactory.scala create mode 100644 designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/ScheduledExecutionPerformer.scala create mode 100644 designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/ScheduledProcessListener.scala rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/common/V1_103__drop_buildinfo.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/common/V1_105__add_indexes.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/common/V1_107__add_retry_at_and_retries_to_periodic_process_deployments.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/common/V1_109__add_action_id_to_periodic_processes.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/hsql/V1_101__create_batch_periodic_tables.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/hsql/V1_102__add_program_args_and_jar_id.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/hsql/V1_104__multiple_schedules.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/hsql/V1_106__rename_model_config.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/hsql/V1_108__add_processing_type.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/postgres/V1_101__create_batch_periodic_tables.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/postgres/V1_102__add_program_args_and_jar_id.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/postgres/V1_104__multiple_schedules.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/postgres/V1_106__rename_model_config.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/db/batch_periodic/migration/postgres/V1_108__add_processing_type.sql (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/web/static/assets/states/scheduled.svg (100%) rename {engine/flink/management/periodic => designer/server}/src/main/resources/web/static/assets/states/wait-reschedule.svg (100%) create mode 100644 designer/server/src/main/scala/db/migration/V1_061__PeriodicDeploymentManagerTablesDefinition.scala create mode 100644 designer/server/src/main/scala/db/migration/hsql/V1_061__PeriodicDeploymentManagerTables.scala create mode 100644 designer/server/src/main/scala/db/migration/postgres/V1_061__PeriodicDeploymentManagerTables.scala rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db => designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity}/PeriodicProcessDeploymentsTable.scala (84%) create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/PeriodicProcessesTable.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/DefaultAdditionalDeploymentDataProvider.scala rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management => designer/server/src/main/scala/pl/touk/nussknacker/ui/process}/periodic/DeploymentActor.scala (70%) rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management => designer/server/src/main/scala/pl/touk/nussknacker/ui/process}/periodic/PeriodicDeploymentManager.scala (81%) create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicDeploymentManagerDecorator.scala rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management => designer/server/src/main/scala/pl/touk/nussknacker/ui/process}/periodic/PeriodicProcessException.scala (74%) rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management => designer/server/src/main/scala/pl/touk/nussknacker/ui/process}/periodic/PeriodicProcessService.scala (74%) rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management => designer/server/src/main/scala/pl/touk/nussknacker/ui/process}/periodic/PeriodicProcessStateDefinitionManager.scala (80%) rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management => designer/server/src/main/scala/pl/touk/nussknacker/ui/process}/periodic/PeriodicStateStatus.scala (98%) rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management => designer/server/src/main/scala/pl/touk/nussknacker/ui/process}/periodic/RescheduleFinishedActor.scala (90%) rename engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/SingleScheduleProperty.scala => designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/ScheduleProperty.scala (95%) rename engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicBatchConfig.scala => designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/SchedulingConfig.scala (71%) rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management => designer/server/src/main/scala/pl/touk/nussknacker/ui/process}/periodic/Utils.scala (95%) rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management => designer/server/src/main/scala/pl/touk/nussknacker/ui/process}/periodic/cron/CronParameterValidator.scala (63%) create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/cron/CronSchedulePropertyExtractor.scala rename engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/DbInitializer.scala => designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyDbInitializer.scala (95%) create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyPeriodicProcessDeploymentsTableFactory.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyPeriodicProcessesRepository.scala rename engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/PeriodicProcessesTable.scala => designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyPeriodicProcessesTableFactory.scala (92%) create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/model/PeriodicProcess.scala create mode 100644 designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/model/PeriodicProcessDeployment.scala rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management => designer/server/src/main/scala/pl/touk/nussknacker/ui/process}/periodic/model/SchedulesState.scala (66%) rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/util => designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/utils}/DeterministicUUIDFromLong.scala (93%) rename engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/SchedulePropertyExtractor.scala => designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/utils/SchedulePropertyExtractorUtils.scala (76%) rename {engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db => designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository}/PeriodicProcessesRepository.scala (65%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management => designer/server/src/test/scala/pl/touk/nussknacker/ui/process}/periodic/PeriodicProcessServiceIntegrationTest.scala (75%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management => designer/server/src/test/scala/pl/touk/nussknacker/ui/process}/periodic/PeriodicProcessesFetchingTest.scala (65%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink}/CronSchedulePropertyExtractorTest.scala (73%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink}/CronSchedulePropertyTest.scala (89%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink}/DeploymentActorTest.scala (63%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink}/DeploymentManagerStub.scala (93%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink}/FlinkClientStub.scala (96%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink}/PeriodicDeploymentManagerTest.scala (94%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink}/PeriodicProcessDeploymentGen.scala (53%) create mode 100644 designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessGen.scala rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink}/PeriodicProcessServiceTest.scala (81%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink}/PeriodicProcessStateDefinitionManagerTest.scala (86%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink}/RescheduleFinishedActorTest.scala (91%) create mode 100644 designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/ScheduledExecutionPerformerStub.scala rename engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/JarManagerTest.scala => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/ScheduledExecutionPerformerTest.scala (53%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink}/UtilsSpec.scala (94%) rename {engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic => designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink}/db/InMemPeriodicProcessesRepository.scala (53%) delete mode 100644 engine/flink/management/periodic/README.md delete mode 100644 engine/flink/management/periodic/src/main/resources/META-INF/services/pl.touk.nussknacker.engine.DeploymentManagerProvider delete mode 100644 engine/flink/management/periodic/src/main/resources/META-INF/services/pl.touk.nussknacker.engine.api.definition.CustomParameterValidator delete mode 100644 engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/FlinkPeriodicDeploymentManagerProvider.scala delete mode 100644 engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/JarManager.scala delete mode 100644 engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/SchedulePropertyExtractorFactory.scala delete mode 100644 engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/flink/FlinkJarManager.scala delete mode 100644 engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/DeploymentWithJarData.scala delete mode 100644 engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/PeriodicProcess.scala delete mode 100644 engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/PeriodicProcessDeployment.scala delete mode 100644 engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/service/AdditionalDeploymentDataProvider.scala delete mode 100644 engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/service/PeriodicProcessListener.scala delete mode 100644 engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/JarManagerStub.scala delete mode 100644 engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessGen.scala create mode 100644 engine/flink/management/src/main/scala/pl/touk/nussknacker/engine/management/FlinkScheduledExecutionPerformer.scala diff --git a/build.sbt b/build.sbt index 388e6e75faa..bcd09bc56a6 100644 --- a/build.sbt +++ b/build.sbt @@ -510,8 +510,7 @@ lazy val distribution: Project = sbt }, devArtifacts := { modelArtifacts.value ++ List( - (flinkDevModel / assembly).value -> "model/devModel.jar", - (flinkPeriodicDeploymentManager / assembly).value -> "managers/nussknacker-flink-periodic-manager.jar", + (flinkDevModel / assembly).value -> "model/devModel.jar", ) }, Universal / packageName := ("nussknacker" + "-" + version.value), @@ -611,8 +610,8 @@ lazy val flinkDeploymentManager = (project in flink("management")) libraryDependencies ++= { Seq( "org.typelevel" %% "cats-core" % catsV % Provided, - "org.apache.flink" % "flink-streaming-java" % flinkV % flinkScope - excludeAll ( + ("org.apache.flink" % "flink-streaming-java" % flinkV % flinkScope) + .excludeAll( ExclusionRule("log4j", "log4j"), ExclusionRule("org.slf4j", "slf4j-log4j12"), ExclusionRule("com.esotericsoftware", "kryo-shaded"), @@ -636,37 +635,6 @@ lazy val flinkDeploymentManager = (project in flink("management")) kafkaTestUtils % "it,test" ) -lazy val flinkPeriodicDeploymentManager = (project in flink("management/periodic")) - .settings(commonSettings) - .settings(assemblyNoScala("nussknacker-flink-periodic-manager.jar"): _*) - .settings(publishAssemblySettings: _*) - .settings( - name := "nussknacker-flink-periodic-manager", - libraryDependencies ++= { - Seq( - "org.typelevel" %% "cats-core" % catsV % Provided, - "com.typesafe.slick" %% "slick" % slickV % Provided, - "com.typesafe.slick" %% "slick-hikaricp" % slickV % "provided, test", - "com.github.tminglei" %% "slick-pg" % slickPgV, - "org.hsqldb" % "hsqldb" % hsqldbV % Test, - "org.flywaydb" % "flyway-core" % flywayV % Provided, - "com.cronutils" % "cron-utils" % cronParserV, - "com.typesafe.akka" %% "akka-actor" % akkaV, - "com.typesafe.akka" %% "akka-testkit" % akkaV % Test, - "com.dimafeng" %% "testcontainers-scala-scalatest" % testContainersScalaV % Test, - "com.dimafeng" %% "testcontainers-scala-postgresql" % testContainersScalaV % Test, - ) - } - ) - .dependsOn( - flinkDeploymentManager, - deploymentManagerApi % Provided, - scenarioCompiler % Provided, - componentsApi % Provided, - httpUtils % Provided, - testUtils % Test - ) - lazy val flinkMetricsDeferredReporter = (project in flink("metrics-deferred-reporter")) .settings(commonSettings) .settings( @@ -1811,10 +1779,10 @@ lazy val flinkBaseUnboundedComponents = (project in flink("components/base-unbou .settings( name := "nussknacker-flink-base-unbounded-components", libraryDependencies ++= Seq( - "org.apache.flink" % "flink-streaming-java" % flinkV % Provided, - "com.clearspring.analytics" % "stream" % "2.9.8" + "org.apache.flink" % "flink-streaming-java" % flinkV % Provided, // It is used only in QDigest which we don't use, while it's >20MB in size... - exclude ("it.unimi.dsi", "fastutil") + ("com.clearspring.analytics" % "stream" % "2.9.8") + .exclude("it.unimi.dsi", "fastutil") ) ) .dependsOn( @@ -2004,6 +1972,7 @@ lazy val designer = (project in file("designer/server")) "com.typesafe.akka" %% "akka-testkit" % akkaV % Test, "de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirceV, "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % sttpV, + "com.cronutils" % "cron-utils" % cronParserV, "ch.qos.logback" % "logback-core" % logbackV, "ch.qos.logback" % "logback-classic" % logbackV, "ch.qos.logback.contrib" % "logback-json-classic" % logbackJsonV, @@ -2026,6 +1995,7 @@ lazy val designer = (project in file("designer/server")) "com.beachape" %% "enumeratum-circe" % enumeratumV, "tf.tofu" %% "derevo-circe" % "0.13.0", "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % openapiCirceYamlV, + "com.github.tminglei" %% "slick-pg" % slickPgV, "com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % tapirV, "com.softwaremill.sttp.tapir" %% "tapir-core" % tapirV, "com.softwaremill.sttp.tapir" %% "tapir-derevo" % tapirV, @@ -2080,7 +2050,6 @@ lazy val designer = (project in file("designer/server")) liteEmbeddedDeploymentManager % Provided, liteK8sDeploymentManager % Provided, developmentTestsDeploymentManager % Provided, - flinkPeriodicDeploymentManager % Provided, schemedKafkaComponentsUtils % Provided, ) @@ -2170,7 +2139,6 @@ lazy val modules = List[ProjectReference]( requestResponseRuntime, liteEngineRuntimeApp, flinkDeploymentManager, - flinkPeriodicDeploymentManager, flinkDevModel, flinkDevModelJava, flinkTableApiComponents, diff --git a/designer/client/src/types/node.ts b/designer/client/src/types/node.ts index 014a4c13670..fe25fc99084 100644 --- a/designer/client/src/types/node.ts +++ b/designer/client/src/types/node.ts @@ -2,7 +2,7 @@ import { ProcessAdditionalFields, ReturnedType } from "./scenarioGraph"; import { FragmentInputParameter } from "../components/graph/node-modal/fragment-input-definition/item"; import { StickyNoteType } from "./stickyNote"; -type Type = "FragmentInput" | typeof StickyNoteType | string; +type Type = "FragmentInput" | typeof StickyNoteType | string; export type LayoutData = { x: number; y: number }; diff --git a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/DeploymentManager.scala b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/DeploymentManager.scala index bade92ff999..8aa3ab96743 100644 --- a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/DeploymentManager.scala +++ b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/DeploymentManager.scala @@ -1,9 +1,11 @@ package pl.touk.nussknacker.engine.api.deployment +import com.typesafe.config.Config import pl.touk.nussknacker.engine.api.deployment.inconsistency.InconsistentStateDetector +import pl.touk.nussknacker.engine.api.deployment.scheduler.services._ import pl.touk.nussknacker.engine.api.process.{ProcessIdWithName, ProcessName, VersionId} -import pl.touk.nussknacker.engine.newdeployment import pl.touk.nussknacker.engine.util.WithDataFreshnessStatusUtils.WithDataFreshnessStatusOps +import pl.touk.nussknacker.engine.{BaseModelData, DeploymentManagerDependencies, newdeployment} import java.time.Instant import scala.concurrent.ExecutionContext.Implicits._ @@ -48,6 +50,8 @@ trait DeploymentManager extends AutoCloseable { def stateQueryForAllScenariosSupport: StateQueryForAllScenariosSupport + def schedulingSupport: SchedulingSupport + def processCommand[Result](command: DMScenarioCommand[Result]): Future[Result] final def getProcessState( @@ -132,3 +136,25 @@ trait DeploymentSynchronisationSupported extends DeploymentSynchronisationSuppor } case object NoDeploymentSynchronisationSupport extends DeploymentSynchronisationSupport + +sealed trait SchedulingSupport + +trait SchedulingSupported extends SchedulingSupport { + + def createScheduledExecutionPerformer( + modelData: BaseModelData, + dependencies: DeploymentManagerDependencies, + deploymentConfig: Config, + ): ScheduledExecutionPerformer + + def customSchedulePropertyExtractorFactory: Option[SchedulePropertyExtractorFactory] + + def customProcessConfigEnricherFactory: Option[ProcessConfigEnricherFactory] + + def customScheduledProcessListenerFactory: Option[ScheduledProcessListenerFactory] + + def customAdditionalDeploymentDataProvider: Option[AdditionalDeploymentDataProvider] + +} + +case object NoSchedulingSupport extends SchedulingSupport diff --git a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/cache/CachingProcessStateDeploymentManager.scala b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/cache/CachingProcessStateDeploymentManager.scala index 97ca468a2ec..1689041c83b 100644 --- a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/cache/CachingProcessStateDeploymentManager.scala +++ b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/cache/CachingProcessStateDeploymentManager.scala @@ -16,6 +16,7 @@ class CachingProcessStateDeploymentManager( cacheTTL: FiniteDuration, override val deploymentSynchronisationSupport: DeploymentSynchronisationSupport, override val stateQueryForAllScenariosSupport: StateQueryForAllScenariosSupport, + override val schedulingSupport: SchedulingSupport, ) extends DeploymentManager { private val cache: AsyncCache[ProcessName, List[StatusDetails]] = Caffeine @@ -83,7 +84,8 @@ object CachingProcessStateDeploymentManager extends LazyLogging { delegate, cacheTTL, delegate.deploymentSynchronisationSupport, - delegate.stateQueryForAllScenariosSupport + delegate.stateQueryForAllScenariosSupport, + delegate.schedulingSupport, ) } .getOrElse { diff --git a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/DeploymentWithRuntimeParams.scala b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/DeploymentWithRuntimeParams.scala new file mode 100644 index 00000000000..a7d30f9c982 --- /dev/null +++ b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/DeploymentWithRuntimeParams.scala @@ -0,0 +1,12 @@ +package pl.touk.nussknacker.engine.api.deployment.scheduler.model + +import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId} + +final case class DeploymentWithRuntimeParams( + processId: Option[ProcessId], + processName: ProcessName, + versionId: VersionId, + runtimeParams: RuntimeParams, +) + +final case class RuntimeParams(params: Map[String, String]) diff --git a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/ScheduleProperty.scala b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/ScheduleProperty.scala new file mode 100644 index 00000000000..4b8a1f7826b --- /dev/null +++ b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/ScheduleProperty.scala @@ -0,0 +1,16 @@ +package pl.touk.nussknacker.engine.api.deployment.scheduler.model + +sealed trait ScheduleProperty + +object ScheduleProperty { + sealed trait SingleScheduleProperty extends ScheduleProperty + + final case class MultipleScheduleProperty( + schedules: Map[String, SingleScheduleProperty] + ) extends ScheduleProperty + + final case class CronScheduleProperty( + labelOrCronExpr: String + ) extends SingleScheduleProperty + +} diff --git a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/ScheduledDeploymentDetails.scala b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/ScheduledDeploymentDetails.scala new file mode 100644 index 00000000000..3d784ac7f0a --- /dev/null +++ b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/ScheduledDeploymentDetails.scala @@ -0,0 +1,28 @@ +package pl.touk.nussknacker.engine.api.deployment.scheduler.model + +import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} + +import java.time.LocalDateTime + +case class ScheduledDeploymentDetails( + id: Long, + processName: ProcessName, + versionId: VersionId, + scheduleName: Option[String], + createdAt: LocalDateTime, + runAt: LocalDateTime, + deployedAt: Option[LocalDateTime], + completedAt: Option[LocalDateTime], + status: ScheduledDeploymentStatus, +) + +sealed trait ScheduledDeploymentStatus + +object ScheduledDeploymentStatus { + case object Scheduled extends ScheduledDeploymentStatus + case object Deployed extends ScheduledDeploymentStatus + case object Finished extends ScheduledDeploymentStatus + case object Failed extends ScheduledDeploymentStatus + case object RetryingDeploy extends ScheduledDeploymentStatus + case object FailedOnDeploy extends ScheduledDeploymentStatus +} diff --git a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/ScheduledProcessDetails.scala b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/ScheduledProcessDetails.scala new file mode 100644 index 00000000000..8100c6f33c7 --- /dev/null +++ b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/model/ScheduledProcessDetails.scala @@ -0,0 +1,9 @@ +package pl.touk.nussknacker.engine.api.deployment.scheduler.model + +import pl.touk.nussknacker.engine.api.{MetaData, ProcessVersion} + +case class ScheduledProcessDetails( + processVersion: ProcessVersion, + processMetaData: MetaData, + inputConfigDuringExecutionJson: String, +) diff --git a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/AdditionalDeploymentDataProvider.scala b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/AdditionalDeploymentDataProvider.scala new file mode 100644 index 00000000000..158836ce6ca --- /dev/null +++ b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/AdditionalDeploymentDataProvider.scala @@ -0,0 +1,9 @@ +package pl.touk.nussknacker.engine.api.deployment.scheduler.services + +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.ScheduledDeploymentDetails + +trait AdditionalDeploymentDataProvider { + + def prepareAdditionalData(runDetails: ScheduledDeploymentDetails): Map[String, String] + +} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/service/ProcessConfigEnricher.scala b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/ProcessConfigEnricher.scala similarity index 77% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/service/ProcessConfigEnricher.scala rename to designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/ProcessConfigEnricher.scala index e428dbd5ba4..0e89cd851cb 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/service/ProcessConfigEnricher.scala +++ b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/ProcessConfigEnricher.scala @@ -1,14 +1,10 @@ -package pl.touk.nussknacker.engine.management.periodic.service +package pl.touk.nussknacker.engine.api.deployment.scheduler.services import com.typesafe.config.{Config, ConfigFactory} +import pl.touk.nussknacker.engine.api.ProcessVersion +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{ScheduledDeploymentDetails, ScheduledProcessDetails} +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.ProcessConfigEnricher.{DeployData, EnrichedProcessConfig, InitialScheduleData} import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData.WithCanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeployment -import pl.touk.nussknacker.engine.management.periodic.service.ProcessConfigEnricher.{ - DeployData, - EnrichedProcessConfig, - InitialScheduleData -} import pl.touk.nussknacker.engine.modelconfig.InputConfigDuringExecution import sttp.client3.SttpBackend @@ -32,7 +28,6 @@ trait ProcessConfigEnricher { object ProcessConfigEnricher { trait ProcessConfigEnricherInputData { - def canonicalProcess: CanonicalProcess def inputConfigDuringExecutionJson: String def inputConfigDuringExecution: Config = { @@ -41,13 +36,16 @@ object ProcessConfigEnricher { } - case class InitialScheduleData(canonicalProcess: CanonicalProcess, inputConfigDuringExecutionJson: String) - extends ProcessConfigEnricherInputData + case class InitialScheduleData( + canonicalProcess: CanonicalProcess, + inputConfigDuringExecutionJson: String + ) extends ProcessConfigEnricherInputData case class DeployData( - canonicalProcess: CanonicalProcess, - inputConfigDuringExecutionJson: String, - deployment: PeriodicProcessDeployment[WithCanonicalProcess] + canonicalProcess: CanonicalProcess, + processVersion: ProcessVersion, + inputConfigDuringExecutionJson: String, + deploymentDetails: ScheduledDeploymentDetails, ) extends ProcessConfigEnricherInputData case class EnrichedProcessConfig(inputConfigDuringExecutionJson: String) diff --git a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/SchedulePropertyExtractorFactory.scala b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/SchedulePropertyExtractorFactory.scala new file mode 100644 index 00000000000..112c14ea68a --- /dev/null +++ b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/SchedulePropertyExtractorFactory.scala @@ -0,0 +1,13 @@ +package pl.touk.nussknacker.engine.api.deployment.scheduler.services + +import com.typesafe.config.Config +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.ScheduleProperty +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess + +trait SchedulePropertyExtractorFactory { + def apply(config: Config): SchedulePropertyExtractor +} + +trait SchedulePropertyExtractor { + def apply(canonicalProcess: CanonicalProcess): Either[String, ScheduleProperty] +} diff --git a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/ScheduledExecutionPerformer.scala b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/ScheduledExecutionPerformer.scala new file mode 100644 index 00000000000..d971c640d48 --- /dev/null +++ b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/ScheduledExecutionPerformer.scala @@ -0,0 +1,31 @@ +package pl.touk.nussknacker.engine.api.deployment.scheduler.services + +import pl.touk.nussknacker.engine.api.ProcessVersion +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{DeploymentWithRuntimeParams, RuntimeParams} +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess +import pl.touk.nussknacker.engine.deployment.{DeploymentData, ExternalDeploymentId} +import pl.touk.nussknacker.engine.modelconfig.InputConfigDuringExecution + +import scala.concurrent.Future + +trait ScheduledExecutionPerformer { + + def provideInputConfigDuringExecutionJson(): Future[InputConfigDuringExecution] + + def prepareDeploymentWithRuntimeParams( + processVersion: ProcessVersion, + ): Future[DeploymentWithRuntimeParams] + + def deployWithRuntimeParams( + deploymentWithJarData: DeploymentWithRuntimeParams, + inputConfigDuringExecutionJson: String, + deploymentData: DeploymentData, + canonicalProcess: CanonicalProcess, + processVersion: ProcessVersion, + ): Future[Option[ExternalDeploymentId]] + + def cleanAfterDeployment( + runtimeParams: RuntimeParams + ): Future[Unit] + +} diff --git a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/ScheduledProcessListener.scala b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/ScheduledProcessListener.scala new file mode 100644 index 00000000000..b1653dedff3 --- /dev/null +++ b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/scheduler/services/ScheduledProcessListener.scala @@ -0,0 +1,60 @@ +package pl.touk.nussknacker.engine.api.deployment.scheduler.services + +import com.typesafe.config.Config +import pl.touk.nussknacker.engine.api.deployment.StatusDetails +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.ScheduledDeploymentDetails +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess +import pl.touk.nussknacker.engine.deployment.ExternalDeploymentId + +/* + Listener is at-least-once. If there are problems e.g. with DB, invocation can be repeated for same event. + Implementation should be aware of that. Listener is invoked during DB transaction, for that reason it's *synchronous* + */ +trait ScheduledProcessListener { + + def onScheduledProcessEvent: PartialFunction[ScheduledProcessEvent, Unit] + def close(): Unit = {} +} + +trait ScheduledProcessListenerFactory { + def create(config: Config): ScheduledProcessListener +} + +sealed trait ScheduledProcessEvent { + val deployment: ScheduledDeploymentDetails +} + +case class DeployedEvent( + deployment: ScheduledDeploymentDetails, + externalDeploymentId: Option[ExternalDeploymentId] +) extends ScheduledProcessEvent + +case class FinishedEvent( + deployment: ScheduledDeploymentDetails, + canonicalProcess: CanonicalProcess, + processState: Option[StatusDetails] +) extends ScheduledProcessEvent + +case class FailedOnDeployEvent( + deployment: ScheduledDeploymentDetails, + processState: Option[StatusDetails] +) extends ScheduledProcessEvent + +case class FailedOnRunEvent( + deployment: ScheduledDeploymentDetails, + processState: Option[StatusDetails] +) extends ScheduledProcessEvent + +case class ScheduledEvent(deployment: ScheduledDeploymentDetails, firstSchedule: Boolean) extends ScheduledProcessEvent + +object EmptyListener extends EmptyListener + +trait EmptyListener extends ScheduledProcessListener { + + override def onScheduledProcessEvent: PartialFunction[ScheduledProcessEvent, Unit] = Map.empty + +} + +object EmptyScheduledProcessListenerFactory extends ScheduledProcessListenerFactory { + override def create(config: Config): ScheduledProcessListener = EmptyListener +} diff --git a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/testing/DeploymentManagerStub.scala b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/testing/DeploymentManagerStub.scala index 3fad23ad8a9..1919c1d4b35 100644 --- a/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/testing/DeploymentManagerStub.scala +++ b/designer/deployment-manager-api/src/main/scala/pl/touk/nussknacker/engine/testing/DeploymentManagerStub.scala @@ -61,6 +61,8 @@ class DeploymentManagerStub extends BaseDeploymentManager with StubbingCommands override def stateQueryForAllScenariosSupport: StateQueryForAllScenariosSupport = NoStateQueryForAllScenariosSupport + override def schedulingSupport: SchedulingSupport = NoSchedulingSupport + override def close(): Unit = {} } diff --git a/designer/deployment-manager-api/src/test/scala/pl/touk/nussknacker/engine/api/deployment/cache/CachingProcessStateDeploymentManagerSpec.scala b/designer/deployment-manager-api/src/test/scala/pl/touk/nussknacker/engine/api/deployment/cache/CachingProcessStateDeploymentManagerSpec.scala index 172d141e792..c24c358ff9e 100644 --- a/designer/deployment-manager-api/src/test/scala/pl/touk/nussknacker/engine/api/deployment/cache/CachingProcessStateDeploymentManagerSpec.scala +++ b/designer/deployment-manager-api/src/test/scala/pl/touk/nussknacker/engine/api/deployment/cache/CachingProcessStateDeploymentManagerSpec.scala @@ -30,7 +30,8 @@ class CachingProcessStateDeploymentManagerSpec delegate, 10 seconds, NoDeploymentSynchronisationSupport, - NoStateQueryForAllScenariosSupport + NoStateQueryForAllScenariosSupport, + NoSchedulingSupport, ) val results = List( @@ -49,7 +50,8 @@ class CachingProcessStateDeploymentManagerSpec delegate, 10 seconds, NoDeploymentSynchronisationSupport, - NoStateQueryForAllScenariosSupport + NoStateQueryForAllScenariosSupport, + NoSchedulingSupport, ) val firstInvocation = cachingManager.getProcessStatesDeploymentIdNow(DataFreshnessPolicy.CanBeCached) @@ -67,7 +69,8 @@ class CachingProcessStateDeploymentManagerSpec delegate, 10 seconds, NoDeploymentSynchronisationSupport, - NoStateQueryForAllScenariosSupport + NoStateQueryForAllScenariosSupport, + NoSchedulingSupport, ) val resultForFresh = cachingManager.getProcessStatesDeploymentIdNow(DataFreshnessPolicy.Fresh) diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/common/V1_103__drop_buildinfo.sql b/designer/server/src/main/resources/db/batch_periodic/migration/common/V1_103__drop_buildinfo.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/common/V1_103__drop_buildinfo.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/common/V1_103__drop_buildinfo.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/common/V1_105__add_indexes.sql b/designer/server/src/main/resources/db/batch_periodic/migration/common/V1_105__add_indexes.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/common/V1_105__add_indexes.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/common/V1_105__add_indexes.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/common/V1_107__add_retry_at_and_retries_to_periodic_process_deployments.sql b/designer/server/src/main/resources/db/batch_periodic/migration/common/V1_107__add_retry_at_and_retries_to_periodic_process_deployments.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/common/V1_107__add_retry_at_and_retries_to_periodic_process_deployments.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/common/V1_107__add_retry_at_and_retries_to_periodic_process_deployments.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/common/V1_109__add_action_id_to_periodic_processes.sql b/designer/server/src/main/resources/db/batch_periodic/migration/common/V1_109__add_action_id_to_periodic_processes.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/common/V1_109__add_action_id_to_periodic_processes.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/common/V1_109__add_action_id_to_periodic_processes.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/hsql/V1_101__create_batch_periodic_tables.sql b/designer/server/src/main/resources/db/batch_periodic/migration/hsql/V1_101__create_batch_periodic_tables.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/hsql/V1_101__create_batch_periodic_tables.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/hsql/V1_101__create_batch_periodic_tables.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/hsql/V1_102__add_program_args_and_jar_id.sql b/designer/server/src/main/resources/db/batch_periodic/migration/hsql/V1_102__add_program_args_and_jar_id.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/hsql/V1_102__add_program_args_and_jar_id.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/hsql/V1_102__add_program_args_and_jar_id.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/hsql/V1_104__multiple_schedules.sql b/designer/server/src/main/resources/db/batch_periodic/migration/hsql/V1_104__multiple_schedules.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/hsql/V1_104__multiple_schedules.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/hsql/V1_104__multiple_schedules.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/hsql/V1_106__rename_model_config.sql b/designer/server/src/main/resources/db/batch_periodic/migration/hsql/V1_106__rename_model_config.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/hsql/V1_106__rename_model_config.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/hsql/V1_106__rename_model_config.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/hsql/V1_108__add_processing_type.sql b/designer/server/src/main/resources/db/batch_periodic/migration/hsql/V1_108__add_processing_type.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/hsql/V1_108__add_processing_type.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/hsql/V1_108__add_processing_type.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/postgres/V1_101__create_batch_periodic_tables.sql b/designer/server/src/main/resources/db/batch_periodic/migration/postgres/V1_101__create_batch_periodic_tables.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/postgres/V1_101__create_batch_periodic_tables.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/postgres/V1_101__create_batch_periodic_tables.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/postgres/V1_102__add_program_args_and_jar_id.sql b/designer/server/src/main/resources/db/batch_periodic/migration/postgres/V1_102__add_program_args_and_jar_id.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/postgres/V1_102__add_program_args_and_jar_id.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/postgres/V1_102__add_program_args_and_jar_id.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/postgres/V1_104__multiple_schedules.sql b/designer/server/src/main/resources/db/batch_periodic/migration/postgres/V1_104__multiple_schedules.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/postgres/V1_104__multiple_schedules.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/postgres/V1_104__multiple_schedules.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/postgres/V1_106__rename_model_config.sql b/designer/server/src/main/resources/db/batch_periodic/migration/postgres/V1_106__rename_model_config.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/postgres/V1_106__rename_model_config.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/postgres/V1_106__rename_model_config.sql diff --git a/engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/postgres/V1_108__add_processing_type.sql b/designer/server/src/main/resources/db/batch_periodic/migration/postgres/V1_108__add_processing_type.sql similarity index 100% rename from engine/flink/management/periodic/src/main/resources/db/batch_periodic/migration/postgres/V1_108__add_processing_type.sql rename to designer/server/src/main/resources/db/batch_periodic/migration/postgres/V1_108__add_processing_type.sql diff --git a/engine/flink/management/periodic/src/main/resources/web/static/assets/states/scheduled.svg b/designer/server/src/main/resources/web/static/assets/states/scheduled.svg similarity index 100% rename from engine/flink/management/periodic/src/main/resources/web/static/assets/states/scheduled.svg rename to designer/server/src/main/resources/web/static/assets/states/scheduled.svg diff --git a/engine/flink/management/periodic/src/main/resources/web/static/assets/states/wait-reschedule.svg b/designer/server/src/main/resources/web/static/assets/states/wait-reschedule.svg similarity index 100% rename from engine/flink/management/periodic/src/main/resources/web/static/assets/states/wait-reschedule.svg rename to designer/server/src/main/resources/web/static/assets/states/wait-reschedule.svg diff --git a/designer/server/src/main/scala/db/migration/V1_061__PeriodicDeploymentManagerTablesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_061__PeriodicDeploymentManagerTablesDefinition.scala new file mode 100644 index 00000000000..75279963168 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/V1_061__PeriodicDeploymentManagerTablesDefinition.scala @@ -0,0 +1,153 @@ +package db.migration + +import com.typesafe.scalalogging.LazyLogging +import db.migration.V1_061__PeriodicDeploymentManagerTablesDefinition.Definitions +import pl.touk.nussknacker.ui.db.migration.SlickMigration +import slick.jdbc.JdbcProfile +import slick.lifted.ProvenShape +import slick.sql.SqlProfile.ColumnOption.NotNull + +import java.time.LocalDateTime +import java.util.UUID +import scala.concurrent.ExecutionContext.Implicits.global + +trait V1_061__PeriodicDeploymentManagerTablesDefinition extends SlickMigration with LazyLogging { + + import profile.api._ + + private val definitions = new Definitions(profile) + + override def migrateActions: DBIOAction[Any, NoStream, Effect.All] = { + logger.info("Starting migration V1_061__PeriodicDeploymentManagerTablesDefinition") + for { + _ <- definitions.periodicProcessesTable.schema.create + _ <- definitions.periodicProcessDeploymentsTable.schema.create + } yield () + } + +} + +object V1_061__PeriodicDeploymentManagerTablesDefinition { + + class Definitions(val profile: JdbcProfile) { + import profile.api._ + + val periodicProcessDeploymentsTable = TableQuery[PeriodicProcessDeploymentsTable] + + class PeriodicProcessDeploymentsTable(tag: Tag) + extends Table[PeriodicProcessDeploymentEntity](tag, "periodic_scenario_deployments") { + + def id: Rep[Long] = column[Long]("id", O.PrimaryKey, O.AutoInc) + + def periodicProcessId: Rep[Long] = column[Long]("periodic_process_id", NotNull) + + def createdAt: Rep[LocalDateTime] = column[LocalDateTime]("created_at", NotNull) + + def runAt: Rep[LocalDateTime] = column[LocalDateTime]("run_at", NotNull) + + def scheduleName: Rep[Option[String]] = column[Option[String]]("schedule_name") + + def deployedAt: Rep[Option[LocalDateTime]] = column[Option[LocalDateTime]]("deployed_at") + + def completedAt: Rep[Option[LocalDateTime]] = column[Option[LocalDateTime]]("completed_at") + + def retriesLeft: Rep[Int] = column[Int]("retries_left") + + def nextRetryAt: Rep[Option[LocalDateTime]] = column[Option[LocalDateTime]]("next_retry_at") + + def status: Rep[String] = column[String]("status", NotNull) + + def periodicProcessIdIndex = index("periodic_scenario_deployments_periodic_process_id_idx", periodicProcessId) + def createdAtIndex = index("periodic_scenario_deployments_created_at_idx", createdAt) + def runAtIndex = index("periodic_scenario_deployments_run_at_idx", runAt) + + override def * : ProvenShape[PeriodicProcessDeploymentEntity] = ( + id, + periodicProcessId, + createdAt, + runAt, + scheduleName, + deployedAt, + completedAt, + retriesLeft, + nextRetryAt, + status + ) <> + ((PeriodicProcessDeploymentEntity.apply _).tupled, PeriodicProcessDeploymentEntity.unapply) + + } + + case class PeriodicProcessDeploymentEntity( + id: Long, + periodicProcessId: Long, + createdAt: LocalDateTime, + runAt: LocalDateTime, + scheduleName: Option[String], + deployedAt: Option[LocalDateTime], + completedAt: Option[LocalDateTime], + retriesLeft: Int, + nextRetryAt: Option[LocalDateTime], + status: String + ) + + val periodicProcessesTable = TableQuery[PeriodicProcessesTable] + + class PeriodicProcessesTable(tag: Tag) extends Table[PeriodicProcessEntity](tag, "periodic_scenarios") { + + def periodicProcessId: Rep[Long] = column[Long]("id", O.Unique, O.AutoInc) + + def processId: Rep[Option[Long]] = column[Option[Long]]("process_id") + + def processName: Rep[String] = column[String]("process_name", NotNull) + + def processVersionId: Rep[Long] = column[Long]("process_version_id", NotNull) + + def processingType: Rep[String] = column[String]("processing_type", NotNull) + + def runtimeParams: Rep[String] = column[String]("runtime_params") + + def scheduleProperty: Rep[String] = column[String]("schedule_property", NotNull) + + def active: Rep[Boolean] = column[Boolean]("active", NotNull) + + def createdAt: Rep[LocalDateTime] = column[LocalDateTime]("created_at", NotNull) + + def processActionId: Rep[Option[UUID]] = column[Option[UUID]]("process_action_id") + + def inputConfigDuringExecutionJson: Rep[String] = column[String]("input_config_during_execution", NotNull) + + def processNameAndActiveIndex = index("periodic_scenarios_process_name_active_idx", (processName, active)) + + override def * : ProvenShape[PeriodicProcessEntity] = ( + periodicProcessId, + processId, + processName, + processVersionId, + processingType, + runtimeParams, + scheduleProperty, + active, + createdAt, + processActionId, + inputConfigDuringExecutionJson, + ) <> (PeriodicProcessEntity.apply _ tupled, PeriodicProcessEntity.unapply) + + } + + case class PeriodicProcessEntity( + id: Long, + processId: Option[Long], + processName: String, + processVersionId: Long, + processingType: String, + runtimeParams: String, + scheduleProperty: String, + active: Boolean, + createdAt: LocalDateTime, + processActionId: Option[UUID], + inputConfigDuringExecutionJson: String, + ) + + } + +} diff --git a/designer/server/src/main/scala/db/migration/hsql/V1_061__PeriodicDeploymentManagerTables.scala b/designer/server/src/main/scala/db/migration/hsql/V1_061__PeriodicDeploymentManagerTables.scala new file mode 100644 index 00000000000..350e8fdc9e0 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/hsql/V1_061__PeriodicDeploymentManagerTables.scala @@ -0,0 +1,8 @@ +package db.migration.hsql + +import db.migration.V1_061__PeriodicDeploymentManagerTablesDefinition +import slick.jdbc.{HsqldbProfile, JdbcProfile} + +class V1_061__PeriodicDeploymentManagerTables extends V1_061__PeriodicDeploymentManagerTablesDefinition { + override protected lazy val profile: JdbcProfile = HsqldbProfile +} diff --git a/designer/server/src/main/scala/db/migration/postgres/V1_061__PeriodicDeploymentManagerTables.scala b/designer/server/src/main/scala/db/migration/postgres/V1_061__PeriodicDeploymentManagerTables.scala new file mode 100644 index 00000000000..97271414d93 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/postgres/V1_061__PeriodicDeploymentManagerTables.scala @@ -0,0 +1,8 @@ +package db.migration.postgres + +import db.migration.V1_061__PeriodicDeploymentManagerTablesDefinition +import slick.jdbc.{JdbcProfile, PostgresProfile} + +class V1_061__PeriodicDeploymentManagerTables extends V1_061__PeriodicDeploymentManagerTablesDefinition { + override protected lazy val profile: JdbcProfile = PostgresProfile +} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/PeriodicProcessDeploymentsTable.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/PeriodicProcessDeploymentsTable.scala similarity index 84% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/PeriodicProcessDeploymentsTable.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/PeriodicProcessDeploymentsTable.scala index f9b1bacb69b..8c82157e36d 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/PeriodicProcessDeploymentsTable.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/PeriodicProcessDeploymentsTable.scala @@ -1,7 +1,7 @@ -package pl.touk.nussknacker.engine.management.periodic.db +package pl.touk.nussknacker.ui.db.entity -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus -import pl.touk.nussknacker.engine.management.periodic.model.{ +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus +import pl.touk.nussknacker.ui.process.periodic.model.{ PeriodicProcessDeploymentId, PeriodicProcessDeploymentStatus, PeriodicProcessId @@ -14,15 +14,16 @@ import java.time.LocalDateTime trait PeriodicProcessDeploymentsTableFactory extends PeriodicProcessesTableFactory { - protected val profile: JdbcProfile - import profile.api._ + implicit val periodicProcessDeploymentIdMapping: BaseColumnType[PeriodicProcessDeploymentId] = + MappedColumnType.base[PeriodicProcessDeploymentId, Long](_.value, PeriodicProcessDeploymentId.apply) + implicit val periodicProcessDeploymentStatusColumnTyped: JdbcType[PeriodicProcessDeploymentStatus] = MappedColumnType.base[PeriodicProcessDeploymentStatus, String](_.toString, PeriodicProcessDeploymentStatus.withName) class PeriodicProcessDeploymentsTable(tag: Tag) - extends Table[PeriodicProcessDeploymentEntity](tag, "periodic_process_deployments") { + extends Table[PeriodicProcessDeploymentEntity](tag, "periodic_scenario_deployments") { def id: Rep[PeriodicProcessDeploymentId] = column[PeriodicProcessDeploymentId]("id", O.PrimaryKey, O.AutoInc) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/PeriodicProcessesTable.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/PeriodicProcessesTable.scala new file mode 100644 index 00000000000..f4d7c968a50 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/PeriodicProcessesTable.scala @@ -0,0 +1,160 @@ +package pl.touk.nussknacker.ui.db.entity + +import io.circe.Decoder +import io.circe.syntax.EncoderOps +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.RuntimeParams +import pl.touk.nussknacker.engine.api.deployment.ProcessActionId +import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId} +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessId +import slick.jdbc.JdbcProfile +import slick.lifted.ProvenShape +import slick.sql.SqlProfile.ColumnOption.NotNull + +import java.time.LocalDateTime +import java.util.UUID + +trait PeriodicProcessesTableFactory extends BaseEntityFactory { + + protected val profile: JdbcProfile + + import profile.api._ + + implicit val periodicProcessIdMapping: BaseColumnType[PeriodicProcessId] = + MappedColumnType.base[PeriodicProcessId, Long](_.value, PeriodicProcessId.apply) + + private implicit val processActionIdTypedType: BaseColumnType[ProcessActionId] = + MappedColumnType.base[ProcessActionId, UUID]( + _.value, + ProcessActionId(_) + ) + + implicit val runtimeParamsTypedType: BaseColumnType[RuntimeParams] = + MappedColumnType.base[RuntimeParams, String]( + _.params.asJson.noSpaces, + jsonStr => + io.circe.parser.parse(jsonStr).flatMap(Decoder[Map[String, String]].decodeJson) match { + case Right(params) => RuntimeParams(params) + case Left(error) => throw error + } + ) + + abstract class PeriodicProcessesTable[ENTITY <: PeriodicProcessEntity](tag: Tag) + extends Table[ENTITY](tag, "periodic_scenarios") { + + def id: Rep[PeriodicProcessId] = column[PeriodicProcessId]("id", O.PrimaryKey, O.AutoInc) + + def processId: Rep[Option[ProcessId]] = column[Option[ProcessId]]("process_id") + + def processName: Rep[ProcessName] = column[ProcessName]("process_name", NotNull) + + def processVersionId: Rep[VersionId] = column[VersionId]("process_version_id", NotNull) + + def processingType: Rep[String] = column[String]("processing_type", NotNull) + + def runtimeParams: Rep[RuntimeParams] = column[RuntimeParams]("runtime_params") + + def scheduleProperty: Rep[String] = column[String]("schedule_property", NotNull) + + def active: Rep[Boolean] = column[Boolean]("active", NotNull) + + def createdAt: Rep[LocalDateTime] = column[LocalDateTime]("created_at", NotNull) + + def processActionId: Rep[Option[ProcessActionId]] = column[Option[ProcessActionId]]("process_action_id") + + } + + class PeriodicProcessesWithInputConfigJsonTable(tag: Tag) + extends PeriodicProcessesTable[PeriodicProcessEntityWithInputConfigJson](tag) { + + def inputConfigDuringExecutionJson: Rep[String] = column[String]("input_config_during_execution", NotNull) + + override def * : ProvenShape[PeriodicProcessEntityWithInputConfigJson] = ( + id, + processId, + processName, + processVersionId, + processingType, + runtimeParams, + scheduleProperty, + active, + createdAt, + processActionId, + inputConfigDuringExecutionJson, + ) <> (PeriodicProcessEntityWithInputConfigJson.apply _ tupled, PeriodicProcessEntityWithInputConfigJson.unapply) + + } + + class PeriodicProcessesWithoutInputConfigJsonTable(tag: Tag) + extends PeriodicProcessesTable[PeriodicProcessEntityWithoutInputConfigJson](tag) { + + override def * : ProvenShape[PeriodicProcessEntityWithoutInputConfigJson] = ( + id, + processId, + processName, + processVersionId, + processingType, + runtimeParams, + scheduleProperty, + active, + createdAt, + processActionId + ) <> (PeriodicProcessEntityWithoutInputConfigJson.apply _ tupled, PeriodicProcessEntityWithoutInputConfigJson.unapply) + + } + + object PeriodicProcessesWithoutInputConfig extends TableQuery(new PeriodicProcessesWithoutInputConfigJsonTable(_)) + + object PeriodicProcessesWithInputConfig extends TableQuery(new PeriodicProcessesWithInputConfigJsonTable(_)) + +} + +trait PeriodicProcessEntity { + + def id: PeriodicProcessId + + def processId: Option[ProcessId] + + def processName: ProcessName + + def processVersionId: VersionId + + def processingType: String + + def runtimeParams: RuntimeParams + + def scheduleProperty: String + + def active: Boolean + + def createdAt: LocalDateTime + + def processActionId: Option[ProcessActionId] + +} + +case class PeriodicProcessEntityWithInputConfigJson( + id: PeriodicProcessId, + processId: Option[ProcessId], + processName: ProcessName, + processVersionId: VersionId, + processingType: String, + runtimeParams: RuntimeParams, + scheduleProperty: String, + active: Boolean, + createdAt: LocalDateTime, + processActionId: Option[ProcessActionId], + inputConfigDuringExecutionJson: String, +) extends PeriodicProcessEntity + +case class PeriodicProcessEntityWithoutInputConfigJson( + id: PeriodicProcessId, + processId: Option[ProcessId], + processName: ProcessName, + processVersionId: VersionId, + processingType: String, + runtimeParams: RuntimeParams, + scheduleProperty: String, + active: Boolean, + createdAt: LocalDateTime, + processActionId: Option[ProcessActionId] +) extends PeriodicProcessEntity diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/DefaultAdditionalDeploymentDataProvider.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/DefaultAdditionalDeploymentDataProvider.scala new file mode 100644 index 00000000000..c82cd46b59c --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/DefaultAdditionalDeploymentDataProvider.scala @@ -0,0 +1,18 @@ +package pl.touk.nussknacker.ui.process.periodic + +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.ScheduledDeploymentDetails +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.AdditionalDeploymentDataProvider + +import java.time.format.DateTimeFormatter + +object DefaultAdditionalDeploymentDataProvider extends AdditionalDeploymentDataProvider { + + override def prepareAdditionalData(runDetails: ScheduledDeploymentDetails): Map[String, String] = { + Map( + "deploymentId" -> runDetails.id.toString, + "runAt" -> runDetails.runAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), + "scheduleName" -> runDetails.scheduleName.getOrElse("") + ) + } + +} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/DeploymentActor.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/DeploymentActor.scala similarity index 70% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/DeploymentActor.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/DeploymentActor.scala index 1beb82e32ac..d8361fbbb9e 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/DeploymentActor.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/DeploymentActor.scala @@ -1,15 +1,9 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic import akka.actor.{Actor, Props, Timers} import com.typesafe.scalalogging.LazyLogging -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.DeploymentActor.{ - CheckToBeDeployed, - DeploymentCompleted, - WaitingForDeployment -} -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData.WithCanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeployment +import pl.touk.nussknacker.ui.process.periodic.DeploymentActor._ +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeployment import scala.concurrent.Future import scala.concurrent.duration._ @@ -22,8 +16,8 @@ object DeploymentActor { } private[periodic] def props( - findToBeDeployed: => Future[Seq[PeriodicProcessDeployment[WithCanonicalProcess]]], - deploy: PeriodicProcessDeployment[WithCanonicalProcess] => Future[Unit], + findToBeDeployed: => Future[Seq[PeriodicProcessDeployment]], + deploy: PeriodicProcessDeployment => Future[Unit], interval: FiniteDuration ) = { Props(new DeploymentActor(findToBeDeployed, deploy, interval)) @@ -31,14 +25,14 @@ object DeploymentActor { private[periodic] case object CheckToBeDeployed - private case class WaitingForDeployment(ids: List[PeriodicProcessDeployment[WithCanonicalProcess]]) + private case class WaitingForDeployment(ids: List[PeriodicProcessDeployment]) private case object DeploymentCompleted } class DeploymentActor( - findToBeDeployed: => Future[Seq[PeriodicProcessDeployment[WithCanonicalProcess]]], - deploy: PeriodicProcessDeployment[WithCanonicalProcess] => Future[Unit], + findToBeDeployed: => Future[Seq[PeriodicProcessDeployment]], + deploy: PeriodicProcessDeployment => Future[Unit], interval: FiniteDuration ) extends Actor with Timers @@ -74,7 +68,9 @@ class DeploymentActor( } } - private def receiveOngoingDeployment(runDetails: PeriodicProcessDeployment[WithCanonicalProcess]): Receive = { + private def receiveOngoingDeployment( + runDetails: PeriodicProcessDeployment + ): Receive = { case CheckToBeDeployed => logger.debug(s"Still waiting for ${runDetails.display} to be deployed") case DeploymentCompleted => diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicDeploymentManager.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicDeploymentManager.scala similarity index 81% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicDeploymentManager.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicDeploymentManager.scala index d5bac21c6a1..5242de64a68 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicDeploymentManager.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicDeploymentManager.scala @@ -1,29 +1,18 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic import cats.data.OptionT import com.typesafe.config.Config import com.typesafe.scalalogging.LazyLogging +import pl.touk.nussknacker.engine.DeploymentManagerDependencies import pl.touk.nussknacker.engine.api.deployment._ +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{ScheduleProperty => ApiScheduleProperty} +import pl.touk.nussknacker.engine.api.deployment.scheduler.services._ import pl.touk.nussknacker.engine.api.process.{ProcessIdWithName, ProcessName, VersionId} import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.engine.deployment.ExternalDeploymentId -import pl.touk.nussknacker.engine.management.FlinkConfig -import pl.touk.nussknacker.engine.management.periodic.PeriodicProcessService.PeriodicProcessStatus -import pl.touk.nussknacker.engine.management.periodic.Utils.{createActorWithRetry, runSafely} -import pl.touk.nussknacker.engine.management.periodic.db.{ - DbInitializer, - PeriodicProcessesRepository, - SlickPeriodicProcessesRepository -} -import pl.touk.nussknacker.engine.management.periodic.flink.FlinkJarManager -import pl.touk.nussknacker.engine.management.periodic.service.{ - AdditionalDeploymentDataProvider, - PeriodicProcessListenerFactory, - ProcessConfigEnricherFactory -} -import pl.touk.nussknacker.engine.{BaseModelData, DeploymentManagerDependencies} -import slick.jdbc -import slick.jdbc.JdbcProfile +import pl.touk.nussknacker.ui.process.periodic.PeriodicProcessService.PeriodicProcessStatus +import pl.touk.nussknacker.ui.process.periodic.Utils._ +import pl.touk.nussknacker.ui.process.repository.PeriodicProcessesRepository import java.time.{Clock, Instant} import scala.concurrent.{ExecutionContext, Future} @@ -32,51 +21,46 @@ object PeriodicDeploymentManager { def apply( delegate: DeploymentManager, + scheduledExecutionPerformer: ScheduledExecutionPerformer, schedulePropertyExtractorFactory: SchedulePropertyExtractorFactory, processConfigEnricherFactory: ProcessConfigEnricherFactory, - periodicBatchConfig: PeriodicBatchConfig, - flinkConfig: FlinkConfig, + schedulingConfig: SchedulingConfig, originalConfig: Config, - modelData: BaseModelData, - listenerFactory: PeriodicProcessListenerFactory, + listenerFactory: ScheduledProcessListenerFactory, additionalDeploymentDataProvider: AdditionalDeploymentDataProvider, - dependencies: DeploymentManagerDependencies + dependencies: DeploymentManagerDependencies, + periodicProcessesRepository: PeriodicProcessesRepository, ): PeriodicDeploymentManager = { import dependencies._ - val clock = Clock.systemDefaultZone() - - val (db: jdbc.JdbcBackend.DatabaseDef, dbProfile: JdbcProfile) = DbInitializer.init(periodicBatchConfig.db) - val scheduledProcessesRepository = - new SlickPeriodicProcessesRepository(db, dbProfile, clock, periodicBatchConfig.processingType) - val jarManager = FlinkJarManager(flinkConfig, periodicBatchConfig, modelData) + val clock = Clock.systemDefaultZone() val listener = listenerFactory.create(originalConfig) val processConfigEnricher = processConfigEnricherFactory(originalConfig) val service = new PeriodicProcessService( delegate, - jarManager, - scheduledProcessesRepository, + scheduledExecutionPerformer, + periodicProcessesRepository, listener, additionalDeploymentDataProvider, - periodicBatchConfig.deploymentRetry, - periodicBatchConfig.executionConfig, - periodicBatchConfig.maxFetchedPeriodicScenarioActivities, + schedulingConfig.deploymentRetry, + schedulingConfig.executionConfig, + schedulingConfig.maxFetchedPeriodicScenarioActivities, processConfigEnricher, clock, dependencies.actionService, - dependencies.configsFromProvider + dependencies.configsFromProvider, ) // These actors have to be created with retries because they can initially fail to create due to taken names, // if the actors (with the same names) created before reload aren't fully stopped (and their names freed) yet val deploymentActor = createActorWithRetry( - s"periodic-${periodicBatchConfig.processingType}-deployer", - DeploymentActor.props(service, periodicBatchConfig.deployInterval), + s"periodic-${schedulingConfig.processingType}-deployer", + DeploymentActor.props(service, schedulingConfig.deployInterval), dependencies.actorSystem ) val rescheduleFinishedActor = createActorWithRetry( - s"periodic-${periodicBatchConfig.processingType}-rescheduler", - RescheduleFinishedActor.props(service, periodicBatchConfig.rescheduleCheckInterval), + s"periodic-${schedulingConfig.processingType}-rescheduler", + RescheduleFinishedActor.props(service, schedulingConfig.rescheduleCheckInterval), dependencies.actorSystem ) @@ -86,12 +70,11 @@ object PeriodicDeploymentManager { // they don't have any internal state, so stopping them non-gracefully is safe runSafely(dependencies.actorSystem.stop(deploymentActor)) runSafely(dependencies.actorSystem.stop(rescheduleFinishedActor)) - runSafely(db.close()) } new PeriodicDeploymentManager( delegate, service, - scheduledProcessesRepository, + periodicProcessesRepository, schedulePropertyExtractorFactory(originalConfig), toClose ) @@ -102,7 +85,7 @@ object PeriodicDeploymentManager { class PeriodicDeploymentManager private[periodic] ( val delegate: DeploymentManager, service: PeriodicProcessService, - repository: PeriodicProcessesRepository, + periodicProcessesRepository: PeriodicProcessesRepository, schedulePropertyExtractor: SchedulePropertyExtractor, toClose: () => Unit )(implicit val ec: ExecutionContext) @@ -110,7 +93,7 @@ class PeriodicDeploymentManager private[periodic] ( with ManagerSpecificScenarioActivitiesStoredByManager with LazyLogging { - import repository._ + import periodicProcessesRepository._ override def processCommand[Result](command: DMScenarioCommand[Result]): Future[Result] = command match { @@ -158,12 +141,27 @@ class PeriodicDeploymentManager private[periodic] ( private def extractScheduleProperty(canonicalProcess: CanonicalProcess): Future[ScheduleProperty] = { schedulePropertyExtractor(canonicalProcess) match { case Right(scheduleProperty) => - Future.successful(scheduleProperty) + Future.successful(toDomain(scheduleProperty)) case Left(error) => Future.failed(new PeriodicProcessException(error)) } } + private def toDomain( + apiScheduleProperty: ApiScheduleProperty, + ): ScheduleProperty = apiScheduleProperty match { + case property: ApiScheduleProperty.SingleScheduleProperty => + toDomain(property) + case ApiScheduleProperty.MultipleScheduleProperty(schedules) => + MultipleScheduleProperty(schedules.map { case (k, v) => (k, toDomain(v)) }) + } + + private def toDomain( + apiSingleScheduleProperty: ApiScheduleProperty.SingleScheduleProperty + ): SingleScheduleProperty = apiSingleScheduleProperty match { + case ApiScheduleProperty.CronScheduleProperty(labelOrCronExpr) => CronScheduleProperty(labelOrCronExpr) + } + private def stopScenario(command: DMStopScenarioCommand): Future[SavepointResult] = { import command._ service.deactivate(scenarioName).flatMap { deploymentIdsToStop => @@ -282,9 +280,10 @@ class PeriodicDeploymentManager private[periodic] ( .map(_.groupedByPeriodicProcess.headOption.flatMap(_.deployments.headOption)) ) processDeploymentWithProcessJson <- OptionT.liftF( - repository.findProcessData(processDeployment.id).run + periodicProcessesRepository.findProcessData(processDeployment.id).run ) _ <- OptionT.liftF(service.deploy(processDeploymentWithProcessJson)) } yield () + override def schedulingSupport: SchedulingSupport = NoSchedulingSupport } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicDeploymentManagerDecorator.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicDeploymentManagerDecorator.scala new file mode 100644 index 00000000000..e4b36398383 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicDeploymentManagerDecorator.scala @@ -0,0 +1,117 @@ +package pl.touk.nussknacker.ui.process.periodic + +import com.typesafe.config.Config +import com.typesafe.scalalogging.LazyLogging +import pl.touk.nussknacker.engine.api.component.ScenarioPropertyConfig +import pl.touk.nussknacker.engine.api.definition.{MandatoryParameterValidator, StringParameterEditor} +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.{ + EmptyScheduledProcessListenerFactory, + ProcessConfigEnricherFactory, + SchedulePropertyExtractorFactory +} +import pl.touk.nussknacker.engine.api.deployment.{DeploymentManager, SchedulingSupported} +import pl.touk.nussknacker.engine.{DeploymentManagerDependencies, ModelData} +import pl.touk.nussknacker.ui.db.DbRef +import pl.touk.nussknacker.ui.process.periodic.cron.{CronParameterValidator, CronSchedulePropertyExtractor} +import pl.touk.nussknacker.ui.process.periodic.legacy.db.{LegacyDbInitializer, SlickLegacyPeriodicProcessesRepository} +import pl.touk.nussknacker.ui.process.repository.{ + DBFetchingProcessRepository, + DbScenarioActionReadOnlyRepository, + ScenarioLabelsRepository, + SlickPeriodicProcessesRepository +} +import slick.jdbc +import slick.jdbc.JdbcProfile + +import java.time.Clock + +object PeriodicDeploymentManagerDecorator extends LazyLogging { + + def decorate( + underlying: DeploymentManager, + schedulingSupported: SchedulingSupported, + modelData: ModelData, + deploymentConfig: Config, + dependencies: DeploymentManagerDependencies, + dbRef: DbRef, + ): DeploymentManager = { + logger.info("Decorating DM with periodic functionality") + import dependencies._ + import net.ceedubs.ficus.Ficus._ + import net.ceedubs.ficus.readers.ArbitraryTypeReader._ + + val clock = Clock.systemDefaultZone() + + deploymentConfig.as[SchedulingConfig]("scheduling") + val rawSchedulingConfig = deploymentConfig.getConfig("scheduling") + val schedulingConfig = rawSchedulingConfig.as[SchedulingConfig] + + val schedulePropertyExtractorFactory: SchedulePropertyExtractorFactory = + schedulingSupported.customSchedulePropertyExtractorFactory + .getOrElse(_ => CronSchedulePropertyExtractor()) + + val processConfigEnricherFactory = + schedulingSupported.customProcessConfigEnricherFactory + .getOrElse(ProcessConfigEnricherFactory.noOp) + + val periodicProcessListenerFactory = + schedulingSupported.customScheduledProcessListenerFactory + .getOrElse(EmptyScheduledProcessListenerFactory) + + val additionalDeploymentDataProvider = + schedulingSupported.customAdditionalDeploymentDataProvider + .getOrElse(DefaultAdditionalDeploymentDataProvider) + + val actionRepository = + DbScenarioActionReadOnlyRepository.create(dbRef) + val scenarioLabelsRepository = + new ScenarioLabelsRepository(dbRef) + val fetchingProcessRepository = + DBFetchingProcessRepository.createFutureRepository(dbRef, actionRepository, scenarioLabelsRepository) + + val periodicProcessesRepository = schedulingConfig.legacyDb match { + case None => + new SlickPeriodicProcessesRepository( + schedulingConfig.processingType, + dbRef.db, + dbRef.profile, + clock, + fetchingProcessRepository + ) + case Some(customDbConfig) => + val (db: jdbc.JdbcBackend.DatabaseDef, dbProfile: JdbcProfile) = LegacyDbInitializer.init(customDbConfig) + new SlickLegacyPeriodicProcessesRepository( + schedulingConfig.processingType, + db, + dbProfile, + clock, + fetchingProcessRepository + ) + } + + PeriodicDeploymentManager( + delegate = underlying, + dependencies = dependencies, + periodicProcessesRepository = periodicProcessesRepository, + scheduledExecutionPerformer = + schedulingSupported.createScheduledExecutionPerformer(modelData, dependencies, deploymentConfig), + schedulePropertyExtractorFactory = schedulePropertyExtractorFactory, + processConfigEnricherFactory = processConfigEnricherFactory, + listenerFactory = periodicProcessListenerFactory, + schedulingConfig = schedulingConfig, + originalConfig = deploymentConfig, + additionalDeploymentDataProvider = additionalDeploymentDataProvider, + ) + } + + def additionalScenarioProperties: Map[String, ScenarioPropertyConfig] = Map(cronConfig) + + private val cronConfig = CronSchedulePropertyExtractor.CronPropertyDefaultName -> ScenarioPropertyConfig( + defaultValue = None, + editor = Some(StringParameterEditor), + validators = Some(List(MandatoryParameterValidator, CronParameterValidator)), + label = Some("Schedule"), + hintText = Some("Quartz cron syntax. You can specify multiple schedulers separated by '|'.") + ) + +} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessException.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessException.scala similarity index 74% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessException.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessException.scala index 72a267b3317..53257f71403 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessException.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessException.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic class PeriodicProcessException(message: String, parent: Throwable) extends RuntimeException(message, parent) { def this(message: String) = this(message, null) diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessService.scala similarity index 74% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessService.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessService.scala index 43c41264a8f..9569e7d4ade 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessService.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic import cats.implicits._ import com.typesafe.scalalogging.LazyLogging @@ -10,29 +10,21 @@ import pl.touk.nussknacker.engine.api.component.{ } import pl.touk.nussknacker.engine.api.deployment.StateStatus.StatusName import pl.touk.nussknacker.engine.api.deployment._ +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{ScheduleProperty => _, _} +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{ScheduleProperty => ApiScheduleProperty} +import pl.touk.nussknacker.engine.api.deployment.scheduler.services._ import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus.ProblemStateStatus -import pl.touk.nussknacker.engine.api.process.{ProcessIdWithName, ProcessName} +import pl.touk.nussknacker.engine.api.process.{ProcessIdWithName, ProcessName, VersionId} import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.engine.deployment.{AdditionalModelConfigs, DeploymentData, DeploymentId} -import pl.touk.nussknacker.engine.management.periodic.PeriodicProcessService.{ - DeploymentStatus, - EngineStatusesToReschedule, - FinishedScheduledExecutionMetadata, - MaxDeploymentsStatus, - PeriodicProcessStatus -} -import pl.touk.nussknacker.engine.management.periodic.PeriodicStateStatus.{ScheduledStatus, WaitingForScheduleStatus} -import pl.touk.nussknacker.engine.management.periodic.db.PeriodicProcessesRepository -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData.{ - WithCanonicalProcess, - WithoutCanonicalProcess -} -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus -import pl.touk.nussknacker.engine.management.periodic.model._ -import pl.touk.nussknacker.engine.management.periodic.service._ -import pl.touk.nussknacker.engine.management.periodic.util.DeterministicUUIDFromLong import pl.touk.nussknacker.engine.util.AdditionalComponentConfigsForRuntimeExtractor +import pl.touk.nussknacker.ui.process.periodic.PeriodicProcessService._ +import pl.touk.nussknacker.ui.process.periodic.PeriodicStateStatus._ +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus +import pl.touk.nussknacker.ui.process.periodic.model._ +import pl.touk.nussknacker.ui.process.periodic.utils.DeterministicUUIDFromLong +import pl.touk.nussknacker.ui.process.repository.PeriodicProcessesRepository import java.time.chrono.ChronoLocalDateTime import java.time.temporal.ChronoUnit @@ -42,9 +34,9 @@ import scala.util.control.NonFatal class PeriodicProcessService( delegateDeploymentManager: DeploymentManager, - jarManager: JarManager, - scheduledProcessesRepository: PeriodicProcessesRepository, - periodicProcessListener: PeriodicProcessListener, + scheduledExecutionPerformer: ScheduledExecutionPerformer, + periodicProcessesRepository: PeriodicProcessesRepository, + periodicProcessListener: ScheduledProcessListener, additionalDeploymentDataProvider: AdditionalDeploymentDataProvider, deploymentRetryConfig: DeploymentRetryConfig, executionConfig: PeriodicExecutionConfig, @@ -52,19 +44,19 @@ class PeriodicProcessService( processConfigEnricher: ProcessConfigEnricher, clock: Clock, actionService: ProcessingTypeActionService, - configsFromProvider: Map[DesignerWideComponentId, ComponentAdditionalConfig] + configsFromProvider: Map[DesignerWideComponentId, ComponentAdditionalConfig], )(implicit ec: ExecutionContext) extends LazyLogging { import cats.syntax.all._ - import scheduledProcessesRepository._ - private type RepositoryAction[T] = scheduledProcessesRepository.Action[T] - private type Callback = () => Future[Unit] - private type NeedsReschedule = Boolean + import periodicProcessesRepository._ + + private type Callback = () => Future[Unit] + private type NeedsReschedule = Boolean - private implicit class WithCallbacksSeq(result: RepositoryAction[List[Callback]]) { + private implicit class WithCallbacksSeq(result: Future[List[Callback]]) { def runWithCallbacks: Future[Unit] = - result.run.flatMap(callbacks => Future.sequence(callbacks.map(_()))).map(_ => ()) + result.flatMap(callbacks => Future.sequence(callbacks.map(_()))).map(_ => ()) } private val emptyCallback: Callback = () => Future.successful(()) @@ -75,8 +67,11 @@ class PeriodicProcessService( processIdWithName: ProcessIdWithName, after: Option[Instant], ): Future[List[ScenarioActivity]] = for { - schedulesState <- scheduledProcessesRepository - .getSchedulesState(processIdWithName.name, after.map(localDateTimeAtSystemDefaultZone)) + schedulesState <- periodicProcessesRepository + .getSchedulesState( + processIdWithName.name, + after.map(localDateTimeAtSystemDefaultZone) + ) .run groupedByProcess = schedulesState.groupedByPeriodicProcess deployments = groupedByProcess.flatMap(_.deployments) @@ -91,7 +86,7 @@ class PeriodicProcessService( scenarioActivityId = ScenarioActivityId(DeterministicUUIDFromLong.longUUID(deployment.id.value)), user = ScenarioUser.internalNuUser, date = metadata.dateDeployed.getOrElse(metadata.dateFinished), - scenarioVersionId = Some(ScenarioVersionId.from(deployment.periodicProcess.processVersion.versionId)), + scenarioVersionId = Some(ScenarioVersionId.from(deployment.periodicProcess.deploymentData.versionId)), scheduledExecutionStatus = metadata.status, dateFinished = metadata.dateFinished, scheduleName = deployment.scheduleName.display, @@ -148,56 +143,70 @@ class PeriodicProcessService( processVersion: ProcessVersion, canonicalProcess: CanonicalProcess, scheduleDates: List[(ScheduleName, Option[LocalDateTime])], - processActionId: ProcessActionId + processActionId: ProcessActionId, ): Future[Unit] = { logger.info("Scheduling periodic scenario: {} on {}", processVersion, scheduleDates) + for { - deploymentWithJarData <- jarManager.prepareDeploymentWithJar(processVersion, canonicalProcess) + inputConfigDuringExecution <- scheduledExecutionPerformer.provideInputConfigDuringExecutionJson() + deploymentWithJarData <- scheduledExecutionPerformer.prepareDeploymentWithRuntimeParams( + processVersion, + ) enrichedProcessConfig <- processConfigEnricher.onInitialSchedule( - ProcessConfigEnricher.InitialScheduleData( - deploymentWithJarData.process, - deploymentWithJarData.inputConfigDuringExecutionJson - ) + ProcessConfigEnricher.InitialScheduleData(canonicalProcess, inputConfigDuringExecution.serialized) ) - enrichedDeploymentWithJarData = deploymentWithJarData.copy(inputConfigDuringExecutionJson = - enrichedProcessConfig.inputConfigDuringExecutionJson + _ <- initialSchedule( + scheduleProperty, + scheduleDates, + deploymentWithJarData, + canonicalProcess, + enrichedProcessConfig.inputConfigDuringExecutionJson, + processActionId, ) - _ <- initialSchedule(scheduleProperty, scheduleDates, enrichedDeploymentWithJarData, processActionId) } yield () } private def initialSchedule( scheduleMap: ScheduleProperty, scheduleDates: List[(ScheduleName, Option[LocalDateTime])], - deploymentWithJarData: DeploymentWithJarData.WithCanonicalProcess, - processActionId: ProcessActionId + deploymentWithJarData: DeploymentWithRuntimeParams, + canonicalProcess: CanonicalProcess, + inputConfigDuringExecutionJson: String, + processActionId: ProcessActionId, ): Future[Unit] = { - scheduledProcessesRepository - .create(deploymentWithJarData, scheduleMap, processActionId) + periodicProcessesRepository + .create( + deploymentWithJarData, + inputConfigDuringExecutionJson, + canonicalProcess, + scheduleMap, + processActionId + ) + .run .flatMap { process => scheduleDates.collect { case (name, Some(date)) => - scheduledProcessesRepository + periodicProcessesRepository .schedule(process.id, name, date, deploymentRetryConfig.deployMaxRetries) + .run .flatMap { data => - handleEvent(ScheduledEvent(data, firstSchedule = true)) + handleEvent(ScheduledEvent(data.toDetails, firstSchedule = true)) } case (name, None) => logger.warn(s"Schedule $name does not have date to schedule") - monad.pure(()) + Future.successful(()) }.sequence } - .run .map(_ => ()) } - def findToBeDeployed: Future[Seq[PeriodicProcessDeployment[WithCanonicalProcess]]] = { + def findToBeDeployed: Future[Seq[PeriodicProcessDeployment]] = { for { - toBeDeployed <- scheduledProcessesRepository.findToBeDeployed.run.flatMap { toDeployList => + toBeDeployed <- periodicProcessesRepository.findToBeDeployed.run.flatMap { toDeployList => Future.sequence(toDeployList.map(checkIfNotRunning)).map(_.flatten) } // We retry scenarios that failed on deployment. Failure recovery of running scenarios should be handled by Flink's restart strategy - toBeRetried <- scheduledProcessesRepository.findToBeRetried.run + toBeRetried <- periodicProcessesRepository.findToBeRetried.run // We don't block scheduled deployments by retries } yield toBeDeployed.sortBy(d => (d.runAt, d.createdAt)) ++ toBeRetried.sortBy(d => (d.nextRetryAt, d.createdAt)) } @@ -205,10 +214,10 @@ class PeriodicProcessService( // Currently we don't allow simultaneous runs of one scenario - only sequential, so if other schedule kicks in, it'll have to wait // TODO: we show allow to deploy scenarios with different scheduleName to be deployed simultaneous private def checkIfNotRunning( - toDeploy: PeriodicProcessDeployment[WithCanonicalProcess] - ): Future[Option[PeriodicProcessDeployment[WithCanonicalProcess]]] = { + toDeploy: PeriodicProcessDeployment + ): Future[Option[PeriodicProcessDeployment]] = { delegateDeploymentManager - .getProcessStates(toDeploy.periodicProcess.processVersion.processName)(DataFreshnessPolicy.Fresh) + .getProcessStates(toDeploy.periodicProcess.deploymentData.processName)(DataFreshnessPolicy.Fresh) .map( _.value .map(_.status) @@ -228,7 +237,7 @@ class PeriodicProcessService( schedules.groupedByPeriodicProcess .collect { case processScheduleData - if processScheduleData.existsDeployment(d => needRescheduleDeploymentIds.contains(d.id)) => + if processScheduleData.deployments.exists(d => needRescheduleDeploymentIds.contains(d.id)) => reschedule(processScheduleData, needRescheduleDeploymentIds) } .sequence @@ -236,9 +245,9 @@ class PeriodicProcessService( } for { - schedules <- scheduledProcessesRepository + schedules <- periodicProcessesRepository .findActiveSchedulesForProcessesHavingDeploymentWithMatchingStatus( - Set(PeriodicProcessDeploymentStatus.Deployed, PeriodicProcessDeploymentStatus.FailedOnDeploy) + Set(PeriodicProcessDeploymentStatus.Deployed, PeriodicProcessDeploymentStatus.FailedOnDeploy), ) .run // we handle each job separately, if we fail at some point, we will continue on next handleFinished run @@ -261,32 +270,40 @@ class PeriodicProcessService( s"Process '$processName' latest deployment ids: ${scheduleData.latestDeployments.map(_.id.toString)}" ) scheduleData.latestDeployments.map { deployment => - (deployment, runtimeStatuses.getStatus(deployment.id)) + ( + scheduleData.process.deploymentData.processName, + scheduleData.process.deploymentData.versionId, + deployment, + runtimeStatuses.getStatus(deployment.id) + ) } } _ = logger.debug( s"Process '$processName' schedule deployments with status: ${scheduleDeploymentsWithStatus.map(_.toString)}" ) needRescheduleDeployments <- Future - .sequence(scheduleDeploymentsWithStatus.map { case (deploymentData, statusOpt) => - synchronizeDeploymentState(deploymentData, statusOpt).run.map { needReschedule => + .sequence(scheduleDeploymentsWithStatus.map { case (processName, versionId, deploymentData, statusOpt) => + synchronizeDeploymentState(processName, versionId, deploymentData, statusOpt).map { needReschedule => Option(deploymentData.id).filter(_ => needReschedule) } }) .map(_.flatten.toSet) followingDeployDeploymentsForSchedules = scheduleDeploymentsWithStatus.collect { - case (deployment, Some(status)) if SimpleStateStatus.DefaultFollowingDeployStatuses.contains(status.status) => + case (_, _, deployment, Some(status)) + if SimpleStateStatus.DefaultFollowingDeployStatuses.contains(status.status) => deployment.id }.toSet } yield (followingDeployDeploymentsForSchedules, needRescheduleDeployments) // We assume that this method leaves with data in consistent state private def synchronizeDeploymentState( + processName: ProcessName, + versionId: VersionId, deployment: ScheduleDeploymentData, - processState: Option[StatusDetails] - ): RepositoryAction[NeedsReschedule] = { - implicit class RichRepositoryAction[Unit](a: RepositoryAction[Unit]) { - def needsReschedule(value: Boolean): RepositoryAction[NeedsReschedule] = a.map(_ => value) + processState: Option[StatusDetails], + ): Future[NeedsReschedule] = { + implicit class RichFuture[Unit](a: Future[Unit]) { + def needsReschedule(value: Boolean): Future[NeedsReschedule] = a.map(_ => value) } processState.map(_.status) match { case Some(status) @@ -298,7 +315,7 @@ class PeriodicProcessService( if EngineStatusesToReschedule.contains( status ) && deployment.state.status != PeriodicProcessDeploymentStatus.Finished => - markFinished(deployment, processState).needsReschedule(value = true) + markFinished(processName, versionId, deployment, processState).needsReschedule(value = true) case None if deployment.state.status == PeriodicProcessDeploymentStatus.Deployed && deployment.deployedAt.exists(_.isBefore(LocalDateTime.now().minusMinutes(5))) => @@ -306,26 +323,27 @@ class PeriodicProcessService( // this can be caused by a race in e.g. FlinkRestManager // (because /jobs/overview used in getProcessStates isn't instantly aware of submitted jobs) // so freshly deployed deployments aren't considered - markFinished(deployment, processState).needsReschedule(value = true) + markFinished(processName, versionId, deployment, processState).needsReschedule(value = true) case _ => - scheduledProcessesRepository.monad.pure(()).needsReschedule(value = false) + Future.successful(()).needsReschedule(value = false) } } private def reschedule( processScheduleData: PeriodicProcessScheduleData, needRescheduleDeploymentIds: Set[PeriodicProcessDeploymentId] - ): RepositoryAction[Callback] = { + ): Future[Callback] = { import processScheduleData._ val scheduleActions = deployments.map { deployment => if (needRescheduleDeploymentIds.contains(deployment.id)) - deployment.nextRunAt(clock) match { + nextRunAt(deployment, clock) match { case Right(Some(futureDate)) => logger.info(s"Rescheduling ${deployment.display} to $futureDate") - val action = scheduledProcessesRepository + val action = periodicProcessesRepository .schedule(process.id, deployment.scheduleName, futureDate, deploymentRetryConfig.deployMaxRetries) + .run .flatMap { data => - handleEvent(ScheduledEvent(data, firstSchedule = false)) + handleEvent(ScheduledEvent(data.toDetails, firstSchedule = false)) } Some(action) case Right(None) => @@ -338,7 +356,7 @@ class PeriodicProcessService( else Option(deployment) .filter(_.state.status == PeriodicProcessDeploymentStatus.Scheduled) - .map(_ => scheduledProcessesRepository.monad.pure(())) + .map(_ => Future.successful(())) } @@ -352,18 +370,45 @@ class PeriodicProcessService( scheduleActions.flatten.sequence.as(emptyCallback) } - private def markFinished(deployment: ScheduleDeploymentData, state: Option[StatusDetails]): RepositoryAction[Unit] = { + private def nextRunAt( + deployment: PeriodicProcessDeployment, + clock: Clock + ): Either[String, Option[LocalDateTime]] = + (deployment.periodicProcess.scheduleProperty, deployment.scheduleName.value) match { + case (MultipleScheduleProperty(schedules), Some(name)) => + schedules.get(name).toRight(s"Failed to find schedule: $deployment.scheduleName").flatMap(_.nextRunAt(clock)) + case (e: SingleScheduleProperty, None) => e.nextRunAt(clock) + case (schedule, name) => Left(s"Schedule name: $name mismatch with schedule: $schedule") + } + + private def markFinished( + processName: ProcessName, + versionId: VersionId, + deployment: ScheduleDeploymentData, + state: Option[StatusDetails], + ): Future[Unit] = { logger.info(s"Marking ${deployment.display} with status: ${deployment.state.status} as finished") for { - _ <- scheduledProcessesRepository.markFinished(deployment.id) - currentState <- scheduledProcessesRepository.findProcessData(deployment.id) - } yield handleEvent(FinishedEvent(currentState, state)) + _ <- periodicProcessesRepository.markFinished(deployment.id).run + currentState <- periodicProcessesRepository.findProcessData(deployment.id).run + canonicalProcessOpt <- periodicProcessesRepository + .fetchCanonicalProcessWithVersion( + processName, + versionId + ) + .map(_.map(_._1)) + canonicalProcess = canonicalProcessOpt.getOrElse { + throw new PeriodicProcessException( + s"Could not fetch CanonicalProcess with ProcessVersion for processName=$processName, versionId=$versionId" + ) + } + } yield handleEvent(FinishedEvent(currentState.toDetails, canonicalProcess, state)) } private def handleFailedDeployment( - deployment: PeriodicProcessDeployment[_], + deployment: PeriodicProcessDeployment, state: Option[StatusDetails] - ): RepositoryAction[Unit] = { + ): Future[Unit] = { def calculateNextRetryAt = now().plus(deploymentRetryConfig.deployRetryPenalize.toMillis, ChronoUnit.MILLIS) val retriesLeft = @@ -382,20 +427,20 @@ class PeriodicProcessService( ) for { - _ <- scheduledProcessesRepository.markFailedOnDeployWithStatus(deployment.id, status, retriesLeft, nextRetryAt) - currentState <- scheduledProcessesRepository.findProcessData(deployment.id) - } yield handleEvent(FailedOnDeployEvent(currentState, state)) + _ <- periodicProcessesRepository.markFailedOnDeployWithStatus(deployment.id, status, retriesLeft, nextRetryAt).run + currentState <- periodicProcessesRepository.findProcessData(deployment.id).run + } yield handleEvent(FailedOnDeployEvent(currentState.toDetails, state)) } private def markFailedAction( deployment: ScheduleDeploymentData, state: Option[StatusDetails] - ): RepositoryAction[Unit] = { + ): Future[Unit] = { logger.info(s"Marking ${deployment.display} as failed.") for { - _ <- scheduledProcessesRepository.markFailed(deployment.id) - currentState <- scheduledProcessesRepository.findProcessData(deployment.id) - } yield handleEvent(FailedOnRunEvent(currentState, state)) + _ <- periodicProcessesRepository.markFailed(deployment.id).run + currentState <- periodicProcessesRepository.findProcessData(deployment.id).run + } yield handleEvent(FailedOnRunEvent(currentState.toDetails, state)) } def deactivate(processName: ProcessName): Future[Iterable[DeploymentId]] = @@ -405,32 +450,34 @@ class PeriodicProcessService( _ <- activeSchedules.groupedByPeriodicProcess.map(p => deactivateAction(p.process)).sequence.runWithCallbacks } yield runningDeploymentsForSchedules.map(deployment => DeploymentId(deployment.toString)) - private def deactivateAction(process: PeriodicProcess[WithoutCanonicalProcess]): RepositoryAction[Callback] = { + private def deactivateAction( + process: PeriodicProcess + ): Future[Callback] = { logger.info(s"Deactivate periodic process id: ${process.id.value}") for { - _ <- scheduledProcessesRepository.markInactive(process.id) + _ <- periodicProcessesRepository.markInactive(process.id).run // we want to delete jars only after we successfully mark process as inactive. It's better to leave jar garbage than // have process without jar - } yield () => jarManager.deleteJar(process.deploymentData.jarFileName) + } yield () => scheduledExecutionPerformer.cleanAfterDeployment(process.deploymentData.runtimeParams) } private def markProcessActionExecutionFinished( processActionIdOption: Option[ProcessActionId] - ): RepositoryAction[Callback] = - scheduledProcessesRepository.monad.pure { () => + ): Future[Callback] = + Future.successful { () => processActionIdOption .map(actionService.markActionExecutionFinished) .sequence .map(_ => ()) } - def deploy(deployment: PeriodicProcessDeployment[WithCanonicalProcess]): Future[Unit] = { + def deploy(deployment: PeriodicProcessDeployment): Future[Unit] = { // TODO: set status before deployment? val id = deployment.id val deploymentData = DeploymentData( DeploymentId(id.toString), DeploymentData.systemUser, - additionalDeploymentDataProvider.prepareAdditionalData(deployment), + additionalDeploymentDataProvider.prepareAdditionalData(deployment.toDetails), // TODO: in the future we could allow users to specify nodes data during schedule requesting NodesDeploymentData.empty, AdditionalModelConfigs( @@ -440,42 +487,69 @@ class PeriodicProcessService( val deploymentWithJarData = deployment.periodicProcess.deploymentData val deploymentAction = for { _ <- Future.successful( - logger.info("Deploying scenario {} for deployment id {}", deploymentWithJarData.processVersion, id) + logger.info("Deploying scenario {} for deployment id {}", deploymentWithJarData, id) ) + processName = deploymentWithJarData.processName + versionId = deploymentWithJarData.versionId + canonicalProcessWithVersionOpt <- periodicProcessesRepository + .fetchCanonicalProcessWithVersion( + processName, + versionId + ) + canonicalProcessWithVersion = canonicalProcessWithVersionOpt.getOrElse { + throw new PeriodicProcessException( + s"Could not fetch CanonicalProcess with ProcessVersion for processName=$processName, versionId=$versionId" + ) + } + inputConfigDuringExecutionJsonOpt <- periodicProcessesRepository + .fetchInputConfigDuringExecutionJson( + processName, + versionId, + ) + .run + inputConfigDuringExecutionJson = inputConfigDuringExecutionJsonOpt.getOrElse { + throw new PeriodicProcessException( + s"Could not fetch inputConfigDuringExecutionJson for processName=${processName}, versionId=${versionId}" + ) + } enrichedProcessConfig <- processConfigEnricher.onDeploy( ProcessConfigEnricher.DeployData( - deploymentWithJarData.process, - deploymentWithJarData.inputConfigDuringExecutionJson, - deployment + canonicalProcessWithVersion._1, + canonicalProcessWithVersion._2, + inputConfigDuringExecutionJson, + deployment.toDetails ) ) - enrichedDeploymentWithJarData = deploymentWithJarData.copy(inputConfigDuringExecutionJson = - enrichedProcessConfig.inputConfigDuringExecutionJson + externalDeploymentId <- scheduledExecutionPerformer.deployWithRuntimeParams( + deploymentWithJarData, + enrichedProcessConfig.inputConfigDuringExecutionJson, + deploymentData, + canonicalProcessWithVersion._1, + canonicalProcessWithVersion._2, ) - externalDeploymentId <- jarManager.deployWithJar(enrichedDeploymentWithJarData, deploymentData) } yield externalDeploymentId deploymentAction .flatMap { externalDeploymentId => - logger.info("Scenario has been deployed {} for deployment id {}", deploymentWithJarData.processVersion, id) + logger.info("Scenario has been deployed {} for deployment id {}", deploymentWithJarData, id) // TODO: add externalDeploymentId?? - scheduledProcessesRepository + periodicProcessesRepository .markDeployed(id) - .flatMap(_ => scheduledProcessesRepository.findProcessData(id)) - .flatMap(afterChange => handleEvent(DeployedEvent(afterChange, externalDeploymentId))) .run + .flatMap(_ => periodicProcessesRepository.findProcessData(id).run) + .flatMap(afterChange => handleEvent(DeployedEvent(afterChange.toDetails, externalDeploymentId))) } // We can recover since deployment actor watches only future completion. .recoverWith { case exception => logger.error(s"Scenario deployment ${deployment.display} failed", exception) - handleFailedDeployment(deployment, None).run + handleFailedDeployment(deployment, None) } } // TODO: allow access to DB in listener? - private def handleEvent(event: PeriodicProcessEvent): scheduledProcessesRepository.Action[Unit] = { - scheduledProcessesRepository.monad.pure { + private def handleEvent(event: ScheduledProcessEvent): Future[Unit] = { + Future.successful { try { - periodicProcessListener.onPeriodicProcessEvent.applyOrElse(event, (_: PeriodicProcessEvent) => ()) + periodicProcessListener.onScheduledProcessEvent.applyOrElse(event, (_: ScheduledProcessEvent) => ()) } catch { case NonFatal(e) => throw new PeriodicProcessException("Failed to invoke listener", e) } @@ -523,7 +597,7 @@ class PeriodicProcessService( def toDeploymentStatuses(schedulesState: SchedulesState) = schedulesState.schedules.toList .flatMap { case (scheduleId, scheduleData) => scheduleData.latestDeployments.map { deployment => - DeploymentStatus( + PeriodicDeploymentStatus( deployment.id, scheduleId, deployment.createdAt, @@ -534,7 +608,7 @@ class PeriodicProcessService( ) } } - .sorted(DeploymentStatus.ordering.reverse) + .sorted(PeriodicDeploymentStatus.ordering.reverse) for { activeSchedules <- getLatestDeploymentsForActiveSchedules(name, MaxDeploymentsStatus) @@ -556,7 +630,7 @@ class PeriodicProcessService( schedulesState.schedules.toList .flatMap { case (scheduleId, scheduleData) => scheduleData.latestDeployments.map { deployment => - DeploymentStatus( + PeriodicDeploymentStatus( deployment.id, scheduleId, deployment.createdAt, @@ -567,7 +641,7 @@ class PeriodicProcessService( ) } } - .sorted(DeploymentStatus.ordering.reverse) + .sorted(PeriodicDeploymentStatus.ordering.reverse) for { activeSchedules <- getLatestDeploymentsForActiveSchedules(MaxDeploymentsStatus) @@ -591,23 +665,28 @@ class PeriodicProcessService( processName: ProcessName, deploymentsPerScheduleMaxCount: Int = 1 ): Future[SchedulesState] = - scheduledProcessesRepository.getLatestDeploymentsForActiveSchedules(processName, deploymentsPerScheduleMaxCount).run + periodicProcessesRepository + .getLatestDeploymentsForActiveSchedules( + processName, + deploymentsPerScheduleMaxCount, + ) + .run def getLatestDeploymentsForActiveSchedules( deploymentsPerScheduleMaxCount: Int ): Future[Map[ProcessName, SchedulesState]] = - scheduledProcessesRepository.getLatestDeploymentsForActiveSchedules(deploymentsPerScheduleMaxCount).run + periodicProcessesRepository.getLatestDeploymentsForActiveSchedules(deploymentsPerScheduleMaxCount).run def getLatestDeploymentsForLatestInactiveSchedules( processName: ProcessName, inactiveProcessesMaxCount: Int, deploymentsPerScheduleMaxCount: Int ): Future[SchedulesState] = - scheduledProcessesRepository + periodicProcessesRepository .getLatestDeploymentsForLatestInactiveSchedules( processName, inactiveProcessesMaxCount, - deploymentsPerScheduleMaxCount + deploymentsPerScheduleMaxCount, ) .run @@ -615,7 +694,7 @@ class PeriodicProcessService( inactiveProcessesMaxCount: Int, deploymentsPerScheduleMaxCount: Int ): Future[Map[ProcessName, SchedulesState]] = - scheduledProcessesRepository + periodicProcessesRepository .getLatestDeploymentsForLatestInactiveSchedules( inactiveProcessesMaxCount, deploymentsPerScheduleMaxCount @@ -631,7 +710,7 @@ class PeriodicProcessService( } private def scheduledExecutionStatusAndDateFinished( - entity: PeriodicProcessDeployment[WithoutCanonicalProcess], + entity: PeriodicProcessDeployment, ): Option[FinishedScheduledExecutionMetadata] = { for { status <- entity.state.status match { @@ -688,15 +767,15 @@ object PeriodicProcessService { // of single, merged status similar to this available for streaming job. This merged status should be a straightforward derivative // of these deployments statuses so it will be easy to figure out it by user. case class PeriodicProcessStatus( - activeDeploymentsStatuses: List[DeploymentStatus], - inactiveDeploymentsStatuses: List[DeploymentStatus] + activeDeploymentsStatuses: List[PeriodicDeploymentStatus], + inactiveDeploymentsStatuses: List[PeriodicDeploymentStatus] ) extends StateStatus with LazyLogging { - def limitedAndSortedDeployments: List[DeploymentStatus] = + def limitedAndSortedDeployments: List[PeriodicDeploymentStatus] = (activeDeploymentsStatuses ++ inactiveDeploymentsStatuses.take( MaxDeploymentsStatus - activeDeploymentsStatuses.size - )).sorted(DeploymentStatus.ordering.reverse) + )).sorted(PeriodicDeploymentStatus.ordering.reverse) // We present merged name to be possible to filter scenario by status override def name: StatusName = mergedStatusDetails.status.name @@ -761,7 +840,7 @@ object PeriodicProcessService { * should be deactivated earlier. * */ - def pickMostImportantActiveDeployment: Option[DeploymentStatus] = { + def pickMostImportantActiveDeployment: Option[PeriodicDeploymentStatus] = { val lastActiveDeploymentStatusForEachSchedule = latestDeploymentForEachSchedule(activeDeploymentsStatuses).sorted @@ -779,17 +858,17 @@ object PeriodicProcessService { .orElse(last(PeriodicProcessDeploymentStatus.Finished)) } - private def latestDeploymentForEachSchedule(deploymentsStatuses: List[DeploymentStatus]) = { + private def latestDeploymentForEachSchedule(deploymentsStatuses: List[PeriodicDeploymentStatus]) = { deploymentsStatuses .groupBy(_.scheduleId) .values .toList - .map(_.min(DeploymentStatus.ordering.reverse)) + .map(_.min(PeriodicDeploymentStatus.ordering.reverse)) } } - case class DeploymentStatus( // Probably it is too much technical to present to users, but the only other alternative + case class PeriodicDeploymentStatus( // Probably it is too much technical to present to users, but the only other alternative // to present to users is scheduleName+runAt deploymentId: PeriodicProcessDeploymentId, scheduleId: ScheduleId, @@ -820,14 +899,15 @@ object PeriodicProcessService { } - object DeploymentStatus { + object PeriodicDeploymentStatus { - implicit val ordering: Ordering[DeploymentStatus] = (self: DeploymentStatus, that: DeploymentStatus) => { - self.runAt.compareTo(that.runAt) match { - case 0 => self.createdAt.compareTo(that.createdAt) - case a => a + implicit val ordering: Ordering[PeriodicDeploymentStatus] = + (self: PeriodicDeploymentStatus, that: PeriodicDeploymentStatus) => { + self.runAt.compareTo(that.runAt) match { + case 0 => self.createdAt.compareTo(that.createdAt) + case a => a + } } - } } diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessStateDefinitionManager.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessStateDefinitionManager.scala similarity index 80% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessStateDefinitionManager.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessStateDefinitionManager.scala index 008aabcca05..17c3642a8c8 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessStateDefinitionManager.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessStateDefinitionManager.scala @@ -1,13 +1,8 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic import pl.touk.nussknacker.engine.api.deployment.ProcessStateDefinitionManager.defaultVisibleActions -import pl.touk.nussknacker.engine.api.deployment.{ - OverridingProcessStateDefinitionManager, - ProcessStateDefinitionManager, - ScenarioActionName, - StateStatus -} -import pl.touk.nussknacker.engine.management.periodic.PeriodicProcessService.{DeploymentStatus, PeriodicProcessStatus} +import pl.touk.nussknacker.engine.api.deployment.{OverridingProcessStateDefinitionManager, ProcessStateDefinitionManager, ScenarioActionName, StateStatus} +import pl.touk.nussknacker.ui.process.periodic.PeriodicProcessService.{PeriodicDeploymentStatus, PeriodicProcessStatus} class PeriodicProcessStateDefinitionManager(delegate: ProcessStateDefinitionManager) extends OverridingProcessStateDefinitionManager( @@ -33,7 +28,7 @@ object PeriodicProcessStateDefinitionManager { def statusTooltip(processStatus: PeriodicProcessStatus): String = { processStatus.limitedAndSortedDeployments - .map { case d @ DeploymentStatus(_, scheduleId, _, runAt, status, _, _) => + .map { case d @ PeriodicDeploymentStatus(_, scheduleId, _, runAt, status, _, _) => val refinedStatus = { if (d.isCanceled) { "Canceled" diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicStateStatus.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicStateStatus.scala similarity index 98% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicStateStatus.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicStateStatus.scala index a90c0cee3fb..ce47c08485d 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicStateStatus.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicStateStatus.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic import pl.touk.nussknacker.engine.api.deployment.ProcessStateDefinitionManager.ProcessStatus import pl.touk.nussknacker.engine.api.deployment.StateStatus.StatusName diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/RescheduleFinishedActor.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/RescheduleFinishedActor.scala similarity index 90% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/RescheduleFinishedActor.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/RescheduleFinishedActor.scala index d53b95c95c0..33d240de627 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/RescheduleFinishedActor.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/RescheduleFinishedActor.scala @@ -1,8 +1,8 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic import akka.actor.{Actor, Props, Timers} import com.typesafe.scalalogging.LazyLogging -import pl.touk.nussknacker.engine.management.periodic.RescheduleFinishedActor.{CheckStates, CheckStatesCompleted} +import pl.touk.nussknacker.ui.process.periodic.RescheduleFinishedActor.{CheckStates, CheckStatesCompleted} import scala.concurrent.Future import scala.concurrent.duration._ diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/SingleScheduleProperty.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/ScheduleProperty.scala similarity index 95% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/SingleScheduleProperty.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/ScheduleProperty.scala index 97b7aa80af9..86f01f37130 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/SingleScheduleProperty.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/ScheduleProperty.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic import com.cronutils.model.definition.CronDefinitionBuilder import com.cronutils.model.time.ExecutionTime @@ -34,7 +34,7 @@ object SingleScheduleProperty { @JsonCodec case class CronScheduleProperty(labelOrCronExpr: String) extends SingleScheduleProperty { import cats.implicits._ - import pl.touk.nussknacker.engine.management.periodic.CronScheduleProperty._ + import pl.touk.nussknacker.ui.process.periodic.CronScheduleProperty._ private lazy val cronsOrError: Either[String, List[Cron]] = { val (errors, crons) = labelOrCronExpr diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicBatchConfig.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/SchedulingConfig.scala similarity index 71% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicBatchConfig.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/SchedulingConfig.scala index 61b8f9bb1ac..d8b6f8df610 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicBatchConfig.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/SchedulingConfig.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic import com.typesafe.config.Config @@ -7,22 +7,23 @@ import scala.concurrent.duration._ /** * Periodic Flink scenarios deployment configuration. * - * @param db Nussknacker db configuration. + * @param legacyDb Optional custom db, that will be used instead of main Nussknacker DB. Will be removed in the future. * @param processingType processing type of scenarios to be managed by this instance of the periodic engine. * @param rescheduleCheckInterval {@link RescheduleFinishedActor} check interval. * @param deployInterval {@link DeploymentActor} check interval. * @param deploymentRetry {@link DeploymentRetryConfig} for deployment failure recovery. - * @param jarsDir Directory for jars storage. * @param maxFetchedPeriodicScenarioActivities Optional limit of number of latest periodic-related Scenario Activities that are returned by Periodic DM. */ -case class PeriodicBatchConfig( - db: Config, +case class SchedulingConfig( + legacyDb: Option[Config], + // The `processingType` value should be removed in the future, because it should correspond to the real value of processingType. + // But at the moment it may not be equal to the value of processingType of the DM that uses scheduling mechanism. + // Therefore, we must keep the separate value SchedulingConfig, until we ensure consistency between the real processingType and the one defined here. processingType: String, rescheduleCheckInterval: FiniteDuration = 13 seconds, deployInterval: FiniteDuration = 17 seconds, deploymentRetry: DeploymentRetryConfig, executionConfig: PeriodicExecutionConfig, - jarsDir: String, maxFetchedPeriodicScenarioActivities: Option[Int] = Some(200), ) diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/Utils.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/Utils.scala similarity index 95% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/Utils.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/Utils.scala index b3fe622b411..a3ba8073295 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/Utils.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/Utils.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic import akka.actor.{ActorRef, ActorSystem, Props} import com.typesafe.scalalogging.LazyLogging diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/cron/CronParameterValidator.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/cron/CronParameterValidator.scala similarity index 63% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/cron/CronParameterValidator.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/cron/CronParameterValidator.scala index 9eddcdbde26..18f11368259 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/cron/CronParameterValidator.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/cron/CronParameterValidator.scala @@ -1,27 +1,17 @@ -package pl.touk.nussknacker.engine.management.periodic.cron +package pl.touk.nussknacker.ui.process.periodic.cron import cats.data.Validated import cats.data.Validated.{invalid, valid} import pl.touk.nussknacker.engine.api import pl.touk.nussknacker.engine.api.context.PartSubGraphCompilationError import pl.touk.nussknacker.engine.api.context.ProcessCompilationError.CustomParameterValidationError -import pl.touk.nussknacker.engine.api.definition.{ - CustomParameterValidator, - CustomParameterValidatorDelegate, - ParameterValidator -} +import pl.touk.nussknacker.engine.api.definition.CustomParameterValidatorDelegate import pl.touk.nussknacker.engine.api.parameter.ParameterName import pl.touk.nussknacker.engine.graph.expression.Expression -import pl.touk.nussknacker.engine.management.periodic.SchedulePropertyExtractor - -object CronParameterValidator extends CronParameterValidator { - - def delegate: ParameterValidator = CustomParameterValidatorDelegate(name) - -} +import pl.touk.nussknacker.ui.process.periodic.utils.SchedulePropertyExtractorUtils // Valid expression is e.g.: 0 * * * * ? * which means run every minute at 0 second -class CronParameterValidator extends CustomParameterValidator { +object CronParameterValidator extends CustomParameterValidatorDelegate("cron_validator") { override def isValid(paramName: ParameterName, expression: Expression, value: Option[Any], label: Option[String])( implicit nodeId: api.NodeId @@ -36,12 +26,12 @@ class CronParameterValidator extends CustomParameterValidator { } value match { case Some(s: String) => - SchedulePropertyExtractor.parseAndValidateProperty(s).fold(_ => invalid(createValidationError), _ => valid(())) + SchedulePropertyExtractorUtils + .parseAndValidateProperty(s) + .fold(_ => invalid(createValidationError), _ => valid(())) case _ => invalid(createValidationError) } } - override def name: String = "cron_validator" - } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/cron/CronSchedulePropertyExtractor.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/cron/CronSchedulePropertyExtractor.scala new file mode 100644 index 00000000000..96294b9a08a --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/cron/CronSchedulePropertyExtractor.scala @@ -0,0 +1,34 @@ +package pl.touk.nussknacker.ui.process.periodic.cron + +import com.typesafe.scalalogging.LazyLogging +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{ScheduleProperty => ApiScheduleProperty} +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.SchedulePropertyExtractor +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess +import pl.touk.nussknacker.ui.process.periodic.cron.CronSchedulePropertyExtractor.CronPropertyDefaultName +import pl.touk.nussknacker.ui.process.periodic.utils.SchedulePropertyExtractorUtils +import pl.touk.nussknacker.ui.process.periodic.{CronScheduleProperty, MultipleScheduleProperty, SingleScheduleProperty} + +object CronSchedulePropertyExtractor { + val CronPropertyDefaultName = "cron" +} + +case class CronSchedulePropertyExtractor(propertyName: String = CronPropertyDefaultName) + extends SchedulePropertyExtractor + with LazyLogging { + + override def apply(canonicalProcess: CanonicalProcess): Either[String, ApiScheduleProperty] = { + SchedulePropertyExtractorUtils.extractProperty(canonicalProcess, propertyName).map { + case MultipleScheduleProperty(schedules) => + ApiScheduleProperty.MultipleScheduleProperty(schedules.map { case (k, v) => (k, toApi(v)) }) + case cronProperty: CronScheduleProperty => + toApi(cronProperty) + } + } + + private def toApi(singleProperty: SingleScheduleProperty): ApiScheduleProperty.SingleScheduleProperty = { + singleProperty match { + case CronScheduleProperty(labelOrCronExpr) => ApiScheduleProperty.CronScheduleProperty(labelOrCronExpr) + } + } + +} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/DbInitializer.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyDbInitializer.scala similarity index 95% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/DbInitializer.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyDbInitializer.scala index bb65cef5794..0dbaa7fd6a3 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/DbInitializer.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyDbInitializer.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic.db +package pl.touk.nussknacker.ui.process.periodic.legacy.db import com.github.tminglei.slickpg.ExPostgresProfile import com.typesafe.config.Config @@ -9,7 +9,7 @@ import org.flywaydb.core.api.configuration.FluentConfiguration import org.flywaydb.core.internal.database.postgresql.PostgreSQLDatabaseType import slick.jdbc.{HsqldbProfile, JdbcBackend, JdbcProfile, PostgresProfile} -object DbInitializer extends LazyLogging { +object LegacyDbInitializer extends LazyLogging { def init(configDb: Config): (JdbcBackend.DatabaseDef, JdbcProfile) = { import net.ceedubs.ficus.Ficus._ diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyPeriodicProcessDeploymentsTableFactory.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyPeriodicProcessDeploymentsTableFactory.scala new file mode 100644 index 00000000000..7518b5cda0a --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyPeriodicProcessDeploymentsTableFactory.scala @@ -0,0 +1,76 @@ +package pl.touk.nussknacker.ui.process.periodic.legacy.db + +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus +import pl.touk.nussknacker.ui.process.periodic.model.{PeriodicProcessDeploymentId, PeriodicProcessDeploymentStatus, PeriodicProcessId} +import slick.jdbc.{JdbcProfile, JdbcType} +import slick.lifted.ProvenShape +import slick.sql.SqlProfile.ColumnOption.NotNull + +import java.time.LocalDateTime + +trait LegacyPeriodicProcessDeploymentsTableFactory extends LegacyPeriodicProcessesTableFactory { + + protected val profile: JdbcProfile + + import profile.api._ + + implicit val periodicProcessDeploymentIdMapping: BaseColumnType[PeriodicProcessDeploymentId] = + MappedColumnType.base[PeriodicProcessDeploymentId, Long](_.value, PeriodicProcessDeploymentId.apply) + + implicit val periodicProcessDeploymentStatusColumnTyped: JdbcType[PeriodicProcessDeploymentStatus] = + MappedColumnType.base[PeriodicProcessDeploymentStatus, String](_.toString, PeriodicProcessDeploymentStatus.withName) + + class PeriodicProcessDeploymentsTable(tag: Tag) + extends Table[PeriodicProcessDeploymentEntity](tag, "periodic_process_deployments") { + + def id: Rep[PeriodicProcessDeploymentId] = column[PeriodicProcessDeploymentId]("id", O.PrimaryKey, O.AutoInc) + + def periodicProcessId: Rep[PeriodicProcessId] = column[PeriodicProcessId]("periodic_process_id", NotNull) + + def createdAt: Rep[LocalDateTime] = column[LocalDateTime]("created_at", NotNull) + + def runAt: Rep[LocalDateTime] = column[LocalDateTime]("run_at", NotNull) + + def scheduleName: Rep[Option[String]] = column[Option[String]]("schedule_name") + + def deployedAt: Rep[Option[LocalDateTime]] = column[Option[LocalDateTime]]("deployed_at") + + def completedAt: Rep[Option[LocalDateTime]] = column[Option[LocalDateTime]]("completed_at") + + def retriesLeft: Rep[Int] = column[Int]("retries_left") + + def nextRetryAt: Rep[Option[LocalDateTime]] = column[Option[LocalDateTime]]("next_retry_at") + + def status: Rep[PeriodicProcessDeploymentStatus] = column[PeriodicProcessDeploymentStatus]("status", NotNull) + + override def * : ProvenShape[PeriodicProcessDeploymentEntity] = ( + id, + periodicProcessId, + createdAt, + runAt, + scheduleName, + deployedAt, + completedAt, + retriesLeft, + nextRetryAt, + status + ) <> + ((PeriodicProcessDeploymentEntity.apply _).tupled, PeriodicProcessDeploymentEntity.unapply) + + } + + object PeriodicProcessDeployments extends TableQuery(new PeriodicProcessDeploymentsTable(_)) +} + +case class PeriodicProcessDeploymentEntity( + id: PeriodicProcessDeploymentId, + periodicProcessId: PeriodicProcessId, + createdAt: LocalDateTime, + runAt: LocalDateTime, + scheduleName: Option[String], + deployedAt: Option[LocalDateTime], + completedAt: Option[LocalDateTime], + retriesLeft: Int, + nextRetryAt: Option[LocalDateTime], + status: PeriodicProcessDeploymentStatus +) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyPeriodicProcessesRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyPeriodicProcessesRepository.scala new file mode 100644 index 00000000000..d747be320b2 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyPeriodicProcessesRepository.scala @@ -0,0 +1,498 @@ +package pl.touk.nussknacker.ui.process.periodic.legacy.db + +import cats.Monad +import com.github.tminglei.slickpg.ExPostgresProfile +import com.typesafe.scalalogging.LazyLogging +import db.util.DBIOActionInstances.DB +import io.circe.parser.decode +import io.circe.syntax.EncoderOps +import pl.touk.nussknacker.engine.api.ProcessVersion +import pl.touk.nussknacker.engine.api.deployment.ProcessActionId +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{DeploymentWithRuntimeParams, RuntimeParams} +import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess +import pl.touk.nussknacker.engine.management.FlinkScheduledExecutionPerformer.jarFileNameRuntimeParam +import pl.touk.nussknacker.ui.process.periodic.ScheduleProperty +import pl.touk.nussknacker.ui.process.periodic.legacy.db.LegacyPeriodicProcessesRepository.createPeriodicProcess +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus +import pl.touk.nussknacker.ui.process.periodic.model._ +import pl.touk.nussknacker.ui.process.repository.{FetchingProcessRepository, PeriodicProcessesRepository} +import pl.touk.nussknacker.ui.security.api.NussknackerInternalUser +import slick.dbio.{DBIOAction, Effect, NoStream} +import slick.jdbc.PostgresProfile.api._ +import slick.jdbc.{JdbcBackend, JdbcProfile} + +import java.time.{Clock, LocalDateTime} +import scala.concurrent.{ExecutionContext, Future} +import scala.language.higherKinds + +object LegacyPeriodicProcessesRepository { + + def createPeriodicProcessDeployment( + processEntity: PeriodicProcessEntity, + processDeploymentEntity: PeriodicProcessDeploymentEntity + ): PeriodicProcessDeployment = { + val process = createPeriodicProcess(processEntity) + PeriodicProcessDeployment( + processDeploymentEntity.id, + process, + processDeploymentEntity.createdAt, + processDeploymentEntity.runAt, + ScheduleName(processDeploymentEntity.scheduleName), + processDeploymentEntity.retriesLeft, + processDeploymentEntity.nextRetryAt, + createPeriodicDeploymentState(processDeploymentEntity) + ) + } + + def createPeriodicDeploymentState( + processDeploymentEntity: PeriodicProcessDeploymentEntity + ): PeriodicProcessDeploymentState = { + PeriodicProcessDeploymentState( + processDeploymentEntity.deployedAt, + processDeploymentEntity.completedAt, + processDeploymentEntity.status + ) + } + + def createPeriodicProcess( + processEntity: PeriodicProcessEntity + ): PeriodicProcess = { + val scheduleProperty = prepareScheduleProperty(processEntity) + PeriodicProcess( + processEntity.id, + DeploymentWithRuntimeParams( + processId = None, + processName = processEntity.processName, + versionId = processEntity.processVersionId, + runtimeParams = RuntimeParams(Map(jarFileNameRuntimeParam -> processEntity.jarFileName)), + ), + scheduleProperty, + processEntity.active, + processEntity.createdAt, + processEntity.processActionId + ) + } + + private def prepareScheduleProperty(processEntity: PeriodicProcessEntity) = { + val scheduleProperty = decode[ScheduleProperty](processEntity.scheduleProperty) + .fold(e => throw new IllegalArgumentException(e), identity) + scheduleProperty + } + +} + +class SlickLegacyPeriodicProcessesRepository( + processingType: String, + db: JdbcBackend.DatabaseDef, + override val profile: JdbcProfile, + clock: Clock, + fetchingProcessRepository: FetchingProcessRepository[Future], +)(implicit ec: ExecutionContext) + extends PeriodicProcessesRepository + with LegacyPeriodicProcessesTableFactory + with LegacyPeriodicProcessDeploymentsTableFactory + with LazyLogging { + + import pl.touk.nussknacker.engine.util.Implicits._ + + type Action[T] = DBIOActionInstances.DB[T] + + override def run[T](action: DBIOAction[T, NoStream, Effect.All]): Future[T] = db.run(action.transactionally) + + override def getSchedulesState( + scenarioName: ProcessName, + afterOpt: Option[LocalDateTime], + ): Action[SchedulesState] = { + PeriodicProcessesWithoutJson + .filter(_.processName === scenarioName) + .join(PeriodicProcessDeployments) + .on(_.id === _.periodicProcessId) + .filterOpt(afterOpt)((entities, after) => entities._2.completedAt > after) + .result + .map(toSchedulesStateForSinglePeriodicProcess) + } + + override def create( + deploymentWithRuntimeParams: DeploymentWithRuntimeParams, + inputConfigDuringExecutionJson: String, + canonicalProcess: CanonicalProcess, + scheduleProperty: ScheduleProperty, + processActionId: ProcessActionId, + ): Action[PeriodicProcess] = { + val jarFileName = deploymentWithRuntimeParams.runtimeParams.params.getOrElse( + jarFileNameRuntimeParam, + throw new RuntimeException(s"jarFileName runtime param not present") + ) + val processEntity = PeriodicProcessEntityWithJson( + id = PeriodicProcessId(-1), + processName = deploymentWithRuntimeParams.processName, + processVersionId = deploymentWithRuntimeParams.versionId, + processingType = processingType, + jarFileName = jarFileName, + scheduleProperty = scheduleProperty.asJson.noSpaces, + active = true, + createdAt = now(), + processActionId = Some(processActionId), + inputConfigDuringExecutionJson = inputConfigDuringExecutionJson, + processJson = canonicalProcess, + ) + ((PeriodicProcessesWithJson returning PeriodicProcessesWithJson into ((_, id) => id)) += processEntity) + .map(LegacyPeriodicProcessesRepository.createPeriodicProcess) + } + + private def now(): LocalDateTime = LocalDateTime.now(clock) + + override def findToBeDeployed: Action[Seq[PeriodicProcessDeployment]] = + findProcesses( + activePeriodicProcessWithDeploymentQuery(processingType) + .filter { case (_, d) => + d.runAt <= now() && + d.status === (PeriodicProcessDeploymentStatus.Scheduled: PeriodicProcessDeploymentStatus) + } + ) + + override def findToBeRetried: Action[Seq[PeriodicProcessDeployment]] = + findProcesses( + activePeriodicProcessWithDeploymentQuery(processingType) + .filter { case (_, d) => + d.nextRetryAt <= now() && + d.status === (PeriodicProcessDeploymentStatus.RetryingDeploy: PeriodicProcessDeploymentStatus) + } + ) + + private def findProcesses( + query: Query[ + (PeriodicProcessWithoutJson, PeriodicProcessDeploymentsTable), + (PeriodicProcessEntityWithoutJson, PeriodicProcessDeploymentEntity), + Seq + ] + ) = { + query.result + .map(_.map { case (periodicProcess, periodicDeployment) => + LegacyPeriodicProcessesRepository.createPeriodicProcessDeployment( + periodicProcess, + periodicDeployment, + ) + }) + } + + override def findProcessData(id: PeriodicProcessDeploymentId): Action[PeriodicProcessDeployment] = + findProcesses( + (PeriodicProcessesWithoutJson join PeriodicProcessDeployments on (_.id === _.periodicProcessId)) + .filter { case (_, deployment) => deployment.id === id } + ).map(_.head) + + override def markDeployed(id: PeriodicProcessDeploymentId): Action[Unit] = { + val q = for { + d <- PeriodicProcessDeployments if d.id === id + } yield (d.status, d.deployedAt) + val update = q.update((PeriodicProcessDeploymentStatus.Deployed, Some(now()))) + update.map(_ => ()) + } + + override def markFailed(id: PeriodicProcessDeploymentId): Action[Unit] = { + updateCompleted(id, PeriodicProcessDeploymentStatus.Failed) + } + + override def markFinished(id: PeriodicProcessDeploymentId): Action[Unit] = { + updateCompleted(id, PeriodicProcessDeploymentStatus.Finished) + } + + override def markFailedOnDeployWithStatus( + id: PeriodicProcessDeploymentId, + status: PeriodicProcessDeploymentStatus, + retriesLeft: Int, + retryAt: Option[LocalDateTime] + ): Action[Unit] = { + val q = for { + d <- PeriodicProcessDeployments if d.id === id + } yield (d.status, d.completedAt, d.retriesLeft, d.nextRetryAt) + val update = q.update((status, Some(now()), retriesLeft, retryAt)) + update.map(_ => ()) + } + + private def updateCompleted( + id: PeriodicProcessDeploymentId, + status: PeriodicProcessDeploymentStatus + ): Action[Unit] = { + val q = for { + d <- PeriodicProcessDeployments if d.id === id + } yield (d.status, d.completedAt) + val update = q.update((status, Some(now()))) + update.map(_ => ()) + } + + override def findActiveSchedulesForProcessesHavingDeploymentWithMatchingStatus( + expectedDeploymentStatuses: Set[PeriodicProcessDeploymentStatus], + ): Action[SchedulesState] = { + val processesHavingDeploymentsWithMatchingStatus = PeriodicProcessesWithoutJson.filter(p => + p.active && + PeriodicProcessDeployments + .filter(d => d.periodicProcessId === p.id && d.status.inSet(expectedDeploymentStatuses)) + .exists + ) + getLatestDeploymentsForEachSchedule( + processesHavingDeploymentsWithMatchingStatus, + deploymentsPerScheduleMaxCount = 1, + processingType = processingType, + ).map(schedulesForProcessNames => + SchedulesState( + schedulesForProcessNames.values.map(_.schedules).foldLeft(Map.empty[ScheduleId, ScheduleData])(_ ++ _) + ) + ) + } + + override def getLatestDeploymentsForActiveSchedules( + processName: ProcessName, + deploymentsPerScheduleMaxCount: Int, + ): Action[SchedulesState] = { + val activeProcessesQuery = + PeriodicProcessesWithoutJson.filter(p => p.processName === processName && p.active) + getLatestDeploymentsForEachSchedule(activeProcessesQuery, deploymentsPerScheduleMaxCount, processingType) + .map(_.getOrElse(processName, SchedulesState(Map.empty))) + } + + override def getLatestDeploymentsForActiveSchedules( + deploymentsPerScheduleMaxCount: Int, + ): Action[Map[ProcessName, SchedulesState]] = { + val activeProcessesQuery = PeriodicProcessesWithoutJson.filter(_.active) + getLatestDeploymentsForEachSchedule(activeProcessesQuery, deploymentsPerScheduleMaxCount, processingType) + } + + override def getLatestDeploymentsForLatestInactiveSchedules( + processName: ProcessName, + inactiveProcessesMaxCount: Int, + deploymentsPerScheduleMaxCount: Int, + ): Action[SchedulesState] = { + val filteredProcessesQuery = PeriodicProcessesWithoutJson + .filter(p => p.processName === processName && !p.active) + .sortBy(_.createdAt.desc) + .take(inactiveProcessesMaxCount) + getLatestDeploymentsForEachSchedule(filteredProcessesQuery, deploymentsPerScheduleMaxCount, processingType) + .map(_.getOrElse(processName, SchedulesState(Map.empty))) + } + + override def getLatestDeploymentsForLatestInactiveSchedules( + inactiveProcessesMaxCount: Int, + deploymentsPerScheduleMaxCount: Int, + ): Action[Map[ProcessName, SchedulesState]] = { + val filteredProcessesQuery = PeriodicProcessesWithoutJson + .filter(!_.active) + .sortBy(_.createdAt.desc) + .take(inactiveProcessesMaxCount) + getLatestDeploymentsForEachSchedule(filteredProcessesQuery, deploymentsPerScheduleMaxCount, processingType) + } + + private def getLatestDeploymentsForEachSchedule( + periodicProcessesQuery: Query[ + PeriodicProcessWithoutJson, + PeriodicProcessEntityWithoutJson, + Seq + ], + deploymentsPerScheduleMaxCount: Int, + processingType: String, + ): Action[Map[ProcessName, SchedulesState]] = { + val filteredPeriodicProcessQuery = periodicProcessesQuery.filter(p => p.processingType === processingType) + val latestDeploymentsForSchedules = profile match { + case _: ExPostgresProfile => + getLatestDeploymentsForEachSchedulePostgres(filteredPeriodicProcessQuery, deploymentsPerScheduleMaxCount) + case _ => + getLatestDeploymentsForEachScheduleJdbcGeneric(filteredPeriodicProcessQuery, deploymentsPerScheduleMaxCount) + } + latestDeploymentsForSchedules.map(toSchedulesState) + } + + private def getLatestDeploymentsForEachSchedulePostgres( + periodicProcessesQuery: Query[ + PeriodicProcessWithoutJson, + PeriodicProcessEntityWithoutJson, + Seq + ], + deploymentsPerScheduleMaxCount: Int + ): Action[Seq[(PeriodicProcessEntity, PeriodicProcessDeploymentEntity)]] = { + // To effectively limit deployments to given count for each schedule in one query, we use window functions in slick + import ExPostgresProfile.api._ + import com.github.tminglei.slickpg.window.PgWindowFuncSupport.WindowFunctions._ + + val deploymentsForProcesses = + periodicProcessesQuery join PeriodicProcessDeployments on (_.id === _.periodicProcessId) + deploymentsForProcesses + .map { case (process, deployment) => + ( + rowNumber() :: Over + .partitionBy((deployment.periodicProcessId, deployment.scheduleName)) + .sortBy( + deployment.runAt.desc, + deployment.createdAt.desc + ), // Remember to change DeploymentStatus.ordering accordingly + process, + deployment + ) + } + .subquery + .filter(_._1 <= deploymentsPerScheduleMaxCount.longValue()) + .map { case (_, process, deployment) => + (process, deployment) + } + .result + } + + // This variant of method is much less optimal than postgres one. It is highly recommended to use postgres with periodics + // If we decided to support more databases, we should consider some optimization like extracting periodic_schedule table + // with foreign key to periodic_process and with schedule_name column - it would reduce number of queries + private def getLatestDeploymentsForEachScheduleJdbcGeneric( + periodicProcessesQuery: Query[ + PeriodicProcessWithoutJson, + PeriodicProcessEntityWithoutJson, + Seq + ], + deploymentsPerScheduleMaxCount: Int + ): Action[Seq[(PeriodicProcessEntity, PeriodicProcessDeploymentEntity)]] = { + // It is debug instead of warn to not bloast logs when e.g. for some reasons is used hsql under the hood + logger.debug( + "WARN: Using not optimized version of getLatestDeploymentsForEachSchedule that not uses window functions" + ) + for { + processes <- periodicProcessesQuery.result + schedulesForProcesses <- + DBIO + .sequence(processes.map { process => + PeriodicProcessDeployments + .filter(_.periodicProcessId === process.id) + .map(_.scheduleName) + .distinct + .result + .map(_.map((process, _))) + }) + .map(_.flatten) + deploymentsForSchedules <- + DBIO + .sequence(schedulesForProcesses.map { case (process, scheduleName) => + PeriodicProcessDeployments + // In SQL when you compare nulls, you will get always false + .filter(deployment => + deployment.periodicProcessId === process.id && (deployment.scheduleName === scheduleName || deployment.scheduleName.isEmpty && scheduleName.isEmpty) + ) + .sortBy(a => (a.runAt.desc, a.createdAt.desc)) // Remember to change DeploymentStatus.ordering accordingly + .take(deploymentsPerScheduleMaxCount) + .result + .map(_.map((process, _))) + }) + .map(_.flatten) + } yield deploymentsForSchedules + } + + override def schedule( + id: PeriodicProcessId, + scheduleName: ScheduleName, + runAt: LocalDateTime, + deployMaxRetries: Int + ): Action[PeriodicProcessDeployment] = { + val deploymentEntity = PeriodicProcessDeploymentEntity( + id = PeriodicProcessDeploymentId(-1), + periodicProcessId = id, + createdAt = now(), + runAt = runAt, + scheduleName = scheduleName.value, + deployedAt = None, + completedAt = None, + retriesLeft = deployMaxRetries, + nextRetryAt = None, + status = PeriodicProcessDeploymentStatus.Scheduled + ) + ((PeriodicProcessDeployments returning PeriodicProcessDeployments.map(_.id) into ((_, id) => + id + )) += deploymentEntity).flatMap(findProcessData) + } + + override def markInactive(processId: PeriodicProcessId): Action[Unit] = { + val q = for { + p <- PeriodicProcessesWithoutJson if p.id === processId + } yield p.active + val update = q.update(false) + update.map(_ => ()) + } + + override def fetchCanonicalProcessWithVersion( + processName: ProcessName, + versionId: VersionId + ): Future[Option[(CanonicalProcess, ProcessVersion)]] = + fetchingProcessRepository.getCanonicalProcessWithVersion(processName, versionId)(NussknackerInternalUser.instance) + + def fetchInputConfigDuringExecutionJson(processName: ProcessName, versionId: VersionId): Action[Option[String]] = + PeriodicProcessesWithJson + .filter(p => p.processName === processName && p.processVersionId === versionId) + .map(_.inputConfigDuringExecutionJson) + .result + .headOption + + private def activePeriodicProcessWithDeploymentQuery(processingType: String) = { + (PeriodicProcessesWithoutJson.filter(p => p.active === true && p.processingType === processingType) + join PeriodicProcessDeployments on (_.id === _.periodicProcessId)) + } + + private def toSchedulesState( + list: Seq[(PeriodicProcessEntity, PeriodicProcessDeploymentEntity)] + ): Map[ProcessName, SchedulesState] = { + list + .groupBy(_._1.processName) + .map { case (processName, list) => processName -> toSchedulesStateForSinglePeriodicProcess(list) } + } + + private def toSchedulesStateForSinglePeriodicProcess( + list: Seq[(PeriodicProcessEntity, PeriodicProcessDeploymentEntity)] + ): SchedulesState = { + SchedulesState( + list + .map { case (process, deployment) => + val scheduleId = ScheduleId(process.id, ScheduleName(deployment.scheduleName)) + val scheduleData = (scheduleId, process) + val scheduleDeployment = scheduleDeploymentData(deployment) + (scheduleData, scheduleDeployment) + } + .toList + .toGroupedMap + .toList + .map { case ((scheduleId, process), deployments) => + scheduleId -> ScheduleData(createPeriodicProcess(process), deployments) + } + .toMap + ) + } + + private def scheduleDeploymentData(deployment: PeriodicProcessDeploymentEntity): ScheduleDeploymentData = { + ScheduleDeploymentData( + deployment.id, + deployment.createdAt, + deployment.runAt, + deployment.deployedAt, + deployment.retriesLeft, + deployment.nextRetryAt, + LegacyPeriodicProcessesRepository.createPeriodicDeploymentState(deployment) + ) + } + +} + +//Copied from designer/server. +object DBIOActionInstances { + + type DB[A] = DBIOAction[A, NoStream, Effect.All] + + implicit def dbMonad(implicit ec: ExecutionContext): Monad[DB] = new Monad[DB] { + + override def pure[A](x: A) = DBIO.successful(x) + + override def flatMap[A, B](fa: DB[A])(f: (A) => DB[B]) = fa.flatMap(f) + + // this is *not* tail recursive + override def tailRecM[A, B](a: A)(f: (A) => DB[Either[A, B]]): DB[B] = + f(a).flatMap { + case Right(r) => pure(r) + case Left(l) => tailRecM(l)(f) + } + + } + +} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/PeriodicProcessesTable.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyPeriodicProcessesTableFactory.scala similarity index 92% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/PeriodicProcessesTable.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyPeriodicProcessesTableFactory.scala index 1dfaef719eb..cf99e1ec652 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/PeriodicProcessesTable.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/legacy/db/LegacyPeriodicProcessesTableFactory.scala @@ -1,37 +1,35 @@ -package pl.touk.nussknacker.engine.management.periodic.db +package pl.touk.nussknacker.ui.process.periodic.legacy.db import io.circe.syntax.EncoderOps import pl.touk.nussknacker.engine.api.deployment.ProcessActionId import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessId import pl.touk.nussknacker.engine.marshall.ProcessMarshaller -import slick.ast.TypedType +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessId import slick.jdbc.JdbcProfile -import slick.lifted.MappedToBase.mappedToIsomorphism import slick.lifted.ProvenShape import slick.sql.SqlProfile.ColumnOption.NotNull import java.time.LocalDateTime import java.util.UUID -trait PeriodicProcessesTableFactory { +trait LegacyPeriodicProcessesTableFactory { protected val profile: JdbcProfile import profile.api._ + implicit val periodicProcessIdMapping: BaseColumnType[PeriodicProcessId] = + MappedColumnType.base[PeriodicProcessId, Long](_.value, PeriodicProcessId.apply) + implicit val processNameMapping: BaseColumnType[ProcessName] = MappedColumnType.base[ProcessName, String](_.value, ProcessName.apply) implicit val versionIdMapping: BaseColumnType[VersionId] = - MappedColumnType.base[VersionId, Long](_.value, VersionId(_)) + MappedColumnType.base[VersionId, Long](_.value, VersionId.apply) - implicit val ProcessActionIdTypedType: TypedType[ProcessActionId] = - MappedColumnType.base[ProcessActionId, UUID]( - _.value, - ProcessActionId(_) - ) + implicit val processActionIdMapping: BaseColumnType[ProcessActionId] = + MappedColumnType.base[ProcessActionId, UUID](_.value, ProcessActionId.apply) abstract class PeriodicProcessesTable[ENTITY <: PeriodicProcessEntity](tag: Tag) extends Table[ENTITY](tag, "periodic_processes") { diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/model/PeriodicProcess.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/model/PeriodicProcess.scala new file mode 100644 index 00000000000..50c54e5b271 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/model/PeriodicProcess.scala @@ -0,0 +1,18 @@ +package pl.touk.nussknacker.ui.process.periodic.model + +import pl.touk.nussknacker.engine.api.deployment.ProcessActionId +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.DeploymentWithRuntimeParams +import pl.touk.nussknacker.ui.process.periodic.ScheduleProperty + +import java.time.LocalDateTime + +case class PeriodicProcessId(value: Long) + +case class PeriodicProcess( + id: PeriodicProcessId, + deploymentData: DeploymentWithRuntimeParams, + scheduleProperty: ScheduleProperty, + active: Boolean, + createdAt: LocalDateTime, + processActionId: Option[ProcessActionId] +) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/model/PeriodicProcessDeployment.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/model/PeriodicProcessDeployment.scala new file mode 100644 index 00000000000..dba8136bfdf --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/model/PeriodicProcessDeployment.scala @@ -0,0 +1,71 @@ +package pl.touk.nussknacker.ui.process.periodic.model + +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{ScheduledDeploymentDetails, ScheduledDeploymentStatus} +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeploymentStatus.{ + Deployed, + Failed, + FailedOnDeploy, + Finished, + PeriodicProcessDeploymentStatus, + RetryingDeploy, + Scheduled +} + +import java.time.LocalDateTime + +// TODO: We should separate schedules concept from deployments - fully switch to ScheduleData and ScheduleDeploymentData +case class PeriodicProcessDeployment( + id: PeriodicProcessDeploymentId, + periodicProcess: PeriodicProcess, + createdAt: LocalDateTime, + runAt: LocalDateTime, + scheduleName: ScheduleName, + retriesLeft: Int, + nextRetryAt: Option[LocalDateTime], + state: PeriodicProcessDeploymentState +) { + + def display: String = + s"Process with id=${periodicProcess.deploymentData.processId}, name=${periodicProcess.deploymentData.processName}, versionId=${periodicProcess.deploymentData.versionId}, scheduleName=${scheduleName.display} and deploymentId=$id" + + def toDetails: ScheduledDeploymentDetails = + ScheduledDeploymentDetails( + id = id.value, + processName = periodicProcess.deploymentData.processName, + versionId = periodicProcess.deploymentData.versionId, + scheduleName = scheduleName.value, + createdAt = createdAt, + runAt = runAt, + deployedAt = state.deployedAt, + completedAt = state.completedAt, + status = state.status match { + case Scheduled => ScheduledDeploymentStatus.Scheduled + case Deployed => ScheduledDeploymentStatus.Deployed + case Finished => ScheduledDeploymentStatus.Finished + case Failed => ScheduledDeploymentStatus.Failed + case RetryingDeploy => ScheduledDeploymentStatus.RetryingDeploy + case FailedOnDeploy => ScheduledDeploymentStatus.FailedOnDeploy + }, + ) + +} + +case class PeriodicProcessDeploymentState( + deployedAt: Option[LocalDateTime], + completedAt: Option[LocalDateTime], + status: PeriodicProcessDeploymentStatus +) + +case class PeriodicProcessDeploymentId(value: Long) { + override def toString: String = value.toString +} + +object PeriodicProcessDeploymentStatus extends Enumeration { + type PeriodicProcessDeploymentStatus = Value + + val Scheduled, Deployed, Finished, Failed, RetryingDeploy, FailedOnDeploy = Value +} + +case class ScheduleName(value: Option[String]) { + def display: String = value.getOrElse("[default]") +} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/SchedulesState.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/model/SchedulesState.scala similarity index 66% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/SchedulesState.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/model/SchedulesState.scala index ca697fe27d9..b4161cffe46 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/SchedulesState.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/model/SchedulesState.scala @@ -1,8 +1,6 @@ -package pl.touk.nussknacker.engine.management.periodic.model +package pl.touk.nussknacker.ui.process.periodic.model import pl.touk.nussknacker.engine.api.process.ProcessName -import pl.touk.nussknacker.engine.management.periodic.db.{PeriodicProcessDeploymentEntity, PeriodicProcessesRepository} -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData.WithoutCanonicalProcess import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap import java.time.LocalDateTime @@ -20,7 +18,7 @@ case class SchedulesState(schedules: Map[ScheduleId, ScheduleData]) { def isEmpty: Boolean = schedules.isEmpty def groupByProcessName: Map[ProcessName, SchedulesState] = - schedules.groupBy(_._2.process.processVersion.processName).mapValuesNow(SchedulesState) + schedules.groupBy(_._2.process.deploymentData.processName).mapValuesNow(SchedulesState) lazy val groupedByPeriodicProcess: List[PeriodicProcessScheduleData] = schedules.toList.groupBy(_._2.process).toList.map { case (periodicProcess, groupedSchedules) => @@ -36,10 +34,7 @@ case class SchedulesState(schedules: Map[ScheduleId, ScheduleData]) { // For most operations it will contain only one latest deployment but for purpose of statuses of historical deployments // it has list instead of one element. // This structure should contain SingleScheduleProperty as well. See note above -case class ScheduleData( - process: PeriodicProcess[WithoutCanonicalProcess], - latestDeployments: List[ScheduleDeploymentData] -) +case class ScheduleData(process: PeriodicProcess, latestDeployments: List[ScheduleDeploymentData]) // To identify schedule we need scheduleName - None for SingleScheduleProperty and Some(key) for MultipleScheduleProperty keys // Also we need PeriodicProcessId to distinguish between active schedules and some inactive from the past for the same PeriodicProcessId @@ -57,42 +52,17 @@ case class ScheduleDeploymentData( ) { def toFullDeploymentData( - process: PeriodicProcess[WithoutCanonicalProcess], + process: PeriodicProcess, scheduleName: ScheduleName - ): PeriodicProcessDeployment[WithoutCanonicalProcess] = + ): PeriodicProcessDeployment = PeriodicProcessDeployment(id, process, createdAt, runAt, scheduleName, retriesLeft, nextRetryAt, state) def display = s"deploymentId=$id" } -object ScheduleDeploymentData { - - def apply(deployment: PeriodicProcessDeploymentEntity): ScheduleDeploymentData = { - ScheduleDeploymentData( - deployment.id, - deployment.createdAt, - deployment.runAt, - deployment.deployedAt, - deployment.retriesLeft, - deployment.nextRetryAt, - PeriodicProcessesRepository.createPeriodicDeploymentState(deployment) - ) - } - -} - // These below are temporary structures, see notice next to SchedulesState case class PeriodicProcessScheduleData( - process: PeriodicProcess[WithoutCanonicalProcess], - deployments: List[PeriodicProcessDeployment[WithoutCanonicalProcess]] -) { - def existsDeployment(predicate: PeriodicProcessDeployment[WithoutCanonicalProcess] => Boolean): Boolean = - deployments.exists(predicate) - - def display: String = { - val deploymentsForSchedules = deployments.map(_.display) - s"processName=${process.processVersion.processName}, deploymentsForSchedules=$deploymentsForSchedules" - } - -} + process: PeriodicProcess, + deployments: List[PeriodicProcessDeployment] +) diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/util/DeterministicUUIDFromLong.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/utils/DeterministicUUIDFromLong.scala similarity index 93% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/util/DeterministicUUIDFromLong.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/utils/DeterministicUUIDFromLong.scala index 3f1a80b09b4..4cf890790cf 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/util/DeterministicUUIDFromLong.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/utils/DeterministicUUIDFromLong.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic.util +package pl.touk.nussknacker.ui.process.periodic.utils import java.util.UUID diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/SchedulePropertyExtractor.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/utils/SchedulePropertyExtractorUtils.scala similarity index 76% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/SchedulePropertyExtractor.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/utils/SchedulePropertyExtractorUtils.scala index 989d625cb51..0e2ccb4bd55 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/SchedulePropertyExtractor.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/periodic/utils/SchedulePropertyExtractorUtils.scala @@ -1,18 +1,13 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.utils import cats.instances.list._ import cats.syntax.traverse._ -import com.typesafe.scalalogging.LazyLogging import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.CronSchedulePropertyExtractor.CronPropertyDefaultName +import pl.touk.nussknacker.ui.process.periodic.{CronScheduleProperty, MultipleScheduleProperty, ScheduleProperty, SingleScheduleProperty} import java.time.Clock -trait SchedulePropertyExtractor { - def apply(canonicalProcess: CanonicalProcess): Either[String, ScheduleProperty] -} - -object SchedulePropertyExtractor { +object SchedulePropertyExtractorUtils { def extractProperty(canonicalProcess: CanonicalProcess, name: String): Either[String, ScheduleProperty] = { for { @@ -75,19 +70,3 @@ object SchedulePropertyExtractor { } } - -object CronSchedulePropertyExtractor { - - val CronPropertyDefaultName = "cron" - -} - -case class CronSchedulePropertyExtractor(propertyName: String = CronPropertyDefaultName) - extends SchedulePropertyExtractor - with LazyLogging { - - override def apply(canonicalProcess: CanonicalProcess): Either[String, ScheduleProperty] = { - SchedulePropertyExtractor.extractProperty(canonicalProcess, propertyName) - } - -} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/InvalidDeploymentManagerStub.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/InvalidDeploymentManagerStub.scala index df7f930b571..ee66bfe9b5b 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/InvalidDeploymentManagerStub.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/InvalidDeploymentManagerStub.scala @@ -54,5 +54,7 @@ object InvalidDeploymentManagerStub extends DeploymentManager { override def stateQueryForAllScenariosSupport: StateQueryForAllScenariosSupport = NoStateQueryForAllScenariosSupport + override def schedulingSupport: SchedulingSupport = NoSchedulingSupport + override def close(): Unit = () } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeData.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeData.scala index 9390503101c..00aa215ed26 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeData.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeData.scala @@ -4,6 +4,7 @@ import com.typesafe.config.Config import pl.touk.nussknacker.engine._ import pl.touk.nussknacker.engine.api.component.ScenarioPropertyConfig import pl.touk.nussknacker.engine.api.deployment.cache.ScenarioStateCachingConfig +import pl.touk.nussknacker.engine.api.deployment.{NoSchedulingSupport, SchedulingSupported} import pl.touk.nussknacker.engine.api.process.ProcessingType import pl.touk.nussknacker.engine.definition.component.Components.ComponentDefinitionExtractionMode import pl.touk.nussknacker.engine.definition.component.{ @@ -13,6 +14,8 @@ import pl.touk.nussknacker.engine.definition.component.{ } import pl.touk.nussknacker.engine.deployment.EngineSetupName import pl.touk.nussknacker.restmodel.scenariodetails.ScenarioParameters +import pl.touk.nussknacker.ui.db.DbRef +import pl.touk.nussknacker.ui.process.periodic.{PeriodicDeploymentManagerDecorator, SchedulingConfig} import pl.touk.nussknacker.ui.process.processingtype.DesignerModelData.DynamicComponentsStaticDefinitions import scala.util.control.NonFatal @@ -53,6 +56,7 @@ object ProcessingTypeData { name: ProcessingType, modelData: ModelData, deploymentManagerProvider: DeploymentManagerProvider, + schedulingForProcessingType: SchedulingForProcessingType, deploymentManagerDependencies: DeploymentManagerDependencies, engineSetupName: EngineSetupName, deploymentConfig: Config, @@ -64,11 +68,12 @@ object ProcessingTypeData { val deploymentData = createDeploymentData( deploymentManagerProvider, + schedulingForProcessingType, deploymentManagerDependencies, engineSetupName, modelData, deploymentConfig, - metaDataInitializer + metaDataInitializer, ) val designerModelData = @@ -90,22 +95,52 @@ object ProcessingTypeData { private def createDeploymentData( deploymentManagerProvider: DeploymentManagerProvider, + schedulingForProcessingType: SchedulingForProcessingType, deploymentManagerDependencies: DeploymentManagerDependencies, engineSetupName: EngineSetupName, modelData: ModelData, deploymentConfig: Config, - metaDataInitializer: MetaDataInitializer + metaDataInitializer: MetaDataInitializer, ) = { val scenarioStateCacheTTL = ScenarioStateCachingConfig.extractScenarioStateCacheTTL(deploymentConfig) - val validDeploymentManager = - deploymentManagerProvider.createDeploymentManager( + val validDeploymentManager = for { + deploymentManager <- deploymentManagerProvider.createDeploymentManager( modelData, deploymentManagerDependencies, deploymentConfig, scenarioStateCacheTTL ) - val scenarioProperties = + decoratedDeploymentManager = schedulingForProcessingType match { + case SchedulingForProcessingType.Available(dbRef) => + deploymentManager.schedulingSupport match { + case supported: SchedulingSupported => + PeriodicDeploymentManagerDecorator.decorate( + underlying = deploymentManager, + schedulingSupported = supported, + modelData = modelData, + deploymentConfig = deploymentConfig, + dependencies = deploymentManagerDependencies, + dbRef = dbRef, + ) + case NoSchedulingSupport => + throw new IllegalStateException( + s"DeploymentManager ${deploymentManagerProvider.name} does not support periodic execution" + ) + } + + case SchedulingForProcessingType.NotAvailable => + deploymentManager + } + } yield decoratedDeploymentManager + + val additionalScenarioProperties = schedulingForProcessingType match { + case SchedulingForProcessingType.Available(_) => + PeriodicDeploymentManagerDecorator.additionalScenarioProperties + case SchedulingForProcessingType.NotAvailable => + Map.empty[String, ScenarioPropertyConfig] + } + val scenarioProperties = additionalScenarioProperties ++ deploymentManagerProvider.scenarioPropertiesConfig(deploymentConfig) ++ modelData.modelConfig .getOrElse[Map[ProcessingType, ScenarioPropertyConfig]]("scenarioPropertiesConfig", Map.empty) val fragmentProperties = modelData.modelConfig @@ -163,4 +198,14 @@ object ProcessingTypeData { ) } + sealed trait SchedulingForProcessingType + + object SchedulingForProcessingType { + + case object NotAvailable extends SchedulingForProcessingType + + final case class Available(dbRef: DbRef) extends SchedulingForProcessingType + + } + } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/LocalProcessingTypeDataLoader.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/LocalProcessingTypeDataLoader.scala index c5676f87cd3..46f28fd3b6b 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/LocalProcessingTypeDataLoader.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/LocalProcessingTypeDataLoader.scala @@ -5,6 +5,8 @@ import com.typesafe.config.ConfigFactory import pl.touk.nussknacker.engine._ import pl.touk.nussknacker.engine.api.process.ProcessingType import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap +import pl.touk.nussknacker.ui.db.DbRef +import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeData.SchedulingForProcessingType import pl.touk.nussknacker.ui.process.processingtype.loader.ProcessingTypeDataLoader.toValueWithRestriction import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataState import pl.touk.nussknacker.ui.process.processingtype.{ @@ -21,7 +23,8 @@ class LocalProcessingTypeDataLoader( override def loadProcessingTypeData( getModelDependencies: ProcessingType => ModelDependencies, getDeploymentManagerDependencies: ProcessingType => DeploymentManagerDependencies, - modelClassLoaderProvider: ModelClassLoaderProvider + modelClassLoaderProvider: ModelClassLoaderProvider, + dbRef: Option[DbRef], ): IO[ProcessingTypeDataState[ProcessingTypeData, CombinedProcessingTypeData]] = IO { val processingTypes = modelData.map { case (processingType, (category, model)) => val deploymentManagerDependencies = getDeploymentManagerDependencies(processingType) @@ -29,11 +32,12 @@ class LocalProcessingTypeDataLoader( name = processingType, modelData = model, deploymentManagerProvider = deploymentManagerProvider, + schedulingForProcessingType = SchedulingForProcessingType.NotAvailable, deploymentManagerDependencies = deploymentManagerDependencies, engineSetupName = deploymentManagerProvider.defaultEngineSetupName, deploymentConfig = ConfigFactory.empty(), category = category, - componentDefinitionExtractionMode = getModelDependencies(processingType).componentDefinitionExtractionMode + componentDefinitionExtractionMode = getModelDependencies(processingType).componentDefinitionExtractionMode, ) processingType -> data } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypeDataLoader.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypeDataLoader.scala index 28fa2bb20bc..277dd6b96a1 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypeDataLoader.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypeDataLoader.scala @@ -3,6 +3,7 @@ package pl.touk.nussknacker.ui.process.processingtype.loader import cats.effect.IO import pl.touk.nussknacker.engine.api.process.ProcessingType import pl.touk.nussknacker.engine.{DeploymentManagerDependencies, ModelDependencies} +import pl.touk.nussknacker.ui.db.DbRef import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataState import pl.touk.nussknacker.ui.process.processingtype.{ CombinedProcessingTypeData, @@ -16,7 +17,10 @@ trait ProcessingTypeDataLoader { def loadProcessingTypeData( getModelDependencies: ProcessingType => ModelDependencies, getDeploymentManagerDependencies: ProcessingType => DeploymentManagerDependencies, - modelClassLoaderProvider: ModelClassLoaderProvider + modelClassLoaderProvider: ModelClassLoaderProvider, + // should be always available, used by scheduling mechanism, + // but in tests sometimes we do not want to bootstrap the full environment with db + dbRef: Option[DbRef], ): IO[ProcessingTypeDataState[ProcessingTypeData, CombinedProcessingTypeData]] } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypesConfigBasedProcessingTypeDataLoader.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypesConfigBasedProcessingTypeDataLoader.scala index c1d31b5e5c2..6f7e96d6e67 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypesConfigBasedProcessingTypeDataLoader.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/loader/ProcessingTypesConfigBasedProcessingTypeDataLoader.scala @@ -7,6 +7,8 @@ import pl.touk.nussknacker.engine.api.process.ProcessingType import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap import pl.touk.nussknacker.engine.util.loader.ScalaServiceLoader import pl.touk.nussknacker.ui.configloader.{ProcessingTypeConfigs, ProcessingTypeConfigsLoader} +import pl.touk.nussknacker.ui.db.DbRef +import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeData.SchedulingForProcessingType import pl.touk.nussknacker.ui.process.processingtype._ import pl.touk.nussknacker.ui.process.processingtype.loader.ProcessingTypeDataLoader.toValueWithRestriction import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataState @@ -18,12 +20,19 @@ class ProcessingTypesConfigBasedProcessingTypeDataLoader(processingTypeConfigsLo override def loadProcessingTypeData( getModelDependencies: ProcessingType => ModelDependencies, getDeploymentManagerDependencies: ProcessingType => DeploymentManagerDependencies, - modelClassLoaderProvider: ModelClassLoaderProvider + modelClassLoaderProvider: ModelClassLoaderProvider, + dbRef: Option[DbRef], ): IO[ProcessingTypeDataState[ProcessingTypeData, CombinedProcessingTypeData]] = { processingTypeConfigsLoader .loadProcessingTypeConfigs() .map( - createProcessingTypeData(_, getModelDependencies, getDeploymentManagerDependencies, modelClassLoaderProvider) + createProcessingTypeData( + _, + getModelDependencies, + getDeploymentManagerDependencies, + modelClassLoaderProvider, + dbRef + ) ) } @@ -31,7 +40,8 @@ class ProcessingTypesConfigBasedProcessingTypeDataLoader(processingTypeConfigsLo processingTypesConfig: ProcessingTypeConfigs, getModelDependencies: ProcessingType => ModelDependencies, getDeploymentManagerDependencies: ProcessingType => DeploymentManagerDependencies, - modelClassLoaderProvider: ModelClassLoaderProvider + modelClassLoaderProvider: ModelClassLoaderProvider, + dbRef: Option[DbRef], ): ProcessingTypeDataState[ProcessingTypeData, CombinedProcessingTypeData] = { // This step with splitting DeploymentManagerProvider loading for all processing types // and after that creating ProcessingTypeData is done because of the deduplication of deployments @@ -57,17 +67,30 @@ class ProcessingTypesConfigBasedProcessingTypeDataLoader(processingTypeConfigsLo val processingTypesData = providerWithNameInputData .map { case (processingType, (processingTypeConfig, deploymentManagerProvider, _)) => logger.debug(s"Creating Processing Type: $processingType with config: $processingTypeConfig") + val schedulingForProcessingType = + if (processingTypeConfig.deploymentConfig.hasPath("scheduling") && + processingTypeConfig.deploymentConfig.getBoolean("scheduling.enabled")) { + SchedulingForProcessingType.Available( + dbRef.getOrElse( + throw new RuntimeException(s"dbRef not present, but required for Dm with scheduling enabled") + ), + ) + } else { + SchedulingForProcessingType.NotAvailable + } + val modelDependencies = getModelDependencies(processingType) val modelClassLoader = modelClassLoaderProvider.forProcessingTypeUnsafe(processingType) val processingTypeData = ProcessingTypeData.createProcessingTypeData( processingType, ModelData(processingTypeConfig, modelDependencies, modelClassLoader), deploymentManagerProvider, + schedulingForProcessingType, getDeploymentManagerDependencies(processingType), engineSetupNames(processingType), processingTypeConfig.deploymentConfig, processingTypeConfig.category, - modelDependencies.componentDefinitionExtractionMode + modelDependencies.componentDefinitionExtractionMode, ) processingType -> processingTypeData } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepository.scala index c0ff82ef570..86450941a90 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepository.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepository.scala @@ -5,8 +5,10 @@ import cats.data.OptionT import cats.instances.future._ import com.typesafe.scalalogging.LazyLogging import db.util.DBIOActionInstances._ +import pl.touk.nussknacker.engine.api.ProcessVersion import pl.touk.nussknacker.engine.api.deployment.{ProcessAction, ProcessActionState, ScenarioActionName} import pl.touk.nussknacker.engine.api.process._ +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.ui.db.DbRef import pl.touk.nussknacker.ui.db.entity._ import pl.touk.nussknacker.ui.process.label.ScenarioLabel @@ -22,19 +24,20 @@ object DBFetchingProcessRepository { def create( dbRef: DbRef, - actionRepository: ScenarioActionRepository, + actionRepository: ScenarioActionReadOnlyRepository, scenarioLabelsRepository: ScenarioLabelsRepository )(implicit ec: ExecutionContext) = new DBFetchingProcessRepository[DB](dbRef, actionRepository, scenarioLabelsRepository) with DbioRepository def createFutureRepository( dbRef: DbRef, - actionRepository: ScenarioActionRepository, + actionReadOnlyRepository: ScenarioActionReadOnlyRepository, scenarioLabelsRepository: ScenarioLabelsRepository )( implicit ec: ExecutionContext ) = - new DBFetchingProcessRepository[Future](dbRef, actionRepository, scenarioLabelsRepository) with BasicRepository + new DBFetchingProcessRepository[Future](dbRef, actionReadOnlyRepository, scenarioLabelsRepository) + with BasicRepository } @@ -43,7 +46,7 @@ object DBFetchingProcessRepository { // to the resource on the services side abstract class DBFetchingProcessRepository[F[_]: Monad]( protected val dbRef: DbRef, - actionRepository: ScenarioActionRepository, + actionRepository: ScenarioActionReadOnlyRepository, scenarioLabelsRepository: ScenarioLabelsRepository, )(protected implicit val ec: ExecutionContext) extends FetchingProcessRepository[F] @@ -51,6 +54,22 @@ abstract class DBFetchingProcessRepository[F[_]: Monad]( import api._ + override def getCanonicalProcessWithVersion( + processName: ProcessName, + versionId: VersionId + )( + implicit user: LoggedUser, + ): F[Option[(CanonicalProcess, ProcessVersion)]] = { + val result = for { + processId <- OptionT(fetchProcessId(processName)) + details <- OptionT(fetchProcessDetailsForId[CanonicalProcess](processId, versionId)) + } yield ( + details.json, + details.toEngineProcessVersion, + ) + result.value + } + override def fetchLatestProcessesDetails[PS: ScenarioShapeFetchStrategy]( query: ScenarioQuery )(implicit loggedUser: LoggedUser, ec: ExecutionContext): F[List[ScenarioWithDetailsEntity[PS]]] = { diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/FetchingProcessRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/FetchingProcessRepository.scala index db88f7dd0a3..c404f705218 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/FetchingProcessRepository.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/FetchingProcessRepository.scala @@ -1,7 +1,9 @@ package pl.touk.nussknacker.ui.process.repository import cats.Monad -import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessIdWithName, ProcessName, ProcessingType, VersionId} +import pl.touk.nussknacker.engine.api.ProcessVersion +import pl.touk.nussknacker.engine.api.process._ +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.ui.process.ScenarioQuery import pl.touk.nussknacker.ui.security.api.LoggedUser @@ -23,6 +25,13 @@ abstract class FetchingProcessRepository[F[_]: Monad] extends ProcessDBQueryRepo query: ScenarioQuery )(implicit loggedUser: LoggedUser, ec: ExecutionContext): F[List[ScenarioWithDetailsEntity[PS]]] + def getCanonicalProcessWithVersion( + processName: ProcessName, + versionId: VersionId + )( + implicit user: LoggedUser, + ): F[Option[(CanonicalProcess, ProcessVersion)]] + def fetchProcessId(processName: ProcessName)(implicit ec: ExecutionContext): F[Option[ProcessId]] def fetchProcessName(processId: ProcessId)(implicit ec: ExecutionContext): F[Option[ProcessName]] diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/PeriodicProcessesRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/PeriodicProcessesRepository.scala similarity index 65% rename from engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/PeriodicProcessesRepository.scala rename to designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/PeriodicProcessesRepository.scala index 24cf6c47dac..adbc64164ac 100644 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/db/PeriodicProcessesRepository.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/PeriodicProcessesRepository.scala @@ -1,20 +1,22 @@ -package pl.touk.nussknacker.engine.management.periodic.db +package pl.touk.nussknacker.ui.process.repository -import cats.Monad import com.github.tminglei.slickpg.ExPostgresProfile import com.typesafe.scalalogging.LazyLogging +import db.util.DBIOActionInstances +import db.util.DBIOActionInstances.DB import io.circe.parser.decode +import io.circe.syntax.EncoderOps import pl.touk.nussknacker.engine.api.ProcessVersion import pl.touk.nussknacker.engine.api.deployment.ProcessActionId -import pl.touk.nussknacker.engine.api.process.ProcessName -import pl.touk.nussknacker.engine.management.periodic._ -import pl.touk.nussknacker.engine.management.periodic.db.PeriodicProcessesRepository.createPeriodicProcessWithoutJson -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData.{ - WithCanonicalProcess, - WithoutCanonicalProcess -} -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus -import pl.touk.nussknacker.engine.management.periodic.model._ +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.DeploymentWithRuntimeParams +import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess +import pl.touk.nussknacker.ui.db.entity._ +import pl.touk.nussknacker.ui.process.periodic.ScheduleProperty +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus +import pl.touk.nussknacker.ui.process.periodic.model._ +import pl.touk.nussknacker.ui.process.repository.PeriodicProcessesRepository.createPeriodicProcess +import pl.touk.nussknacker.ui.security.api.NussknackerInternalUser import slick.dbio.{DBIOAction, Effect, NoStream} import slick.jdbc.PostgresProfile.api._ import slick.jdbc.{JdbcBackend, JdbcProfile} @@ -26,10 +28,10 @@ import scala.language.higherKinds object PeriodicProcessesRepository { def createPeriodicProcessDeployment( - processEntity: PeriodicProcessEntityWithJson, + processEntity: PeriodicProcessEntity, processDeploymentEntity: PeriodicProcessDeploymentEntity - ): PeriodicProcessDeployment[WithCanonicalProcess] = { - val process = createPeriodicProcessWithJson(processEntity) + ): PeriodicProcessDeployment = { + val process = createPeriodicProcess(processEntity) PeriodicProcessDeployment( processDeploymentEntity.id, process, @@ -52,36 +54,17 @@ object PeriodicProcessesRepository { ) } - def createPeriodicProcessWithJson( - processEntity: PeriodicProcessEntityWithJson - ): PeriodicProcess[WithCanonicalProcess] = { - val processVersion = createProcessVersion(processEntity) - val scheduleProperty = prepareScheduleProperty(processEntity) - PeriodicProcess( - processEntity.id, - model.DeploymentWithJarData.WithCanonicalProcess( - processVersion = processVersion, - inputConfigDuringExecutionJson = processEntity.inputConfigDuringExecutionJson, - jarFileName = processEntity.jarFileName, - process = processEntity.processJson, - ), - scheduleProperty, - processEntity.active, - processEntity.createdAt, - processEntity.processActionId - ) - } - - def createPeriodicProcessWithoutJson( + def createPeriodicProcess( processEntity: PeriodicProcessEntity - ): PeriodicProcess[WithoutCanonicalProcess] = { - val processVersion = createProcessVersion(processEntity) + ): PeriodicProcess = { val scheduleProperty = prepareScheduleProperty(processEntity) PeriodicProcess( processEntity.id, - model.DeploymentWithJarData.WithoutCanonicalProcess( - processVersion = processVersion, - jarFileName = processEntity.jarFileName, + DeploymentWithRuntimeParams( + processId = processEntity.processId, + processName = processEntity.processName, + versionId = processEntity.processVersionId, + runtimeParams = processEntity.runtimeParams, ), scheduleProperty, processEntity.active, @@ -96,18 +79,12 @@ object PeriodicProcessesRepository { scheduleProperty } - private def createProcessVersion(processEntity: PeriodicProcessEntity): ProcessVersion = { - ProcessVersion.empty.copy(versionId = processEntity.processVersionId, processName = processEntity.processName) - } - } trait PeriodicProcessesRepository { type Action[_] - implicit def monad: Monad[Action] - implicit class RunOps[T](action: Action[T]) { def run: Future[T] = PeriodicProcessesRepository.this.run(action) } @@ -122,42 +99,42 @@ trait PeriodicProcessesRepository { ): Action[SchedulesState] def create( - deploymentWithJarData: DeploymentWithJarData.WithCanonicalProcess, + deploymentWithRuntimeParams: DeploymentWithRuntimeParams, + inputConfigDuringExecutionJson: String, + canonicalProcess: CanonicalProcess, scheduleProperty: ScheduleProperty, - processActionId: ProcessActionId - ): Action[PeriodicProcess[WithCanonicalProcess]] + processActionId: ProcessActionId, + ): Action[PeriodicProcess] def getLatestDeploymentsForActiveSchedules( processName: ProcessName, - deploymentsPerScheduleMaxCount: Int + deploymentsPerScheduleMaxCount: Int, ): Action[SchedulesState] def getLatestDeploymentsForActiveSchedules( - deploymentsPerScheduleMaxCount: Int + deploymentsPerScheduleMaxCount: Int, ): Action[Map[ProcessName, SchedulesState]] def getLatestDeploymentsForLatestInactiveSchedules( processName: ProcessName, inactiveProcessesMaxCount: Int, - deploymentsPerScheduleMaxCount: Int + deploymentsPerScheduleMaxCount: Int, ): Action[SchedulesState] def getLatestDeploymentsForLatestInactiveSchedules( inactiveProcessesMaxCount: Int, - deploymentsPerScheduleMaxCount: Int + deploymentsPerScheduleMaxCount: Int, ): Action[Map[ProcessName, SchedulesState]] - def findToBeDeployed: Action[Seq[PeriodicProcessDeployment[WithCanonicalProcess]]] + def findToBeDeployed: Action[Seq[PeriodicProcessDeployment]] - def findToBeRetried: Action[Seq[PeriodicProcessDeployment[WithCanonicalProcess]]] + def findToBeRetried: Action[Seq[PeriodicProcessDeployment]] def findActiveSchedulesForProcessesHavingDeploymentWithMatchingStatus( - expectedDeploymentStatuses: Set[PeriodicProcessDeploymentStatus] + expectedDeploymentStatuses: Set[PeriodicProcessDeploymentStatus], ): Action[SchedulesState] - def findProcessData(id: PeriodicProcessDeploymentId): Action[PeriodicProcessDeployment[WithCanonicalProcess]] - - def findProcessData(processName: ProcessName): Action[Seq[PeriodicProcess[WithCanonicalProcess]]] + def findProcessData(id: PeriodicProcessDeploymentId): Action[PeriodicProcessDeployment] def markDeployed(id: PeriodicProcessDeploymentId): Action[Unit] @@ -177,35 +154,43 @@ trait PeriodicProcessesRepository { scheduleName: ScheduleName, runAt: LocalDateTime, deployMaxRetries: Int - ): Action[PeriodicProcessDeployment[WithCanonicalProcess]] + ): Action[PeriodicProcessDeployment] + + def fetchInputConfigDuringExecutionJson( + processName: ProcessName, + versionId: VersionId + ): Action[Option[String]] + + def fetchCanonicalProcessWithVersion( + processName: ProcessName, + versionId: VersionId + ): Future[Option[(CanonicalProcess, ProcessVersion)]] } class SlickPeriodicProcessesRepository( + processingType: String, db: JdbcBackend.DatabaseDef, override val profile: JdbcProfile, clock: Clock, - processingType: String + fetchingProcessRepository: FetchingProcessRepository[Future], )(implicit ec: ExecutionContext) extends PeriodicProcessesRepository with PeriodicProcessesTableFactory with PeriodicProcessDeploymentsTableFactory with LazyLogging { - import io.circe.syntax._ import pl.touk.nussknacker.engine.util.Implicits._ type Action[T] = DBIOActionInstances.DB[T] - override implicit def monad: Monad[Action] = DBIOActionInstances.dbMonad - override def run[T](action: DBIOAction[T, NoStream, Effect.All]): Future[T] = db.run(action.transactionally) override def getSchedulesState( scenarioName: ProcessName, afterOpt: Option[LocalDateTime], ): Action[SchedulesState] = { - PeriodicProcessesWithoutJson + PeriodicProcessesWithoutInputConfig .filter(_.processName === scenarioName) .join(PeriodicProcessDeployments) .on(_.id === _.periodicProcessId) @@ -215,63 +200,72 @@ class SlickPeriodicProcessesRepository( } override def create( - deploymentWithJarData: DeploymentWithJarData.WithCanonicalProcess, + deploymentWithRuntimeParams: DeploymentWithRuntimeParams, + inputConfigDuringExecutionJson: String, + canonicalProcess: CanonicalProcess, scheduleProperty: ScheduleProperty, - processActionId: ProcessActionId - ): Action[PeriodicProcess[WithCanonicalProcess]] = { - val processEntity = PeriodicProcessEntityWithJson( + processActionId: ProcessActionId, + ): Action[PeriodicProcess] = { + val processEntity = PeriodicProcessEntityWithInputConfigJson( id = PeriodicProcessId(-1), - processName = deploymentWithJarData.processVersion.processName, - processVersionId = deploymentWithJarData.processVersion.versionId, + processId = deploymentWithRuntimeParams.processId, + processName = deploymentWithRuntimeParams.processName, + processVersionId = deploymentWithRuntimeParams.versionId, processingType = processingType, - processJson = deploymentWithJarData.process, - inputConfigDuringExecutionJson = deploymentWithJarData.inputConfigDuringExecutionJson, - jarFileName = deploymentWithJarData.jarFileName, + runtimeParams = deploymentWithRuntimeParams.runtimeParams, scheduleProperty = scheduleProperty.asJson.noSpaces, active = true, createdAt = now(), - Some(processActionId) + Some(processActionId), + inputConfigDuringExecutionJson = inputConfigDuringExecutionJson, ) - ((PeriodicProcessesWithJson returning PeriodicProcessesWithJson into ((_, id) => id)) += processEntity) - .map(PeriodicProcessesRepository.createPeriodicProcessWithJson) + ((PeriodicProcessesWithInputConfig returning PeriodicProcessesWithInputConfig into ((_, id) => + id + )) += processEntity) + .map(PeriodicProcessesRepository.createPeriodicProcess) } private def now(): LocalDateTime = LocalDateTime.now(clock) - override def findToBeDeployed: Action[Seq[PeriodicProcessDeployment[WithCanonicalProcess]]] = - activePeriodicProcessWithDeploymentQuery - .filter { case (_, d) => - d.runAt <= now() && - d.status === (PeriodicProcessDeploymentStatus.Scheduled: PeriodicProcessDeploymentStatus) - } - .result - .map(_.map((PeriodicProcessesRepository.createPeriodicProcessDeployment _).tupled)) + override def findToBeDeployed: Action[Seq[PeriodicProcessDeployment]] = + findProcesses( + activePeriodicProcessWithDeploymentQuery(processingType) + .filter { case (_, d) => + d.runAt <= now() && + d.status === (PeriodicProcessDeploymentStatus.Scheduled: PeriodicProcessDeploymentStatus) + } + ) - override def findToBeRetried: Action[Seq[PeriodicProcessDeployment[WithCanonicalProcess]]] = - activePeriodicProcessWithDeploymentQuery - .filter { case (_, d) => - d.nextRetryAt <= now() && - d.status === (PeriodicProcessDeploymentStatus.RetryingDeploy: PeriodicProcessDeploymentStatus) - } - .result - .map(_.map((PeriodicProcessesRepository.createPeriodicProcessDeployment _).tupled)) + override def findToBeRetried: Action[Seq[PeriodicProcessDeployment]] = + findProcesses( + activePeriodicProcessWithDeploymentQuery(processingType) + .filter { case (_, d) => + d.nextRetryAt <= now() && + d.status === (PeriodicProcessDeploymentStatus.RetryingDeploy: PeriodicProcessDeploymentStatus) + } + ) - override def findProcessData( - id: PeriodicProcessDeploymentId - ): Action[PeriodicProcessDeployment[WithCanonicalProcess]] = { - (PeriodicProcessesWithJson join PeriodicProcessDeployments on (_.id === _.periodicProcessId)) - .filter { case (_, deployment) => deployment.id === id } - .result - .head - .map((PeriodicProcessesRepository.createPeriodicProcessDeployment _).tupled) + private def findProcesses( + query: Query[ + (PeriodicProcessesWithoutInputConfigJsonTable, PeriodicProcessDeploymentsTable), + (PeriodicProcessEntityWithoutInputConfigJson, PeriodicProcessDeploymentEntity), + Seq + ] + ) = { + query.result + .map(_.map { case (periodicProcess, periodicDeployment) => + PeriodicProcessesRepository.createPeriodicProcessDeployment( + periodicProcess, + periodicDeployment, + ) + }) } - override def findProcessData(processName: ProcessName): Action[Seq[PeriodicProcess[WithCanonicalProcess]]] = { - PeriodicProcessesWithJson - .filter(p => p.active === true && p.processName === processName) - .result - .map(_.map(PeriodicProcessesRepository.createPeriodicProcessWithJson)) - } + override def findProcessData(id: PeriodicProcessDeploymentId): Action[PeriodicProcessDeployment] = + findProcesses( + (PeriodicProcessesWithoutInputConfig join PeriodicProcessDeployments on (_.id === _.periodicProcessId)) + .filter { case (_, deployment) => deployment.id === id } + ).map(_.head) override def markDeployed(id: PeriodicProcessDeploymentId): Action[Unit] = { val q = for { @@ -314,9 +308,9 @@ class SlickPeriodicProcessesRepository( } override def findActiveSchedulesForProcessesHavingDeploymentWithMatchingStatus( - expectedDeploymentStatuses: Set[PeriodicProcessDeploymentStatus] + expectedDeploymentStatuses: Set[PeriodicProcessDeploymentStatus], ): Action[SchedulesState] = { - val processesHavingDeploymentsWithMatchingStatus = PeriodicProcessesWithoutJson.filter(p => + val processesHavingDeploymentsWithMatchingStatus = PeriodicProcessesWithoutInputConfig.filter(p => p.active && PeriodicProcessDeployments .filter(d => d.periodicProcessId === p.id && d.status.inSet(expectedDeploymentStatuses)) @@ -324,7 +318,7 @@ class SlickPeriodicProcessesRepository( ) getLatestDeploymentsForEachSchedule( processesHavingDeploymentsWithMatchingStatus, - deploymentsPerScheduleMaxCount = 1 + deploymentsPerScheduleMaxCount = 1, ).map(schedulesForProcessNames => SchedulesState( schedulesForProcessNames.values.map(_.schedules).foldLeft(Map.empty[ScheduleId, ScheduleData])(_ ++ _) @@ -334,26 +328,27 @@ class SlickPeriodicProcessesRepository( override def getLatestDeploymentsForActiveSchedules( processName: ProcessName, - deploymentsPerScheduleMaxCount: Int + deploymentsPerScheduleMaxCount: Int, ): Action[SchedulesState] = { - val activeProcessesQuery = PeriodicProcessesWithoutJson.filter(p => p.processName === processName && p.active) + val activeProcessesQuery = + PeriodicProcessesWithoutInputConfig.filter(p => p.processName === processName && p.active) getLatestDeploymentsForEachSchedule(activeProcessesQuery, deploymentsPerScheduleMaxCount) .map(_.getOrElse(processName, SchedulesState(Map.empty))) } override def getLatestDeploymentsForActiveSchedules( - deploymentsPerScheduleMaxCount: Int + deploymentsPerScheduleMaxCount: Int, ): Action[Map[ProcessName, SchedulesState]] = { - val activeProcessesQuery = PeriodicProcessesWithoutJson.filter(_.active) + val activeProcessesQuery = PeriodicProcessesWithoutInputConfig.filter(_.active) getLatestDeploymentsForEachSchedule(activeProcessesQuery, deploymentsPerScheduleMaxCount) } override def getLatestDeploymentsForLatestInactiveSchedules( processName: ProcessName, inactiveProcessesMaxCount: Int, - deploymentsPerScheduleMaxCount: Int + deploymentsPerScheduleMaxCount: Int, ): Action[SchedulesState] = { - val filteredProcessesQuery = PeriodicProcessesWithoutJson + val filteredProcessesQuery = PeriodicProcessesWithoutInputConfig .filter(p => p.processName === processName && !p.active) .sortBy(_.createdAt.desc) .take(inactiveProcessesMaxCount) @@ -363,9 +358,9 @@ class SlickPeriodicProcessesRepository( override def getLatestDeploymentsForLatestInactiveSchedules( inactiveProcessesMaxCount: Int, - deploymentsPerScheduleMaxCount: Int + deploymentsPerScheduleMaxCount: Int, ): Action[Map[ProcessName, SchedulesState]] = { - val filteredProcessesQuery = PeriodicProcessesWithoutJson + val filteredProcessesQuery = PeriodicProcessesWithoutInputConfig .filter(!_.active) .sortBy(_.createdAt.desc) .take(inactiveProcessesMaxCount) @@ -373,8 +368,12 @@ class SlickPeriodicProcessesRepository( } private def getLatestDeploymentsForEachSchedule( - periodicProcessesQuery: Query[PeriodicProcessWithoutJson, PeriodicProcessEntityWithoutJson, Seq], - deploymentsPerScheduleMaxCount: Int + periodicProcessesQuery: Query[ + PeriodicProcessesWithoutInputConfigJsonTable, + PeriodicProcessEntityWithoutInputConfigJson, + Seq + ], + deploymentsPerScheduleMaxCount: Int, ): Action[Map[ProcessName, SchedulesState]] = { val filteredPeriodicProcessQuery = periodicProcessesQuery.filter(p => p.processingType === processingType) val latestDeploymentsForSchedules = profile match { @@ -387,9 +386,13 @@ class SlickPeriodicProcessesRepository( } private def getLatestDeploymentsForEachSchedulePostgres( - periodicProcessesQuery: Query[PeriodicProcessWithoutJson, PeriodicProcessEntityWithoutJson, Seq], + periodicProcessesQuery: Query[ + PeriodicProcessesWithoutInputConfigJsonTable, + PeriodicProcessEntityWithoutInputConfigJson, + Seq + ], deploymentsPerScheduleMaxCount: Int - ): Action[Seq[(PeriodicProcessEntityWithoutJson, PeriodicProcessDeploymentEntity)]] = { + ): Action[Seq[(PeriodicProcessEntity, PeriodicProcessDeploymentEntity)]] = { // To effectively limit deployments to given count for each schedule in one query, we use window functions in slick import ExPostgresProfile.api._ import com.github.tminglei.slickpg.window.PgWindowFuncSupport.WindowFunctions._ @@ -421,9 +424,13 @@ class SlickPeriodicProcessesRepository( // If we decided to support more databases, we should consider some optimization like extracting periodic_schedule table // with foreign key to periodic_process and with schedule_name column - it would reduce number of queries private def getLatestDeploymentsForEachScheduleJdbcGeneric( - periodicProcessesQuery: Query[PeriodicProcessWithoutJson, PeriodicProcessEntityWithoutJson, Seq], + periodicProcessesQuery: Query[ + PeriodicProcessesWithoutInputConfigJsonTable, + PeriodicProcessEntityWithoutInputConfigJson, + Seq + ], deploymentsPerScheduleMaxCount: Int - ): Action[Seq[(PeriodicProcessEntityWithoutJson, PeriodicProcessDeploymentEntity)]] = { + ): Action[Seq[(PeriodicProcessEntity, PeriodicProcessDeploymentEntity)]] = { // It is debug instead of warn to not bloast logs when e.g. for some reasons is used hsql under the hood logger.debug( "WARN: Using not optimized version of getLatestDeploymentsForEachSchedule that not uses window functions" @@ -463,7 +470,7 @@ class SlickPeriodicProcessesRepository( scheduleName: ScheduleName, runAt: LocalDateTime, deployMaxRetries: Int - ): Action[PeriodicProcessDeployment[WithCanonicalProcess]] = { + ): Action[PeriodicProcessDeployment] = { val deploymentEntity = PeriodicProcessDeploymentEntity( id = PeriodicProcessDeploymentId(-1), periodicProcessId = id, @@ -483,19 +490,26 @@ class SlickPeriodicProcessesRepository( override def markInactive(processId: PeriodicProcessId): Action[Unit] = { val q = for { - p <- PeriodicProcessesWithoutJson if p.id === processId + p <- PeriodicProcessesWithoutInputConfig if p.id === processId } yield p.active val update = q.update(false) update.map(_ => ()) } - private def activePeriodicProcessWithDeploymentQuery = { - (PeriodicProcessesWithJson.filter(p => p.active === true && p.processingType === processingType) + def fetchInputConfigDuringExecutionJson(processName: ProcessName, versionId: VersionId): Action[Option[String]] = + PeriodicProcessesWithInputConfig + .filter(p => p.processName === processName && p.processVersionId === versionId) + .map(_.inputConfigDuringExecutionJson) + .result + .headOption + + private def activePeriodicProcessWithDeploymentQuery(processingType: String) = { + (PeriodicProcessesWithoutInputConfig.filter(p => p.active === true && p.processingType === processingType) join PeriodicProcessDeployments on (_.id === _.periodicProcessId)) } private def toSchedulesState( - list: Seq[(PeriodicProcessEntityWithoutJson, PeriodicProcessDeploymentEntity)] + list: Seq[(PeriodicProcessEntity, PeriodicProcessDeploymentEntity)] ): Map[ProcessName, SchedulesState] = { list .groupBy(_._1.processName) @@ -503,46 +517,42 @@ class SlickPeriodicProcessesRepository( } private def toSchedulesStateForSinglePeriodicProcess( - list: Seq[(PeriodicProcessEntityWithoutJson, PeriodicProcessDeploymentEntity)] + list: Seq[(PeriodicProcessEntity, PeriodicProcessDeploymentEntity)] ): SchedulesState = { SchedulesState( list .map { case (process, deployment) => val scheduleId = ScheduleId(process.id, ScheduleName(deployment.scheduleName)) val scheduleData = (scheduleId, process) - val scheduleDeployment = ScheduleDeploymentData(deployment) + val scheduleDeployment = scheduleDeploymentData(deployment) (scheduleData, scheduleDeployment) } .toList .toGroupedMap .toList .map { case ((scheduleId, process), deployments) => - scheduleId -> ScheduleData(createPeriodicProcessWithoutJson(process), deployments) + scheduleId -> ScheduleData(createPeriodicProcess(process), deployments) } .toMap ) } -} - -//Copied from designer/server. -object DBIOActionInstances { - - type DB[A] = DBIOAction[A, NoStream, Effect.All] - - implicit def dbMonad(implicit ec: ExecutionContext): Monad[DB] = new Monad[DB] { - - override def pure[A](x: A) = DBIO.successful(x) - - override def flatMap[A, B](fa: DB[A])(f: (A) => DB[B]) = fa.flatMap(f) - - // this is *not* tail recursive - override def tailRecM[A, B](a: A)(f: (A) => DB[Either[A, B]]): DB[B] = - f(a).flatMap { - case Right(r) => pure(r) - case Left(l) => tailRecM(l)(f) - } - + private def scheduleDeploymentData(deployment: PeriodicProcessDeploymentEntity): ScheduleDeploymentData = { + ScheduleDeploymentData( + deployment.id, + deployment.createdAt, + deployment.runAt, + deployment.deployedAt, + deployment.retriesLeft, + deployment.nextRetryAt, + PeriodicProcessesRepository.createPeriodicDeploymentState(deployment) + ) } + override def fetchCanonicalProcessWithVersion( + processName: ProcessName, + versionId: VersionId + ): Future[Option[(CanonicalProcess, ProcessVersion)]] = + fetchingProcessRepository.getCanonicalProcessWithVersion(processName, versionId)(NussknackerInternalUser.instance) + } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ScenarioActionRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ScenarioActionRepository.scala index 68aecc26a42..bf048ea9bf4 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ScenarioActionRepository.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ScenarioActionRepository.scala @@ -32,7 +32,7 @@ import scala.concurrent.ExecutionContext // 2. At the moment, the old ScenarioActionRepository // - handles those activities, which underlying operations may be long and may be in progress // 3. Eventually, the new ScenarioActivityRepository should be aware of the state of the underlying operation, and should replace this repository -trait ScenarioActionRepository extends LockableTable { +trait ScenarioActionRepository extends ScenarioActionReadOnlyRepository with LockableTable { def addInstantAction( processId: ProcessId, @@ -80,6 +80,10 @@ trait ScenarioActionRepository extends LockableTable { def deleteInProgressActions(): DB[Unit] +} + +trait ScenarioActionReadOnlyRepository extends LockableTable { + def getInProgressActionNames(processId: ProcessId): DB[Set[ScenarioActionName]] def getInProgressActionNames( @@ -110,10 +114,11 @@ trait ScenarioActionRepository extends LockableTable { } class DbScenarioActionRepository private ( - protected val dbRef: DbRef, + override protected val dbRef: DbRef, buildInfos: ProcessingTypeDataProvider[Map[String, String], _] )(override implicit val executionContext: ExecutionContext) - extends DbioRepository + extends DbScenarioActionReadOnlyRepository(dbRef) + with DbioRepository with NuTables with DbLockableTable with ScenarioActionRepository @@ -340,6 +345,42 @@ class DbScenarioActionRepository private ( } yield updateCount == 1 } + override def deleteInProgressActions(): DB[Unit] = { + run(scenarioActivityTable.filter(_.state === ProcessActionState.InProgress).delete.map(_ => ())) + } + + private def activityId(actionId: ProcessActionId) = + ScenarioActivityId(actionId.value) + +} + +object DbScenarioActionRepository { + + def create(dbRef: DbRef, buildInfos: ProcessingTypeDataProvider[Map[String, String], _])( + implicit executionContext: ExecutionContext, + ): ScenarioActionRepository = { + new ScenarioActionRepositoryAuditLogDecorator( + new DbScenarioActionRepository(dbRef, buildInfos) + ) + } + +} + +class DbScenarioActionReadOnlyRepository( + protected val dbRef: DbRef, +)(override implicit val executionContext: ExecutionContext) + extends DbioRepository + with NuTables + with DbLockableTable + with ScenarioActionReadOnlyRepository + with LazyLogging { + + import profile.api._ + + override type ENTITY = ScenarioActivityEntityFactory#ScenarioActivityEntity + + override protected def table: TableQuery[ScenarioActivityEntityFactory#ScenarioActivityEntity] = scenarioActivityTable + override def getInProgressActionNames(processId: ProcessId): DB[Set[ScenarioActionName]] = { val query = scenarioActivityTable .filter(action => action.scenarioId === processId && action.state === ProcessActionState.InProgress) @@ -391,10 +432,6 @@ class DbScenarioActionRepository private ( ) } - override def deleteInProgressActions(): DB[Unit] = { - run(scenarioActivityTable.filter(_.state === ProcessActionState.InProgress).delete.map(_ => ())) - } - override def getLastActionPerProcess( actionState: Set[ProcessActionState], actionNamesOpt: Option[Set[ScenarioActionName]] @@ -456,7 +493,7 @@ class DbScenarioActionRepository private ( ) } - private def toFinishedProcessAction( + protected def toFinishedProcessAction( activityEntity: ScenarioActivityEntityData ): Option[ProcessAction] = actionName(activityEntity.activityType).flatMap { actionName => (for { @@ -486,10 +523,7 @@ class DbScenarioActionRepository private ( }.toOption } - private def activityId(actionId: ProcessActionId) = - ScenarioActivityId(actionId.value) - - private def actionName(activityType: ScenarioActivityType): Option[ScenarioActionName] = { + protected def actionName(activityType: ScenarioActivityType): Option[ScenarioActionName] = { activityType match { case ScenarioActivityType.ScenarioCreated => None @@ -553,14 +587,12 @@ class DbScenarioActionRepository private ( } -object DbScenarioActionRepository { +object DbScenarioActionReadOnlyRepository { - def create(dbRef: DbRef, buildInfos: ProcessingTypeDataProvider[Map[String, String], _])( + def create(dbRef: DbRef)( implicit executionContext: ExecutionContext, - ): ScenarioActionRepository = { - new ScenarioActionRepositoryAuditLogDecorator( - new DbScenarioActionRepository(dbRef, buildInfos) - ) + ): ScenarioActionReadOnlyRepository = { + new DbScenarioActionReadOnlyRepository(dbRef) } } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala index 4faadc146f0..1b3ec275c25 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala @@ -65,6 +65,8 @@ import pl.touk.nussknacker.ui.process.newdeployment.synchronize.{ DeploymentsStatusesSynchronizer } import pl.touk.nussknacker.ui.process.newdeployment.{DeploymentRepository, DeploymentService} +import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeData +import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeData.SchedulingForProcessingType import pl.touk.nussknacker.ui.process.processingtype.{ModelClassLoaderProvider, ProcessingTypeData} import pl.touk.nussknacker.ui.process.processingtype.loader.ProcessingTypeDataLoader import pl.touk.nussknacker.ui.process.processingtype.provider.ReloadableProcessingTypeDataProvider @@ -738,7 +740,8 @@ class AkkaHttpBasedRouteProvider( sttpBackend, _ ), - modelClassLoaderProvider + modelClassLoaderProvider, + Some(dbRef), ) val loadAndNotifyIO = laodProcessingTypeDataIO .map { state => diff --git a/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala index 38aaa699fb2..6fc518f7cd2 100644 --- a/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala +++ b/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala @@ -8,7 +8,6 @@ import db.migration.V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinit import io.circe.syntax.EncoderOps import org.scalatest.freespec.AnyFreeSpecLike import org.scalatest.matchers.should.Matchers -import pl.touk.nussknacker.engine.api.deployment.ScenarioComment.WithContent import pl.touk.nussknacker.engine.api.deployment._ import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId} import pl.touk.nussknacker.engine.api.{MetaData, ProcessAdditionalFields, RequestResponseMetaData} diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/db/DbTesting.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/db/DbTesting.scala index fbc1af4f450..ce16e649949 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/db/DbTesting.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/db/DbTesting.scala @@ -93,6 +93,8 @@ trait DbTesting extends BeforeAndAfterEach with BeforeAndAfterAll { session.prepareStatement("""delete from "environments"""").execute() session.prepareStatement("""delete from "processes"""").execute() session.prepareStatement("""delete from "fingerprints"""").execute() + session.prepareStatement("""delete from "periodic_scenarios"""").execute() + session.prepareStatement("""delete from "periodic_scenario_deployments"""").execute() } } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/NuResourcesTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/NuResourcesTest.scala index 11600e5b9ed..c662bc0c32a 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/NuResourcesTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/NuResourcesTest.scala @@ -46,6 +46,7 @@ import pl.touk.nussknacker.ui.process._ import pl.touk.nussknacker.ui.process.deployment._ import pl.touk.nussknacker.ui.process.fragment.DefaultFragmentRepository import pl.touk.nussknacker.ui.process.marshall.CanonicalProcessConverter +import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeData.SchedulingForProcessingType import pl.touk.nussknacker.ui.process.processingtype._ import pl.touk.nussknacker.ui.process.processingtype.loader.ProcessingTypesConfigBasedProcessingTypeDataLoader import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider @@ -143,6 +144,7 @@ trait NuResourcesTest Streaming.stringify, modelData, deploymentManagerProvider, + SchedulingForProcessingType.NotAvailable, deploymentManagerDependencies, deploymentManagerProvider.defaultEngineSetupName, processingTypeConfig.deploymentConfig, @@ -160,7 +162,8 @@ trait NuResourcesTest .loadProcessingTypeData( _ => modelDependencies, _ => deploymentManagerDependencies, - modelClassLoaderProvider + modelClassLoaderProvider, + Some(testDbRef), ) .unsafeRunSync() ) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/mock/MockDeploymentManager.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/mock/MockDeploymentManager.scala index 4cf5180d00e..7f2a7b58edf 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/mock/MockDeploymentManager.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/mock/MockDeploymentManager.scala @@ -1,11 +1,11 @@ package pl.touk.nussknacker.test.mock -import _root_.sttp.client3.testing.SttpBackendStub import akka.actor.ActorSystem import cats.data.Validated.valid import cats.data.ValidatedNel import com.google.common.collect.LinkedHashMultimap import com.typesafe.config.Config +import sttp.client3.testing.SttpBackendStub import pl.touk.nussknacker.engine._ import pl.touk.nussknacker.engine.api.deployment._ import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus @@ -264,6 +264,7 @@ class MockDeploymentManager( override def stateQueryForAllScenariosSupport: StateQueryForAllScenariosSupport = NoStateQueryForAllScenariosSupport + override def schedulingSupport: SchedulingSupport = NoSchedulingSupport } class MockManagerProvider(deploymentManager: DeploymentManager = new MockDeploymentManager()) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/mock/MockFetchingProcessRepository.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/mock/MockFetchingProcessRepository.scala index b027c7317cf..cd11115ea55 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/mock/MockFetchingProcessRepository.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/mock/MockFetchingProcessRepository.scala @@ -1,6 +1,8 @@ package pl.touk.nussknacker.test.mock +import cats.data.OptionT import cats.instances.future._ +import pl.touk.nussknacker.engine.api.ProcessVersion import pl.touk.nussknacker.engine.api.deployment.ScenarioActionName import pl.touk.nussknacker.engine.api.graph.ScenarioGraph import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessIdWithName, ProcessName, VersionId} @@ -43,6 +45,19 @@ class MockFetchingProcessRepository private ( extends FetchingProcessRepository[Future] with BasicRepository { + override def getCanonicalProcessWithVersion(processName: ProcessName, versionId: VersionId)( + implicit user: LoggedUser + ): Future[Option[(CanonicalProcess, ProcessVersion)]] = { + val result = for { + processId <- OptionT(fetchProcessId(processName)) + details <- OptionT(fetchProcessDetailsForId[CanonicalProcess](processId, versionId)) + } yield ( + details.json, + details.toEngineProcessVersion, + ) + result.value + } + override def fetchLatestProcessesDetails[PS: ScenarioShapeFetchStrategy]( q: ScenarioQuery )(implicit loggedUser: LoggedUser, ec: ExecutionContext): Future[List[ScenarioWithDetailsEntity[PS]]] = @@ -91,8 +106,8 @@ class MockFetchingProcessRepository private ( val shapeStrategy: ScenarioShapeFetchStrategy[PS] = implicitly[ScenarioShapeFetchStrategy[PS]] shapeStrategy match { - case NotFetch => process.copy(json = ().asInstanceOf[PS]) - case FetchCanonical => process.asInstanceOf[ScenarioWithDetailsEntity[PS]] + case NotFetch => process.copy(json = ()) + case FetchCanonical => process case FetchScenarioGraph => process .mapScenario(canonical => CanonicalProcessConverter.toScenarioGraph(canonical)) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/definition/component/DefaultComponentServiceSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/definition/component/DefaultComponentServiceSpec.scala index 2e9b54cd7c7..43e0f0ef173 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/definition/component/DefaultComponentServiceSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/definition/component/DefaultComponentServiceSpec.scala @@ -41,6 +41,7 @@ import pl.touk.nussknacker.ui.definition.component.ComponentTestProcessData._ import pl.touk.nussknacker.ui.definition.component.DynamicComponentProvider._ import pl.touk.nussknacker.ui.process.DBProcessService import pl.touk.nussknacker.ui.process.fragment.DefaultFragmentRepository +import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeData.SchedulingForProcessingType import pl.touk.nussknacker.ui.process.processingtype.loader.ProcessingTypeDataLoader import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider import pl.touk.nussknacker.ui.process.processingtype.{ProcessingTypeData, ScenarioParametersService} @@ -854,6 +855,7 @@ class DefaultComponentServiceSpec processingType, modelData, new MockManagerProvider, + SchedulingForProcessingType.NotAvailable, TestFactory.deploymentManagerDependencies, EngineSetupName("Mock"), deploymentConfig = ConfigFactory.empty(), diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/ProcessStateDefinitionServiceSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/ProcessStateDefinitionServiceSpec.scala index ab0d8f6a72e..78ccd06c3c2 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/ProcessStateDefinitionServiceSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/ProcessStateDefinitionServiceSpec.scala @@ -15,6 +15,7 @@ import pl.touk.nussknacker.security.Permission import pl.touk.nussknacker.test.mock.{MockDeploymentManager, MockManagerProvider} import pl.touk.nussknacker.test.utils.domain.TestFactory import pl.touk.nussknacker.test.utils.domain.TestFactory.modelDependencies +import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeData.SchedulingForProcessingType import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider import pl.touk.nussknacker.ui.process.processingtype.{ProcessingTypeData, ValueWithRestriction} import pl.touk.nussknacker.ui.security.api.{AdminUser, CommonUser, LoggedUser} @@ -196,6 +197,7 @@ class ProcessStateDefinitionServiceSpec extends AnyFunSuite with Matchers { override def processStateDefinitionManager: ProcessStateDefinitionManager = stateDefinitionManager } ), + SchedulingForProcessingType.NotAvailable, TestFactory.deploymentManagerDependencies, deploymentConfig = ConfigFactory.empty(), engineSetupName = EngineSetupName("mock"), diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessServiceIntegrationTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessServiceIntegrationTest.scala similarity index 75% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessServiceIntegrationTest.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessServiceIntegrationTest.scala index 12b2daa5c41..7737de6761d 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessServiceIntegrationTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessServiceIntegrationTest.scala @@ -1,32 +1,49 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic import com.cronutils.builder.CronBuilder import com.cronutils.model.CronType import com.cronutils.model.definition.CronDefinitionBuilder import com.cronutils.model.field.expression.FieldExpressionFactory.{on, questionMark} -import com.dimafeng.testcontainers.{ForAllTestContainer, PostgreSQLContainer} import com.typesafe.config.{Config, ConfigFactory} import com.typesafe.scalalogging.LazyLogging +import db.util.DBIOActionInstances.DB import org.scalatest.LoneElement._ import org.scalatest.OptionValues import org.scalatest.concurrent.ScalaFutures import org.scalatest.exceptions.TestFailedException import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -import org.testcontainers.utility.DockerImageName import pl.touk.nussknacker.engine.api.deployment._ +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.{ + ProcessConfigEnricher, + ScheduledProcessEvent, + ScheduledProcessListener +} import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus.ProblemStateStatus -import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessIdWithName, ProcessName} +import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessIdWithName, ProcessName, VersionId} import pl.touk.nussknacker.engine.api.{MetaData, ProcessVersion, StreamMetaData} import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.PeriodicProcessService.PeriodicProcessStatus -import pl.touk.nussknacker.engine.management.periodic.db.{DbInitializer, SlickPeriodicProcessesRepository} -import pl.touk.nussknacker.engine.management.periodic.model._ -import pl.touk.nussknacker.engine.management.periodic.service._ import pl.touk.nussknacker.test.PatientScalaFutures +import pl.touk.nussknacker.test.base.db.WithPostgresDbTesting +import pl.touk.nussknacker.test.base.it.WithClock +import pl.touk.nussknacker.test.utils.domain.TestFactory +import pl.touk.nussknacker.test.utils.domain.TestFactory.newWriteProcessRepository +import pl.touk.nussknacker.test.utils.scalas.DBIOActionValues +import pl.touk.nussknacker.ui.process.periodic.PeriodicProcessService.PeriodicProcessStatus +import pl.touk.nussknacker.ui.process.periodic.flink.{DeploymentManagerStub, ScheduledExecutionPerformerStub} +import pl.touk.nussknacker.ui.process.periodic.legacy.db.{LegacyDbInitializer, SlickLegacyPeriodicProcessesRepository} +import pl.touk.nussknacker.ui.process.periodic.model._ +import pl.touk.nussknacker.ui.process.repository.ProcessRepository.CreateProcessAction +import pl.touk.nussknacker.ui.process.repository.{ + DBIOActionRunner, + DBProcessRepository, + PeriodicProcessesRepository, + SlickPeriodicProcessesRepository +} +import pl.touk.nussknacker.ui.security.api.AdminUser import slick.jdbc -import slick.jdbc.{JdbcBackend, JdbcProfile} +import slick.jdbc.JdbcProfile import java.time._ import java.time.temporal.ChronoUnit @@ -42,21 +59,21 @@ class PeriodicProcessServiceIntegrationTest with OptionValues with ScalaFutures with PatientScalaFutures - with ForAllTestContainer - with LazyLogging { - - override val container: PostgreSQLContainer = PostgreSQLContainer(DockerImageName.parse("postgres:11.2")) + with WithPostgresDbTesting + with LazyLogging + with WithClock + with DBIOActionValues { import scala.concurrent.ExecutionContext.Implicits.global + override protected def dbioRunner: DBIOActionRunner = new DBIOActionRunner(testDbRef) + implicit val freshnessPolicy: DataFreshnessPolicy = DataFreshnessPolicy.Fresh private val processingType = "testProcessingType" private val processName = ProcessName("test") - private val processIdWithName = ProcessIdWithName(ProcessId(1), processName) - private val sampleProcess = CanonicalProcess(MetaData(processName.value, StreamMetaData()), Nil) private val startTime = Instant.parse("2021-04-06T13:18:00Z") @@ -74,6 +91,9 @@ class PeriodicProcessServiceIntegrationTest executionConfig: PeriodicExecutionConfig = PeriodicExecutionConfig(), maxFetchedPeriodicScenarioActivities: Option[Int] = None, )(testCode: Fixture => Any): Unit = { + + val fetchingProcessRepository = TestFactory.newFutureFetchingScenarioRepository(testDbRef) + val postgresConfig = ConfigFactory.parseMap( Map( "user" -> container.username, @@ -92,43 +112,73 @@ class PeriodicProcessServiceIntegrationTest ).asJava ) - def runTestCodeWithDbConfig(config: Config) = { - val (db: jdbc.JdbcBackend.DatabaseDef, dbProfile: JdbcProfile) = DbInitializer.init(config) + def runWithLegacyRepository(dbConfig: Config): Unit = { + val (db: jdbc.JdbcBackend.DatabaseDef, dbProfile: JdbcProfile) = LegacyDbInitializer.init(dbConfig) + val creator = (processingType: String, currentTime: Instant) => + new SlickLegacyPeriodicProcessesRepository( + processingType, + db, + dbProfile, + fixedClock(currentTime), + fetchingProcessRepository, + ) try { testCode( - new Fixture(db, dbProfile, deploymentRetryConfig, executionConfig, maxFetchedPeriodicScenarioActivities) + new Fixture(creator, deploymentRetryConfig, executionConfig, maxFetchedPeriodicScenarioActivities) ) } finally { db.close() } } - logger.debug("Running test with hsql") - runTestCodeWithDbConfig(hsqlConfig) - logger.debug("Running test with postgres") - runTestCodeWithDbConfig(postgresConfig) + + def runTestCodeWithNuDb(): Unit = { + val creator = (processingType: String, currentTime: Instant) => + new SlickPeriodicProcessesRepository( + processingType, + testDbRef.db, + testDbRef.profile, + fixedClock(currentTime), + fetchingProcessRepository, + ) + testCode( + new Fixture(creator, deploymentRetryConfig, executionConfig, maxFetchedPeriodicScenarioActivities) + ) + } + + def testHeader(str: String) = "\n\n" + "*" * 100 + s"\n***** $str\n" + "*" * 100 + "\n" + logger.info(testHeader("Running test with legacy hsql-based repository")) + runWithLegacyRepository(hsqlConfig) + cleanDB() + logger.info(testHeader("Running test with legacy postgres-based repository")) + runWithLegacyRepository(postgresConfig) + cleanDB() + logger.info(testHeader("Running test with Nu database")) + runTestCodeWithNuDb() + cleanDB() } class Fixture( - db: JdbcBackend.DatabaseDef, - dbProfile: JdbcProfile, + periodicProcessesRepositoryCreator: (String, Instant) => PeriodicProcessesRepository, deploymentRetryConfig: DeploymentRetryConfig, executionConfig: PeriodicExecutionConfig, maxFetchedPeriodicScenarioActivities: Option[Int], ) { - val delegateDeploymentManagerStub = new DeploymentManagerStub - val jarManagerStub = new JarManagerStub - val events = new ArrayBuffer[PeriodicProcessEvent]() - var failListener = false - - def periodicProcessService(currentTime: Instant, processingType: String = processingType) = + val delegateDeploymentManagerStub = new DeploymentManagerStub + val scheduledExecutionPerformerStub = new ScheduledExecutionPerformerStub + val events = new ArrayBuffer[ScheduledProcessEvent]() + var failListener = false + + def periodicProcessService( + currentTime: Instant, + processingType: String = processingType + ) = new PeriodicProcessService( delegateDeploymentManager = delegateDeploymentManagerStub, - jarManager = jarManagerStub, - scheduledProcessesRepository = - new SlickPeriodicProcessesRepository(db, dbProfile, fixedClock(currentTime), processingType), - periodicProcessListener = new PeriodicProcessListener { + scheduledExecutionPerformer = scheduledExecutionPerformerStub, + periodicProcessesRepository = periodicProcessesRepositoryCreator(processingType, currentTime), + periodicProcessListener = new ScheduledProcessListener { - override def onPeriodicProcessEvent: PartialFunction[PeriodicProcessEvent, Unit] = { + override def onScheduledProcessEvent: PartialFunction[ScheduledProcessEvent, Unit] = { case k if failListener => throw new Exception(s"$k was ordered to fail") case k => events.append(k) } @@ -141,8 +191,26 @@ class PeriodicProcessServiceIntegrationTest processConfigEnricher = ProcessConfigEnricher.identity, clock = fixedClock(currentTime), new ProcessingTypeActionServiceStub, - Map.empty + Map.empty, + ) + + def writeProcessRepository: DBProcessRepository = newWriteProcessRepository(testDbRef, clock) + + def prepareProcess(processName: ProcessName): DB[ProcessIdWithName] = { + val canonicalProcess = CanonicalProcess(MetaData(processName.value, StreamMetaData()), Nil) + val action = CreateProcessAction( + processName = processName, + category = "Category1", + canonicalProcess = canonicalProcess, + processingType = "streaming", + isFragment = false, + forwardedUserName = None ) + writeProcessRepository + .saveNewProcess(action)(AdminUser("artificialTestAdmin", "artificialTestAdmin")) + .map(_.value.processId) + .map(ProcessIdWithName(_, processName)) + } } @@ -159,12 +227,15 @@ class PeriodicProcessServiceIntegrationTest def otherProcessingTypeService = f.periodicProcessService(currentTime, processingType = "other") val otherProcessName = ProcessName("other") + val processIdWithName = f.prepareProcess(processName).dbioActionValues + val otherProcessIdWithName = f.prepareProcess(otherProcessName).dbioActionValues + service .schedule( cronEveryHour, - ProcessVersion.empty.copy(processName = processName), + ProcessVersion(VersionId(1), processIdWithName.name, processIdWithName.id, List.empty, "testUser", None), sampleProcess, - randomProcessActionId + randomProcessActionId, ) .futureValue service @@ -172,7 +243,7 @@ class PeriodicProcessServiceIntegrationTest cronEvery30Minutes, ProcessVersion.empty.copy(processName = every30MinutesProcessName), sampleProcess, - randomProcessActionId + randomProcessActionId, ) .futureValue service @@ -180,15 +251,15 @@ class PeriodicProcessServiceIntegrationTest cronEvery4Hours, ProcessVersion.empty.copy(processName = every4HoursProcessName), sampleProcess, - randomProcessActionId + randomProcessActionId, ) .futureValue otherProcessingTypeService .schedule( cronEveryHour, - ProcessVersion.empty.copy(processName = otherProcessName), + ProcessVersion.empty.copy(processName = otherProcessIdWithName.name), sampleProcess, - randomProcessActionId + randomProcessActionId, ) .futureValue @@ -196,22 +267,22 @@ class PeriodicProcessServiceIntegrationTest stateAfterSchedule should have size 1 val afterSchedule = stateAfterSchedule.firstScheduleData - afterSchedule.process.processVersion.processName shouldBe processName + afterSchedule.process.deploymentData.processName shouldBe processName afterSchedule.latestDeployments.head.state shouldBe PeriodicProcessDeploymentState( None, None, PeriodicProcessDeploymentStatus.Scheduled ) afterSchedule.latestDeployments.head.runAt shouldBe localTime(expectedScheduleTime) - service.getLatestDeploymentsForActiveSchedules(otherProcessName).futureValue shouldBe empty + service.getLatestDeploymentsForActiveSchedules(otherProcessIdWithName.name).futureValue shouldBe empty currentTime = timeToTriggerCheck val allToDeploy = service.findToBeDeployed.futureValue allToDeploy.map( - _.periodicProcess.processVersion.processName + _.periodicProcess.deploymentData.processName ) should contain only (processName, every30MinutesProcessName) - val toDeploy = allToDeploy.find(_.periodicProcess.processVersion.processName == processName).value + val toDeploy = allToDeploy.find(_.periodicProcess.deploymentData.processName == processName).value service.deploy(toDeploy).futureValue otherProcessingTypeService.deploy(otherProcessingTypeService.findToBeDeployed.futureValue.loneElement).futureValue @@ -237,7 +308,7 @@ class PeriodicProcessServiceIntegrationTest // here we check that scenarios that not fired are still on the "toDeploy" list and finished are not on the list val toDeployAfterFinish = service.findToBeDeployed.futureValue - toDeployAfterFinish.map(_.periodicProcess.processVersion.processName) should contain only every30MinutesProcessName + toDeployAfterFinish.map(_.periodicProcess.deploymentData.processName) should contain only every30MinutesProcessName service.deactivate(processName).futureValue service.getLatestDeploymentsForActiveSchedules(processName).futureValue shouldBe empty val inactiveStates = service @@ -256,7 +327,7 @@ class PeriodicProcessServiceIntegrationTest val firstActivity = activities.head.asInstanceOf[ScenarioActivity.PerformedScheduledExecution] activities shouldBe List( ScenarioActivity.PerformedScheduledExecution( - scenarioId = ScenarioId(1), + scenarioId = ScenarioId(processIdWithName.id.value), scenarioActivityId = firstActivity.scenarioActivityId, user = ScenarioUser(None, UserName("Nussknacker"), None, None), date = firstActivity.date, @@ -272,14 +343,23 @@ class PeriodicProcessServiceIntegrationTest } it should "handleFinished for all finished periodic scenarios waiting for reschedule" in withFixture() { f => - val timeToTriggerCheck = startTime.plus(2, ChronoUnit.HOURS) - var currentTime = startTime - def service = f.periodicProcessService(currentTime) + val timeToTriggerCheck = startTime.plus(2, ChronoUnit.HOURS) + var currentTime = startTime + def service = f.periodicProcessService(currentTime) + val firstProcessIdWithName = f.prepareProcess(ProcessName("first")).dbioActionValues + val secondProcessIdWithName = f.prepareProcess(ProcessName("second")).dbioActionValues service .schedule( cronEveryHour, - ProcessVersion.empty.copy(processName = ProcessName("first")), + ProcessVersion( + VersionId(1), + firstProcessIdWithName.name, + firstProcessIdWithName.id, + List.empty, + "testUser", + None + ), sampleProcess, randomProcessActionId ) @@ -287,7 +367,14 @@ class PeriodicProcessServiceIntegrationTest service .schedule( cronEveryHour, - ProcessVersion.empty.copy(processName = ProcessName("second")), + ProcessVersion( + VersionId(1), + secondProcessIdWithName.name, + secondProcessIdWithName.id, + List.empty, + "testUser", + None + ), sampleProcess, randomProcessActionId ) @@ -328,15 +415,18 @@ class PeriodicProcessServiceIntegrationTest ) { f => val timeToTriggerCheck = startTime.plus(2, ChronoUnit.HOURS) var currentTime = startTime - f.jarManagerStub.deployWithJarFuture = Future.failed(new RuntimeException("Flink deploy error")) + f.scheduledExecutionPerformerStub.deployWithJarFuture = Future.failed(new RuntimeException("Flink deploy error")) def service = f.periodicProcessService(currentTime) + + val processIdWithName = f.prepareProcess(processName).dbioActionValues + service .schedule( cronEveryHour, - ProcessVersion.empty.copy(processName = processName), + ProcessVersion(VersionId(1), processIdWithName.name, processIdWithName.id, List.empty, "testUser", None), sampleProcess, - randomProcessActionId + randomProcessActionId, ) .futureValue @@ -357,7 +447,7 @@ class PeriodicProcessServiceIntegrationTest val firstActivity = activities.head.asInstanceOf[ScenarioActivity.PerformedScheduledExecution] activities shouldBe List( ScenarioActivity.PerformedScheduledExecution( - scenarioId = ScenarioId(1), + scenarioId = ScenarioId(processIdWithName.id.value), scenarioActivityId = firstActivity.scenarioActivityId, user = ScenarioUser(None, UserName("Nussknacker"), None, None), date = firstActivity.date, @@ -382,6 +472,9 @@ class PeriodicProcessServiceIntegrationTest val scheduleMinute5 = "scheduleMinute5" val scheduleMinute10 = "scheduleMinute10" + + val processIdWithName = f.prepareProcess(processName).dbioActionValues + service .schedule( MultipleScheduleProperty( @@ -390,9 +483,9 @@ class PeriodicProcessServiceIntegrationTest scheduleMinute10 -> CronScheduleProperty("0 10 * * * ?") ) ), - ProcessVersion.empty.copy(processName = processName), + ProcessVersion(VersionId(1), processIdWithName.name, processIdWithName.id, List.empty, "testUser", None), sampleProcess, - randomProcessActionId + randomProcessActionId, ) .futureValue @@ -407,7 +500,7 @@ class PeriodicProcessServiceIntegrationTest ), ProcessVersion.empty.copy(processName = ProcessName("other")), sampleProcess, - randomProcessActionId + randomProcessActionId, ) .futureValue @@ -424,7 +517,7 @@ class PeriodicProcessServiceIntegrationTest val allToDeploy = service.findToBeDeployed.futureValue allToDeploy should have length 4 - val toDeploy = allToDeploy.filter(_.periodicProcess.processVersion.processName == processName) + val toDeploy = allToDeploy.filter(_.periodicProcess.deploymentData.processName == processName) toDeploy should have length 2 toDeploy.head.runAt shouldBe localTime(expectedScheduleTime.plus(5, ChronoUnit.MINUTES)) toDeploy.head.scheduleName.value shouldBe Some(scheduleMinute5) @@ -447,6 +540,9 @@ class PeriodicProcessServiceIntegrationTest val firstSchedule = "schedule1" val secondSchedule = "schedule2" + + val processIdWithName = f.prepareProcess(processName).dbioActionValues + service .schedule( MultipleScheduleProperty( @@ -455,9 +551,9 @@ class PeriodicProcessServiceIntegrationTest secondSchedule -> CronScheduleProperty("0 5 * * * ?") ) ), - ProcessVersion.empty.copy(processName = processName), + ProcessVersion(VersionId(1), processIdWithName.name, processIdWithName.id, List.empty, "testUser", None), sampleProcess, - randomProcessActionId + randomProcessActionId, ) .futureValue @@ -467,7 +563,7 @@ class PeriodicProcessServiceIntegrationTest toDeploy should have length 2 val deployment = toDeploy.find(_.scheduleName.value.contains(firstSchedule)).value - service.deploy(deployment) + service.deploy(deployment).futureValue f.delegateDeploymentManagerStub.setStateStatus(processName, SimpleStateStatus.Running, Some(deployment.id)) val toDeployAfterDeploy = service.findToBeDeployed.futureValue @@ -487,7 +583,7 @@ class PeriodicProcessServiceIntegrationTest } firstActivity shouldBe ScenarioActivity.PerformedScheduledExecution( - scenarioId = ScenarioId(1), + scenarioId = ScenarioId(processIdWithName.id.value), scenarioActivityId = firstActivity.scenarioActivityId, user = ScenarioUser(None, UserName("Nussknacker"), None, None), date = firstActivity.date, @@ -503,7 +599,9 @@ class PeriodicProcessServiceIntegrationTest it should "handle multiple one time schedules" in withFixture() { f => handleMultipleOneTimeSchedules(f) - def service = f.periodicProcessService(startTime) + def service = f.periodicProcessService(startTime) + val processIdWithName = ProcessIdWithName(ProcessId(1), processName) + val activities = service.getScenarioActivitiesSpecificToPeriodicProcess(processIdWithName, None).futureValue val firstActivity = activities.head.asInstanceOf[ScenarioActivity.PerformedScheduledExecution] val secondActivity = activities(1).asInstanceOf[ScenarioActivity.PerformedScheduledExecution] @@ -541,7 +639,9 @@ class PeriodicProcessServiceIntegrationTest maxFetchedPeriodicScenarioActivities = Some(1) ) { f => handleMultipleOneTimeSchedules(f) - def service = f.periodicProcessService(startTime) + def service = f.periodicProcessService(startTime) + val processIdWithName = ProcessIdWithName(ProcessId(1), processName) + val activities = service.getScenarioActivitiesSpecificToPeriodicProcess(processIdWithName, None).futureValue val firstActivity = activities.head.asInstanceOf[ScenarioActivity.PerformedScheduledExecution] activities shouldBe List( @@ -578,6 +678,9 @@ class PeriodicProcessServiceIntegrationTest val schedule1 = "schedule1" val schedule2 = "schedule2" + + val processIdWithName = f.prepareProcess(processName).dbioActionValues + service .schedule( MultipleScheduleProperty( @@ -586,9 +689,9 @@ class PeriodicProcessServiceIntegrationTest schedule2 -> CronScheduleProperty(convertDateToCron(localTime(timeToTriggerSchedule2))) ) ), - ProcessVersion.empty.copy(processName = processName), + ProcessVersion(VersionId(1), processIdWithName.name, processIdWithName.id, List.empty, "testUser", None), sampleProcess, - randomProcessActionId + randomProcessActionId, ) .futureValue @@ -659,7 +762,6 @@ class PeriodicProcessServiceIntegrationTest .futureValue inactiveStates.latestDeploymentForSchedule(schedule1).state.status shouldBe PeriodicProcessDeploymentStatus.Finished inactiveStates.latestDeploymentForSchedule(schedule2).state.status shouldBe PeriodicProcessDeploymentStatus.Finished - } it should "handle failed event handler" in withFixture() { f => @@ -669,19 +771,21 @@ class PeriodicProcessServiceIntegrationTest def service = f.periodicProcessService(currentTime) - def tryWithFailedListener[T](action: () => Future[T]): T = { + def tryWithFailedListener[T](action: () => Future[T]): Unit = { f.failListener = true - intercept[TestFailedException](action().futureValue).getCause shouldBe a[PeriodicProcessException] + val exception = intercept[TestFailedException](action().futureValue) + exception.getCause shouldBe a[PeriodicProcessException] f.failListener = false - action().futureValue } + val processIdWithName = f.prepareProcess(processName).dbioActionValues + tryWithFailedListener { () => service.schedule( cronEveryHour, - ProcessVersion.empty.copy(processName = processName), + ProcessVersion(VersionId(1), processIdWithName.name, processIdWithName.id, List.empty, "testUser", None), sampleProcess, - randomProcessActionId + randomProcessActionId, ) } @@ -703,15 +807,17 @@ class PeriodicProcessServiceIntegrationTest val timeToTriggerCheck = startTime.plus(1, ChronoUnit.HOURS) var currentTime = startTime - f.jarManagerStub.deployWithJarFuture = Future.failed(new RuntimeException("Flink deploy error")) + f.scheduledExecutionPerformerStub.deployWithJarFuture = Future.failed(new RuntimeException("Flink deploy error")) def service = f.periodicProcessService(currentTime) + val processIdWithName = f.prepareProcess(processName).dbioActionValues + service .schedule( cronEveryHour, - ProcessVersion.empty.copy(processName = processName), + ProcessVersion(VersionId(1), processIdWithName.name, processIdWithName.id, List.empty, "testUser", None), sampleProcess, - randomProcessActionId + randomProcessActionId, ) .futureValue currentTime = timeToTriggerCheck @@ -732,7 +838,7 @@ class PeriodicProcessServiceIntegrationTest val firstActivity = activities.head.asInstanceOf[ScenarioActivity.PerformedScheduledExecution] activities shouldBe List( ScenarioActivity.PerformedScheduledExecution( - scenarioId = ScenarioId(1), + scenarioId = ScenarioId(processIdWithName.id.value), scenarioActivityId = firstActivity.scenarioActivityId, user = ScenarioUser(None, UserName("Nussknacker"), None, None), date = firstActivity.date, diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessesFetchingTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessesFetchingTest.scala similarity index 65% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessesFetchingTest.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessesFetchingTest.scala index a5278f94586..b296f1271ef 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessesFetchingTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/PeriodicProcessesFetchingTest.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic import org.scalatest.concurrent.ScalaFutures import org.scalatest.funsuite.AnyFunSuite @@ -9,17 +9,18 @@ import pl.touk.nussknacker.engine.api.deployment._ import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus import pl.touk.nussknacker.engine.api.process.ProcessName import pl.touk.nussknacker.engine.deployment.DeploymentData -import pl.touk.nussknacker.engine.management.periodic.db.InMemPeriodicProcessesRepository.getLatestDeploymentQueryCount -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeploymentStatus -import pl.touk.nussknacker.engine.management.periodic.service.{ - DefaultAdditionalDeploymentDataProvider, - EmptyListener, - ProcessConfigEnricher -} import pl.touk.nussknacker.test.PatientScalaFutures +import pl.touk.nussknacker.ui.process.periodic.flink.{DeploymentManagerStub, ScheduledExecutionPerformerStub} +import pl.touk.nussknacker.ui.process.periodic.flink.db.InMemPeriodicProcessesRepository +import pl.touk.nussknacker.ui.process.periodic.flink.db.InMemPeriodicProcessesRepository.getLatestDeploymentQueryCount +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeploymentStatus +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.{EmptyListener, ProcessConfigEnricher} +import pl.touk.nussknacker.ui.process.periodic.cron.CronSchedulePropertyExtractor +import pl.touk.nussknacker.ui.process.repository.{FetchingProcessRepository, PeriodicProcessesRepository} import java.time.Clock import java.util.UUID +import scala.concurrent.Future class PeriodicProcessesFetchingTest extends AnyFunSuite @@ -37,15 +38,16 @@ class PeriodicProcessesFetchingTest private def processName(n: Int) = ProcessName(s"test$n") class Fixture(executionConfig: PeriodicExecutionConfig = PeriodicExecutionConfig()) { - val repository = new db.InMemPeriodicProcessesRepository(processingType = "testProcessingType") - val delegateDeploymentManagerStub = new DeploymentManagerStub - val jarManagerStub = new JarManagerStub - val preparedDeploymentData = DeploymentData.withDeploymentId(UUID.randomUUID().toString) + val processingType = "testProcessingType" + val repository = new InMemPeriodicProcessesRepository(processingType) + val delegateDeploymentManagerStub = new DeploymentManagerStub + val scheduledExecutionPerformerStub = new ScheduledExecutionPerformerStub + val preparedDeploymentData = DeploymentData.withDeploymentId(UUID.randomUUID().toString) val periodicProcessService = new PeriodicProcessService( delegateDeploymentManager = delegateDeploymentManagerStub, - jarManager = jarManagerStub, - scheduledProcessesRepository = repository, + scheduledExecutionPerformer = scheduledExecutionPerformerStub, + periodicProcessesRepository = repository, periodicProcessListener = EmptyListener, additionalDeploymentDataProvider = DefaultAdditionalDeploymentDataProvider, deploymentRetryConfig = DeploymentRetryConfig(), @@ -60,7 +62,7 @@ class PeriodicProcessesFetchingTest val periodicDeploymentManager = new PeriodicDeploymentManager( delegate = delegateDeploymentManagerStub, service = periodicProcessService, - repository = repository, + periodicProcessesRepository = repository, schedulePropertyExtractor = CronSchedulePropertyExtractor(), toClose = () => () ) @@ -76,7 +78,8 @@ class PeriodicProcessesFetchingTest f.delegateDeploymentManagerStub.setEmptyStateStatus() for (i <- 1 to n) { - val deploymentId = f.repository.addActiveProcess(processName(i), PeriodicProcessDeploymentStatus.Deployed) + val deploymentId = + f.repository.addActiveProcess(processName(i), PeriodicProcessDeploymentStatus.Deployed) f.delegateDeploymentManagerStub.addStateStatus(processName(i), SimpleStateStatus.Running, Some(deploymentId)) } @@ -91,14 +94,17 @@ class PeriodicProcessesFetchingTest getLatestDeploymentQueryCount.get() shouldEqual 2 * n } - test("getStatusDetails - should perform 2 db queries for N periodic processes when fetching all at once") { + test( + "getAllProcessesStates - should perform 2 db queries for N periodic processes when fetching all at once" + ) { val f = new Fixture val n = 10 f.delegateDeploymentManagerStub.setEmptyStateStatus() for (i <- 1 to n) { - val deploymentId = f.repository.addActiveProcess(processName(i), PeriodicProcessDeploymentStatus.Deployed) + val deploymentId = + f.repository.addActiveProcess(processName(i), PeriodicProcessDeploymentStatus.Deployed) f.delegateDeploymentManagerStub.addStateStatus(processName(i), SimpleStateStatus.Running, Some(deploymentId)) } diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/CronSchedulePropertyExtractorTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/CronSchedulePropertyExtractorTest.scala similarity index 73% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/CronSchedulePropertyExtractorTest.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/CronSchedulePropertyExtractorTest.scala index 5874bd1bb26..4b0c326fd2e 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/CronSchedulePropertyExtractorTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/CronSchedulePropertyExtractorTest.scala @@ -1,14 +1,16 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.flink import org.scalatest.Inside import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import pl.touk.nussknacker.engine.api.NodeId +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{ScheduleProperty => ApiScheduleProperty} import pl.touk.nussknacker.engine.api.parameter.ParameterName import pl.touk.nussknacker.engine.build.ScenarioBuilder import pl.touk.nussknacker.engine.graph.expression.Expression -import pl.touk.nussknacker.engine.management.periodic.cron.CronParameterValidator import pl.touk.nussknacker.test.{EitherValuesDetailedMessage, ValidatedValuesDetailedMessage} +import pl.touk.nussknacker.ui.process.periodic.cron.{CronParameterValidator, CronSchedulePropertyExtractor} +import pl.touk.nussknacker.ui.process.periodic.{CronScheduleProperty, MultipleScheduleProperty} class CronSchedulePropertyExtractorTest extends AnyFunSuite @@ -39,16 +41,16 @@ class CronSchedulePropertyExtractorTest test("should extract cron property") { val result = extractor(PeriodicProcessGen.buildCanonicalProcess()) - inside(result) { case Right(CronScheduleProperty(_)) => } + inside(result) { case Right(ApiScheduleProperty.CronScheduleProperty(_)) => } } test("should extract MultipleScheduleProperty") { val multipleSchedulesExpression = "{foo: '0 0 * * * ?', bar: '1 0 * * * ?'}" val result = extractor(PeriodicProcessGen.buildCanonicalProcess(multipleSchedulesExpression)) - result.rightValue shouldEqual MultipleScheduleProperty( + result.rightValue shouldEqual ApiScheduleProperty.MultipleScheduleProperty( Map( - "foo" -> CronScheduleProperty("0 0 * * * ?"), - "bar" -> CronScheduleProperty("1 0 * * * ?") + "foo" -> ApiScheduleProperty.CronScheduleProperty("0 0 * * * ?"), + "bar" -> ApiScheduleProperty.CronScheduleProperty("1 0 * * * ?") ) ) diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/CronSchedulePropertyTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/CronSchedulePropertyTest.scala similarity index 89% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/CronSchedulePropertyTest.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/CronSchedulePropertyTest.scala index 5fff9dcb12d..0177cd9fea3 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/CronSchedulePropertyTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/CronSchedulePropertyTest.scala @@ -1,8 +1,10 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.flink -import java.time.{Clock, LocalDateTime, ZoneId, ZonedDateTime} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import pl.touk.nussknacker.ui.process.periodic.CronScheduleProperty + +import java.time.{Clock, LocalDateTime, ZoneId, ZonedDateTime} class CronSchedulePropertyTest extends AnyFunSuite with Matchers { diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/DeploymentActorTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/DeploymentActorTest.scala similarity index 63% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/DeploymentActorTest.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/DeploymentActorTest.scala index ac646390e38..eaeed51fe9e 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/DeploymentActorTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/DeploymentActorTest.scala @@ -1,15 +1,14 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.flink import akka.actor.{ActorRef, ActorSystem} import akka.testkit.{TestKit, TestKitBase, TestProbe} -import org.scalatest.LoneElement._ import org.scalatest.BeforeAndAfterAll +import org.scalatest.LoneElement._ import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.DeploymentActor.CheckToBeDeployed -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData.WithCanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeployment +import pl.touk.nussknacker.ui.process.periodic.DeploymentActor +import pl.touk.nussknacker.ui.process.periodic.DeploymentActor.CheckToBeDeployed +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeployment import scala.concurrent.Future import scala.concurrent.duration._ @@ -34,11 +33,11 @@ class DeploymentActorTest extends AnyFunSuite with TestKitBase with Matchers wit } private def shouldFindToBeDeployedScenarios( - result: Future[Seq[PeriodicProcessDeployment[WithCanonicalProcess]]] + result: Future[Seq[PeriodicProcessDeployment]] ): Unit = { val probe = TestProbe() var counter = 0 - def findToBeDeployed: Future[Seq[PeriodicProcessDeployment[WithCanonicalProcess]]] = { + def findToBeDeployed: Future[Seq[PeriodicProcessDeployment]] = { counter += 1 probe.ref ! s"invoked $counter" result @@ -55,14 +54,14 @@ class DeploymentActorTest extends AnyFunSuite with TestKitBase with Matchers wit } test("should deploy found scenario") { - val probe = TestProbe() - val waitingDeployment = PeriodicProcessDeploymentGen() - var toBeDeployed: Seq[PeriodicProcessDeployment[WithCanonicalProcess]] = Seq(waitingDeployment) - var actor: ActorRef = null - def findToBeDeployed: Future[Seq[PeriodicProcessDeployment[WithCanonicalProcess]]] = { + val probe = TestProbe() + val waitingDeployment = PeriodicProcessDeploymentGen() + var toBeDeployed: Seq[PeriodicProcessDeployment] = Seq(waitingDeployment) + var actor: ActorRef = null + def findToBeDeployed: Future[Seq[PeriodicProcessDeployment]] = { Future.successful(toBeDeployed) } - def deploy(deployment: PeriodicProcessDeployment[WithCanonicalProcess]): Future[Unit] = { + def deploy(deployment: PeriodicProcessDeployment): Future[Unit] = { probe.ref ! deployment // Simulate periodic check for waiting scenarios while deploying a scenario. actor ! CheckToBeDeployed diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/DeploymentManagerStub.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/DeploymentManagerStub.scala similarity index 93% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/DeploymentManagerStub.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/DeploymentManagerStub.scala index e424331f837..8e59980e28d 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/DeploymentManagerStub.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/DeploymentManagerStub.scala @@ -1,11 +1,11 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.flink import pl.touk.nussknacker.engine.api.deployment._ import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus import pl.touk.nussknacker.engine.api.process.{ProcessIdWithName, ProcessName, VersionId} import pl.touk.nussknacker.engine.deployment.{DeploymentId, ExternalDeploymentId} -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeploymentId import pl.touk.nussknacker.engine.testing.StubbingCommands +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeploymentId import scala.concurrent.Future @@ -82,6 +82,8 @@ class DeploymentManagerStub extends BaseDeploymentManager with StubbingCommands override def deploymentSynchronisationSupport: DeploymentSynchronisationSupport = NoDeploymentSynchronisationSupport + override def schedulingSupport: SchedulingSupport = NoSchedulingSupport + override def stateQueryForAllScenariosSupport: StateQueryForAllScenariosSupport = new StateQueryForAllScenariosSupported { diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/FlinkClientStub.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/FlinkClientStub.scala similarity index 96% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/FlinkClientStub.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/FlinkClientStub.scala index 5c386e88208..b1b260d1a7d 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/FlinkClientStub.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/FlinkClientStub.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.flink import org.apache.flink.configuration.Configuration import pl.touk.nussknacker.engine.api.deployment.{DataFreshnessPolicy, SavepointResult, WithDataFreshnessStatus} diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicDeploymentManagerTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicDeploymentManagerTest.scala similarity index 94% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicDeploymentManagerTest.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicDeploymentManagerTest.scala index 82f1c575b0b..eb968cee662 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicDeploymentManagerTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicDeploymentManagerTest.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.flink import org.scalatest.concurrent.ScalaFutures import org.scalatest.funsuite.AnyFunSuite @@ -13,15 +13,14 @@ import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessIdWithName, Pro import pl.touk.nussknacker.engine.api.{MetaData, ProcessVersion, StreamMetaData} import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.engine.deployment.{DeploymentData, User} -import pl.touk.nussknacker.engine.management.periodic.PeriodicProcessService.PeriodicProcessStatus -import pl.touk.nussknacker.engine.management.periodic.PeriodicStateStatus.{ScheduledStatus, WaitingForScheduleStatus} -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeploymentStatus -import pl.touk.nussknacker.engine.management.periodic.service.{ - DefaultAdditionalDeploymentDataProvider, - EmptyListener, - ProcessConfigEnricher -} import pl.touk.nussknacker.test.PatientScalaFutures +import pl.touk.nussknacker.ui.process.periodic.PeriodicProcessService.PeriodicProcessStatus +import pl.touk.nussknacker.ui.process.periodic.PeriodicStateStatus.{ScheduledStatus, WaitingForScheduleStatus} +import pl.touk.nussknacker.ui.process.periodic._ +import pl.touk.nussknacker.ui.process.periodic.flink.db.InMemPeriodicProcessesRepository +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeploymentStatus +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.{EmptyListener, ProcessConfigEnricher} +import pl.touk.nussknacker.ui.process.periodic.cron.CronSchedulePropertyExtractor import java.time.{Clock, LocalDateTime, ZoneOffset} import java.util.UUID @@ -59,15 +58,15 @@ class PeriodicDeploymentManagerTest ) class Fixture(executionConfig: PeriodicExecutionConfig = PeriodicExecutionConfig()) { - val repository = new db.InMemPeriodicProcessesRepository(processingType = "testProcessingType") - val delegateDeploymentManagerStub = new DeploymentManagerStub - val jarManagerStub = new JarManagerStub - val preparedDeploymentData = DeploymentData.withDeploymentId(UUID.randomUUID().toString) + val repository = new InMemPeriodicProcessesRepository(processingType = "testProcessingType") + val delegateDeploymentManagerStub = new DeploymentManagerStub + val scheduledExecutionPerformerStub = new ScheduledExecutionPerformerStub + val preparedDeploymentData = DeploymentData.withDeploymentId(UUID.randomUUID().toString) val periodicProcessService = new PeriodicProcessService( delegateDeploymentManager = delegateDeploymentManagerStub, - jarManager = jarManagerStub, - scheduledProcessesRepository = repository, + scheduledExecutionPerformer = scheduledExecutionPerformerStub, + periodicProcessesRepository = repository, periodicProcessListener = EmptyListener, additionalDeploymentDataProvider = DefaultAdditionalDeploymentDataProvider, deploymentRetryConfig = DeploymentRetryConfig(), @@ -76,15 +75,15 @@ class PeriodicDeploymentManagerTest processConfigEnricher = ProcessConfigEnricher.identity, clock = Clock.systemDefaultZone(), new ProcessingTypeActionServiceStub, - Map.empty + Map.empty, ) val periodicDeploymentManager = new PeriodicDeploymentManager( delegate = delegateDeploymentManagerStub, service = periodicProcessService, - repository = repository, + periodicProcessesRepository = repository, schedulePropertyExtractor = CronSchedulePropertyExtractor(), - toClose = () => () + toClose = () => (), ) def getAllowedActions( diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessDeploymentGen.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessDeploymentGen.scala similarity index 53% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessDeploymentGen.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessDeploymentGen.scala index cf813b7b011..dc7ab3014bd 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessDeploymentGen.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessDeploymentGen.scala @@ -1,14 +1,6 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.flink -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData.WithCanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.{ - PeriodicProcessDeployment, - PeriodicProcessDeploymentId, - PeriodicProcessDeploymentState, - PeriodicProcessDeploymentStatus, - ScheduleName -} +import pl.touk.nussknacker.ui.process.periodic.model.{PeriodicProcessDeployment, PeriodicProcessDeploymentId, PeriodicProcessDeploymentState, PeriodicProcessDeploymentStatus, ScheduleName} import java.time.LocalDateTime @@ -16,7 +8,7 @@ object PeriodicProcessDeploymentGen { val now: LocalDateTime = LocalDateTime.now() - def apply(): PeriodicProcessDeployment[WithCanonicalProcess] = { + def apply(): PeriodicProcessDeployment = { PeriodicProcessDeployment( id = PeriodicProcessDeploymentId(42), periodicProcess = PeriodicProcessGen(), diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessGen.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessGen.scala new file mode 100644 index 00000000000..7111db87542 --- /dev/null +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessGen.scala @@ -0,0 +1,39 @@ +package pl.touk.nussknacker.ui.process.periodic.flink + +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{DeploymentWithRuntimeParams, RuntimeParams} +import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId} +import pl.touk.nussknacker.engine.build.ScenarioBuilder +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess +import pl.touk.nussknacker.ui.process.periodic.CronScheduleProperty +import pl.touk.nussknacker.ui.process.periodic.cron.CronSchedulePropertyExtractor.CronPropertyDefaultName +import pl.touk.nussknacker.ui.process.periodic.model.{PeriodicProcess, PeriodicProcessId} + +import java.time.LocalDateTime + +object PeriodicProcessGen { + + def apply(): PeriodicProcess = { + PeriodicProcess( + id = PeriodicProcessId(42), + deploymentData = DeploymentWithRuntimeParams( + processId = Some(ProcessId(1)), + processName = ProcessName(""), + versionId = VersionId.initialVersionId, + runtimeParams = RuntimeParams(Map("jarFileName" -> "jar-file-name.jar")) + ), + scheduleProperty = CronScheduleProperty("0 0 * * * ?"), + active = true, + createdAt = LocalDateTime.now(), + None + ) + } + + def buildCanonicalProcess(cronProperty: String = "0 0 * * * ?"): CanonicalProcess = { + ScenarioBuilder + .streaming("test") + .additionalFields(properties = Map(CronPropertyDefaultName -> cronProperty)) + .source("test", "test") + .emptySink("test", "test") + } + +} diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessServiceTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessServiceTest.scala similarity index 81% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessServiceTest.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessServiceTest.scala index a258d5c2ac4..c8c631a1e5c 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessServiceTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessServiceTest.scala @@ -1,37 +1,29 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.flink import com.typesafe.config.{ConfigFactory, ConfigValueFactory} +import org.scalatest.OptionValues import org.scalatest.concurrent.ScalaFutures import org.scalatest.exceptions.TestFailedException -import org.scalatest.prop.TableDrivenPropertyChecks -import org.scalatest.OptionValues import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import org.scalatest.prop.TableDrivenPropertyChecks import pl.touk.nussknacker.engine.api.ProcessVersion -import pl.touk.nussknacker.engine.api.deployment.{DataFreshnessPolicy, ProcessActionId, ProcessingTypeActionServiceStub} +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.ScheduledDeploymentDetails +import pl.touk.nussknacker.engine.api.deployment.scheduler.services._ +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.AdditionalDeploymentDataProvider +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.ProcessConfigEnricher.EnrichedProcessConfig import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus.ProblemStateStatus -import pl.touk.nussknacker.engine.api.process.ProcessName +import pl.touk.nussknacker.engine.api.deployment.{DataFreshnessPolicy, ProcessActionId, ProcessingTypeActionServiceStub} +import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId} import pl.touk.nussknacker.engine.build.ScenarioBuilder -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.PeriodicProcessService.PeriodicProcessStatus -import pl.touk.nussknacker.engine.management.periodic.db.PeriodicProcessesRepository.createPeriodicProcessDeployment -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData.WithCanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus -import pl.touk.nussknacker.engine.management.periodic.model.{PeriodicProcessDeployment, PeriodicProcessDeploymentStatus} -import pl.touk.nussknacker.engine.management.periodic.service.ProcessConfigEnricher.EnrichedProcessConfig -import pl.touk.nussknacker.engine.management.periodic.service.{ - AdditionalDeploymentDataProvider, - DeployedEvent, - FailedOnDeployEvent, - FailedOnRunEvent, - FinishedEvent, - PeriodicProcessEvent, - PeriodicProcessListener, - ProcessConfigEnricher, - ScheduledEvent -} import pl.touk.nussknacker.test.PatientScalaFutures +import pl.touk.nussknacker.ui.process.periodic.PeriodicProcessService.PeriodicProcessStatus +import pl.touk.nussknacker.ui.process.periodic._ +import pl.touk.nussknacker.ui.process.periodic.flink.db.InMemPeriodicProcessesRepository +import pl.touk.nussknacker.ui.process.periodic.flink.db.InMemPeriodicProcessesRepository.createPeriodicProcessDeployment +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeploymentStatus +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus import java.time.temporal.ChronoField import java.time.{Clock, LocalDate, LocalDateTime} @@ -63,22 +55,31 @@ class PeriodicProcessServiceTest .source("start", "source") .emptySink("end", "KafkaSink") + private val processVersion = ProcessVersion( + versionId = VersionId(1), + processName = processName, + processId = ProcessId(1), + labels = List.empty, + user = "testUser", + modelVersion = None, + ) + class Fixture { - val repository = new db.InMemPeriodicProcessesRepository(processingType = "testProcessingType") - val delegateDeploymentManagerStub = new DeploymentManagerStub - val jarManagerStub = new JarManagerStub - val events = new ArrayBuffer[PeriodicProcessEvent]() - val additionalData = Map("testMap" -> "testValue") + val repository = new InMemPeriodicProcessesRepository(processingType = "testProcessingType") + val delegateDeploymentManagerStub = new DeploymentManagerStub + val scheduledExecutionPerformerStub = new ScheduledExecutionPerformerStub + val events = new ArrayBuffer[ScheduledProcessEvent]() + val additionalData = Map("testMap" -> "testValue") val actionService: ProcessingTypeActionServiceStub = new ProcessingTypeActionServiceStub val periodicProcessService = new PeriodicProcessService( delegateDeploymentManager = delegateDeploymentManagerStub, - jarManager = jarManagerStub, - scheduledProcessesRepository = repository, - new PeriodicProcessListener { + scheduledExecutionPerformer = scheduledExecutionPerformerStub, + periodicProcessesRepository = repository, + new ScheduledProcessListener { - override def onPeriodicProcessEvent: PartialFunction[PeriodicProcessEvent, Unit] = { case k => + override def onScheduledProcessEvent: PartialFunction[ScheduledProcessEvent, Unit] = { case k => events.append(k) } @@ -86,9 +87,9 @@ class PeriodicProcessServiceTest additionalDeploymentDataProvider = new AdditionalDeploymentDataProvider { override def prepareAdditionalData( - runDetails: PeriodicProcessDeployment[WithCanonicalProcess] + runDetails: ScheduledDeploymentDetails ): Map[String, String] = - additionalData + ("runId" -> runDetails.id.value.toString) + additionalData + ("runId" -> runDetails.id.toString) }, DeploymentRetryConfig(), @@ -103,7 +104,7 @@ class PeriodicProcessServiceTest EnrichedProcessConfig( initialScheduleData.inputConfigDuringExecution.withValue( "processName", - ConfigValueFactory.fromAnyRef(initialScheduleData.canonicalProcess.name.value) + ConfigValueFactory.fromAnyRef(processName.value) ) ) ) @@ -115,7 +116,7 @@ class PeriodicProcessServiceTest Future.successful( EnrichedProcessConfig( deployData.inputConfigDuringExecution - .withValue("runAt", ConfigValueFactory.fromAnyRef(deployData.deployment.runAt.toString)) + .withValue("runAt", ConfigValueFactory.fromAnyRef(deployData.deploymentDetails.runAt.toString)) ) ) } @@ -123,7 +124,7 @@ class PeriodicProcessServiceTest }, Clock.systemDefaultZone(), actionService, - Map.empty + Map.empty, ) } @@ -185,7 +186,10 @@ class PeriodicProcessServiceTest val finished :: scheduled :: Nil = f.repository.deploymentEntities.map(createPeriodicProcessDeployment(processEntity, _)).toList - f.events.toList shouldBe List(FinishedEvent(finished, None), ScheduledEvent(scheduled, firstSchedule = false)) + f.events.toList shouldBe List( + FinishedEvent(finished.toDetails, canonicalProcess, None), + ScheduledEvent(scheduled.toDetails, firstSchedule = false) + ) } // Flink job could not be available in Flink console if checked too quickly after submit. @@ -238,8 +242,12 @@ class PeriodicProcessServiceTest val finished :: scheduled :: Nil = f.repository.deploymentEntities.map(createPeriodicProcessDeployment(processEntity, _)).toList f.events.toList shouldBe List( - FinishedEvent(finished, f.delegateDeploymentManagerStub.jobStatus.get(processName).flatMap(_.headOption)), - ScheduledEvent(scheduled, firstSchedule = false) + FinishedEvent( + finished.toDetails, + canonicalProcess, + f.delegateDeploymentManagerStub.jobStatus.get(processName).flatMap(_.headOption) + ), + ScheduledEvent(scheduled.toDetails, firstSchedule = false) ) } @@ -280,9 +288,13 @@ class PeriodicProcessServiceTest f.repository.deploymentEntities.loneElement.status shouldBe PeriodicProcessDeploymentStatus.Finished // TODO: active should be false val event = - createPeriodicProcessDeployment(processEntity.copy(active = true), f.repository.deploymentEntities.loneElement) + createPeriodicProcessDeployment( + processEntity.copy(active = true), + f.repository.deploymentEntities.loneElement, + ) f.events.loneElement shouldBe FinishedEvent( - event, + event.toDetails, + canonicalProcess, f.delegateDeploymentManagerStub.jobStatus.get(processName).flatMap(_.headOption) ) } @@ -330,7 +342,7 @@ class PeriodicProcessServiceTest val f = new Fixture f.periodicProcessService - .schedule(CronScheduleProperty("0 0 * * * ?"), ProcessVersion.empty, canonicalProcess, randomProcessActionId) + .schedule(CronScheduleProperty("0 0 * * * ?"), processVersion, canonicalProcess, randomProcessActionId) .futureValue val processEntity = f.repository.processEntities.loneElement @@ -342,7 +354,10 @@ class PeriodicProcessServiceTest deploymentEntity.status shouldBe PeriodicProcessDeploymentStatus.Scheduled f.events.toList shouldBe List( - ScheduledEvent(createPeriodicProcessDeployment(processEntity, deploymentEntity), firstSchedule = true) + ScheduledEvent( + createPeriodicProcessDeployment(processEntity, deploymentEntity).toDetails, + firstSchedule = true + ) ) } @@ -358,10 +373,11 @@ class PeriodicProcessServiceTest processEntity.active shouldBe true f.repository.deploymentEntities.loneElement.status shouldBe PeriodicProcessDeploymentStatus.Failed - val expectedDetails = createPeriodicProcessDeployment(processEntity, f.repository.deploymentEntities.head) + val expectedDetails = + createPeriodicProcessDeployment(processEntity, f.repository.deploymentEntities.head) f.events.toList shouldBe List( FailedOnRunEvent( - expectedDetails, + expectedDetails.toDetails, f.delegateDeploymentManagerStub.jobStatus.get(processName).flatMap(_.headOption) ) ) @@ -372,7 +388,7 @@ class PeriodicProcessServiceTest f.repository.addActiveProcess(processName, PeriodicProcessDeploymentStatus.Scheduled) val toSchedule = createPeriodicProcessDeployment( f.repository.processEntities.loneElement, - f.repository.deploymentEntities.loneElement + f.repository.deploymentEntities.loneElement, ) f.periodicProcessService.deploy(toSchedule).futureValue @@ -380,21 +396,22 @@ class PeriodicProcessServiceTest val deploymentEntity = f.repository.deploymentEntities.loneElement deploymentEntity.status shouldBe PeriodicProcessDeploymentStatus.Deployed ConfigFactory - .parseString(f.jarManagerStub.lastDeploymentWithJarData.value.inputConfigDuringExecutionJson) + .parseString(f.scheduledExecutionPerformerStub.lastInputConfigDuringExecutionJson.value) .getString("runAt") shouldBe deploymentEntity.runAt.toString - val expectedDetails = createPeriodicProcessDeployment(f.repository.processEntities.loneElement, deploymentEntity) - f.events.toList shouldBe List(DeployedEvent(expectedDetails, None)) + val expectedDetails = + createPeriodicProcessDeployment(f.repository.processEntities.loneElement, deploymentEntity) + f.events.toList shouldBe List(DeployedEvent(expectedDetails.toDetails, None)) } test("deploy - should handle failed deployment") { val f = new Fixture f.repository.addActiveProcess(processName, PeriodicProcessDeploymentStatus.Scheduled) - f.jarManagerStub.deployWithJarFuture = Future.failed(new RuntimeException("Flink deploy error")) + f.scheduledExecutionPerformerStub.deployWithJarFuture = Future.failed(new RuntimeException("Flink deploy error")) val toSchedule = createPeriodicProcessDeployment( f.repository.processEntities.loneElement, - f.repository.deploymentEntities.loneElement + f.repository.deploymentEntities.loneElement, ) f.periodicProcessService.deploy(toSchedule).futureValue @@ -403,9 +420,9 @@ class PeriodicProcessServiceTest val expectedDetails = createPeriodicProcessDeployment( f.repository.processEntities.loneElement, - f.repository.deploymentEntities.loneElement + f.repository.deploymentEntities.loneElement, ) - f.events.toList shouldBe List(FailedOnDeployEvent(expectedDetails, None)) + f.events.toList shouldBe List(FailedOnDeployEvent(expectedDetails.toDetails, None)) } test("Schedule new scenario only if at least one date in the future") { @@ -413,7 +430,7 @@ class PeriodicProcessServiceTest def tryToSchedule(schedule: ScheduleProperty): Unit = f.periodicProcessService - .schedule(schedule, ProcessVersion.empty, canonicalProcess, randomProcessActionId) + .schedule(schedule, processVersion, canonicalProcess, randomProcessActionId) .futureValue tryToSchedule(cronInFuture) shouldBe (()) diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessStateDefinitionManagerTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessStateDefinitionManagerTest.scala similarity index 86% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessStateDefinitionManagerTest.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessStateDefinitionManagerTest.scala index 77e551054a2..49f51d166d1 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessStateDefinitionManagerTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/PeriodicProcessStateDefinitionManagerTest.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.flink import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -6,10 +6,11 @@ import pl.touk.nussknacker.engine.api.deployment.ProcessStateDefinitionManager.P import pl.touk.nussknacker.engine.api.deployment.ScenarioActionName import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus import pl.touk.nussknacker.engine.api.process.VersionId -import pl.touk.nussknacker.engine.management.periodic.PeriodicProcessService.{DeploymentStatus, PeriodicProcessStatus} -import pl.touk.nussknacker.engine.management.periodic.PeriodicProcessStateDefinitionManager.statusTooltip -import pl.touk.nussknacker.engine.management.periodic.PeriodicStateStatus.ScheduledStatus -import pl.touk.nussknacker.engine.management.periodic.model._ +import pl.touk.nussknacker.ui.process.periodic.PeriodicProcessService.{PeriodicDeploymentStatus, PeriodicProcessStatus} +import pl.touk.nussknacker.ui.process.periodic.PeriodicProcessStateDefinitionManager.statusTooltip +import pl.touk.nussknacker.ui.process.periodic.PeriodicStateStatus +import pl.touk.nussknacker.ui.process.periodic.PeriodicStateStatus.ScheduledStatus +import pl.touk.nussknacker.ui.process.periodic.model._ import java.time.LocalDateTime import java.util.concurrent.atomic.AtomicLong @@ -29,7 +30,7 @@ class PeriodicProcessStateDefinitionManagerTest extends AnyFunSuite with Matcher private val nextScheduleId = new AtomicLong() test("display periodic deployment status for not named schedule") { - val deploymentStatus = DeploymentStatus( + val deploymentStatus = PeriodicDeploymentStatus( generateDeploymentId, notNamedScheduleId, fooCreatedAt, @@ -44,7 +45,7 @@ class PeriodicProcessStateDefinitionManagerTest extends AnyFunSuite with Matcher test("display sorted periodic deployment status for named schedules") { val firstScheduleId = generateScheduleId - val firstDeploymentStatus = DeploymentStatus( + val firstDeploymentStatus = PeriodicDeploymentStatus( generateDeploymentId, firstScheduleId, fooCreatedAt.minusMinutes(1), @@ -54,7 +55,7 @@ class PeriodicProcessStateDefinitionManagerTest extends AnyFunSuite with Matcher None ) val secScheduleId = generateScheduleId - val secDeploymentStatus = DeploymentStatus( + val secDeploymentStatus = PeriodicDeploymentStatus( generateDeploymentId, secScheduleId, fooCreatedAt, diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/RescheduleFinishedActorTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/RescheduleFinishedActorTest.scala similarity index 91% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/RescheduleFinishedActorTest.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/RescheduleFinishedActorTest.scala index 720364a78d4..aa05869e6ec 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/RescheduleFinishedActorTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/RescheduleFinishedActorTest.scala @@ -1,10 +1,11 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.flink import akka.actor.ActorSystem import akka.testkit.{TestKit, TestKitBase, TestProbe} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import org.scalatest.BeforeAndAfterAll +import pl.touk.nussknacker.ui.process.periodic.RescheduleFinishedActor import scala.concurrent.Future import scala.concurrent.duration._ diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/ScheduledExecutionPerformerStub.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/ScheduledExecutionPerformerStub.scala new file mode 100644 index 00000000000..40a4d76ca41 --- /dev/null +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/ScheduledExecutionPerformerStub.scala @@ -0,0 +1,49 @@ +package pl.touk.nussknacker.ui.process.periodic.flink + +import com.typesafe.config.ConfigFactory +import pl.touk.nussknacker.engine.api.ProcessVersion +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{DeploymentWithRuntimeParams, RuntimeParams} +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.ScheduledExecutionPerformer +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess +import pl.touk.nussknacker.engine.deployment.{DeploymentData, ExternalDeploymentId} +import pl.touk.nussknacker.engine.modelconfig.InputConfigDuringExecution + +import scala.concurrent.Future + +class ScheduledExecutionPerformerStub extends ScheduledExecutionPerformer { + + var deployWithJarFuture: Future[Option[ExternalDeploymentId]] = Future.successful(None) + var lastDeploymentWithRuntimeParams: Option[DeploymentWithRuntimeParams] = None + var lastInputConfigDuringExecutionJson: Option[String] = None + + override def prepareDeploymentWithRuntimeParams( + processVersion: ProcessVersion, + ): Future[DeploymentWithRuntimeParams] = { + Future.successful( + DeploymentWithRuntimeParams( + processId = Some(processVersion.processId), + processName = processVersion.processName, + versionId = processVersion.versionId, + runtimeParams = RuntimeParams(Map("jarFileName" -> "")) + ) + ) + } + + override def provideInputConfigDuringExecutionJson(): Future[InputConfigDuringExecution] = + Future.successful(InputConfigDuringExecution(ConfigFactory.parseString(""))) + + override def deployWithRuntimeParams( + deploymentWithJarData: DeploymentWithRuntimeParams, + inputConfigDuringExecutionJson: String, + deploymentData: DeploymentData, + canonicalProcess: CanonicalProcess, + processVersion: ProcessVersion, + ): Future[Option[ExternalDeploymentId]] = { + lastDeploymentWithRuntimeParams = Some(deploymentWithJarData) + lastInputConfigDuringExecutionJson = Some(inputConfigDuringExecutionJson) + deployWithJarFuture + } + + override def cleanAfterDeployment(runtimeParams: RuntimeParams): Future[Unit] = Future.successful(()) + +} diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/JarManagerTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/ScheduledExecutionPerformerTest.scala similarity index 53% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/JarManagerTest.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/ScheduledExecutionPerformerTest.scala index b63433a8795..2dc0e14252d 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/JarManagerTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/ScheduledExecutionPerformerTest.scala @@ -1,28 +1,26 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.flink import com.typesafe.config.ConfigFactory import org.scalatest.concurrent.ScalaFutures import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import pl.touk.nussknacker.engine.api.ProcessVersion +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{DeploymentWithRuntimeParams, RuntimeParams} +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.ScheduledExecutionPerformer import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} -import pl.touk.nussknacker.engine.api.{MetaData, ProcessVersion, StreamMetaData} -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.management.FlinkModelJarProvider -import pl.touk.nussknacker.engine.management.periodic.flink.FlinkJarManager -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData +import pl.touk.nussknacker.engine.management.{FlinkModelJarProvider, FlinkScheduledExecutionPerformer} import pl.touk.nussknacker.engine.modelconfig.InputConfigDuringExecution import pl.touk.nussknacker.test.PatientScalaFutures import java.nio.file.{Files, Path, Paths} import scala.concurrent.Future -class JarManagerTest extends AnyFunSuite with Matchers with ScalaFutures with PatientScalaFutures { +class ScheduledExecutionPerformerTest extends AnyFunSuite with Matchers with ScalaFutures with PatientScalaFutures { private val processName = "test" private val processVersionId = 5 private val processVersion = ProcessVersion.empty.copy(processName = ProcessName(processName), versionId = VersionId(processVersionId)) - private val process = CanonicalProcess(MetaData("foo", StreamMetaData()), Nil) private val jarsDir = Files.createTempDirectory("jars-dir") private val modelJarFileContent = "abc".getBytes @@ -34,14 +32,14 @@ class JarManagerTest extends AnyFunSuite with Matchers with ScalaFutures with Pa private val currentModelUrls = List(currentModelJarFile.toURI.toURL) - private val jarManager = createJarManager(jarsDir = jarsDir) + private val scheduledExecutionPerformer = createScheduledExecutionPerformer(jarsDir = jarsDir) - private def createJarManager( + private def createScheduledExecutionPerformer( jarsDir: Path, modelJarProvider: FlinkModelJarProvider = new FlinkModelJarProvider(currentModelUrls) - ): JarManager = { + ): ScheduledExecutionPerformer = { - new FlinkJarManager( + new FlinkScheduledExecutionPerformer( flinkClient = new FlinkClientStub, jarsDir = jarsDir, inputConfigDuringExecution = InputConfigDuringExecution(ConfigFactory.empty()), @@ -50,9 +48,9 @@ class JarManagerTest extends AnyFunSuite with Matchers with ScalaFutures with Pa } test("prepareDeploymentWithJar - should copy to local dir") { - val result = jarManager.prepareDeploymentWithJar(processVersion, process) + val result = scheduledExecutionPerformer.prepareDeploymentWithRuntimeParams(processVersion) - val copiedJarFileName = result.futureValue.jarFileName + val copiedJarFileName = result.futureValue.runtimeParams.params("jarFileName") copiedJarFileName should fullyMatch regex s"^$processName-$processVersionId-\\d+\\.jar$$" val copiedJarFile = jarsDir.resolve(copiedJarFileName) Files.exists(copiedJarFile) shouldBe true @@ -60,33 +58,33 @@ class JarManagerTest extends AnyFunSuite with Matchers with ScalaFutures with Pa } test("prepareDeploymentWithJar - should handle disappearing model JAR") { - val modelJarProvider = new FlinkModelJarProvider(currentModelUrls) - val jarManager = createJarManager(jarsDir, modelJarProvider) + val modelJarProvider = new FlinkModelJarProvider(currentModelUrls) + val scheduledExecutionPerformer = createScheduledExecutionPerformer(jarsDir, modelJarProvider) - def verifyAndDeleteJar(result: Future[DeploymentWithJarData.WithCanonicalProcess]): Unit = { - val copiedJarFile = jarsDir.resolve(result.futureValue.jarFileName) + def verifyAndDeleteJar(result: Future[DeploymentWithRuntimeParams]): Unit = { + val copiedJarFile = jarsDir.resolve(result.futureValue.runtimeParams.params("jarFileName")) Files.exists(copiedJarFile) shouldBe true Files.readAllBytes(copiedJarFile) shouldBe modelJarFileContent Files.delete(copiedJarFile) } - verifyAndDeleteJar(jarManager.prepareDeploymentWithJar(processVersion, process)) + verifyAndDeleteJar(scheduledExecutionPerformer.prepareDeploymentWithRuntimeParams(processVersion)) modelJarProvider.getJobJar().delete() shouldBe true - verifyAndDeleteJar(jarManager.prepareDeploymentWithJar(processVersion, process)) + verifyAndDeleteJar(scheduledExecutionPerformer.prepareDeploymentWithRuntimeParams(processVersion)) } test("prepareDeploymentWithJar - should create jars dir if not exists") { - val tmpDir = System.getProperty("java.io.tmpdir") - val jarsDir = Paths.get(tmpDir, s"jars-dir-not-exists-${System.currentTimeMillis()}") - val jarManager = createJarManager(jarsDir = jarsDir) + val tmpDir = System.getProperty("java.io.tmpdir") + val jarsDir = Paths.get(tmpDir, s"jars-dir-not-exists-${System.currentTimeMillis()}") + val scheduledExecutionPerformer = createScheduledExecutionPerformer(jarsDir = jarsDir) Files.exists(jarsDir) shouldBe false - val result = jarManager.prepareDeploymentWithJar(processVersion, process) + val result = scheduledExecutionPerformer.prepareDeploymentWithRuntimeParams(processVersion) - val copiedJarFileName = result.futureValue.jarFileName + val copiedJarFileName = result.futureValue.runtimeParams.params("jarFileName") Files.exists(jarsDir) shouldBe true Files.exists(jarsDir.resolve(copiedJarFileName)) shouldBe true } @@ -96,13 +94,14 @@ class JarManagerTest extends AnyFunSuite with Matchers with ScalaFutures with Pa val jarPath = jarsDir.resolve(jarFileName) Files.copy(currentModelJarFile.toPath, jarPath) - jarManager.deleteJar(jarFileName).futureValue + scheduledExecutionPerformer.cleanAfterDeployment(RuntimeParams(Map("jarFileName" -> jarFileName))).futureValue Files.exists(jarPath) shouldBe false } test("deleteJar - should handle not existing file") { - val result = jarManager.deleteJar("unknown.jar").futureValue + val result = + scheduledExecutionPerformer.cleanAfterDeployment(RuntimeParams(Map("jarFileName" -> "unknown.jar"))).futureValue result shouldBe (()) } diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/UtilsSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/UtilsSpec.scala similarity index 94% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/UtilsSpec.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/UtilsSpec.scala index a6d53da9c16..ace8ffe0fc7 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/UtilsSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/UtilsSpec.scala @@ -1,4 +1,4 @@ -package pl.touk.nussknacker.engine.management.periodic +package pl.touk.nussknacker.ui.process.periodic.flink import akka.actor.{Actor, ActorSystem, Props} import akka.testkit.TestKit @@ -6,7 +6,8 @@ import com.typesafe.scalalogging.LazyLogging import org.scalatest.BeforeAndAfterAll import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike -import pl.touk.nussknacker.engine.management.periodic.Utils.createActorWithRetry +import pl.touk.nussknacker.ui.process.periodic.Utils +import pl.touk.nussknacker.ui.process.periodic.Utils.createActorWithRetry import scala.concurrent.duration.Duration import scala.concurrent.{Await, Future} diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/db/InMemPeriodicProcessesRepository.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/db/InMemPeriodicProcessesRepository.scala similarity index 53% rename from engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/db/InMemPeriodicProcessesRepository.scala rename to designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/db/InMemPeriodicProcessesRepository.scala index 2ebb103de28..16a14532615 100644 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/db/InMemPeriodicProcessesRepository.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/periodic/flink/db/InMemPeriodicProcessesRepository.scala @@ -1,20 +1,18 @@ -package pl.touk.nussknacker.engine.management.periodic.db +package pl.touk.nussknacker.ui.process.periodic.flink.db -import cats.{Id, Monad} import io.circe.syntax.EncoderOps +import pl.touk.nussknacker.engine.api.ProcessVersion +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{DeploymentWithRuntimeParams, RuntimeParams} import pl.touk.nussknacker.engine.api.deployment.ProcessActionId -import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} +import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId} import pl.touk.nussknacker.engine.build.ScenarioBuilder -import pl.touk.nussknacker.engine.management.periodic._ -import pl.touk.nussknacker.engine.management.periodic.db.InMemPeriodicProcessesRepository.{ - DeploymentIdSequence, - ProcessIdSequence, - getLatestDeploymentQueryCount -} -import pl.touk.nussknacker.engine.management.periodic.db.PeriodicProcessesRepository.createPeriodicProcessDeployment -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData.WithCanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus -import pl.touk.nussknacker.engine.management.periodic.model._ +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess +import pl.touk.nussknacker.ui.process.periodic._ +import pl.touk.nussknacker.ui.process.periodic.flink.db.InMemPeriodicProcessesRepository._ +import pl.touk.nussknacker.ui.process.periodic.flink.db.InMemPeriodicProcessesRepository.getLatestDeploymentQueryCount +import pl.touk.nussknacker.ui.process.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus +import pl.touk.nussknacker.ui.process.periodic.model._ +import pl.touk.nussknacker.ui.process.repository.PeriodicProcessesRepository import java.time.chrono.ChronoLocalDateTime import java.time.{LocalDateTime, ZoneId} @@ -24,25 +22,23 @@ import scala.collection.mutable.ListBuffer import scala.concurrent.Future import scala.util.Random -object InMemPeriodicProcessesRepository { - private val ProcessIdSequence = new AtomicLong(0) - private val DeploymentIdSequence = new AtomicLong(0) - - val getLatestDeploymentQueryCount = new AtomicLong(0) -} - class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicProcessesRepository { - var processEntities: mutable.ListBuffer[PeriodicProcessEntityWithJson] = ListBuffer.empty - var deploymentEntities: mutable.ListBuffer[PeriodicProcessDeploymentEntity] = ListBuffer.empty + override type Action[T] = Future[T] - private implicit val localDateOrdering: Ordering[LocalDateTime] = Ordering.by(identity[ChronoLocalDateTime[_]]) + override def run[T](action: Future[T]): Future[T] = action - override type Action[T] = Id[T] + var processEntities: mutable.ListBuffer[TestPeriodicProcessEntity] = ListBuffer.empty + var deploymentEntities: mutable.ListBuffer[TestPeriodicProcessDeploymentEntity] = ListBuffer.empty - override implicit def monad: Monad[Id] = cats.catsInstancesForId + private def canonicalProcess(processName: ProcessName) = { + ScenarioBuilder + .streaming(processName.value) + .source("start", "source") + .emptySink("end", "KafkaSink") + } - override def run[T](action: Id[T]): Future[T] = Future.successful(action) + private implicit val localDateOrdering: Ordering[LocalDateTime] = Ordering.by(identity[ChronoLocalDateTime[_]]) def addActiveProcess( processName: ProcessName, @@ -71,17 +67,14 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP processActionId: Option[ProcessActionId] = None ): PeriodicProcessId = { val id = PeriodicProcessId(ProcessIdSequence.incrementAndGet()) - val entity = PeriodicProcessEntityWithJson( + val entity = TestPeriodicProcessEntity( id = id, + processId = None, processName = processName, processVersionId = VersionId.initialVersionId, processingType = processingType, - processJson = ScenarioBuilder - .streaming(processName.value) - .source("start", "source") - .emptySink("end", "KafkaSink"), inputConfigDuringExecutionJson = "{}", - jarFileName = "", + runtimeParams = RuntimeParams(Map("jarFileName" -> "")), scheduleProperty = scheduleProperty.asJson.noSpaces, active = true, createdAt = LocalDateTime.now(), @@ -100,7 +93,7 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP deployedAt: Option[LocalDateTime] = None, ): PeriodicProcessDeploymentId = { val id = PeriodicProcessDeploymentId(DeploymentIdSequence.incrementAndGet()) - val entity = PeriodicProcessDeploymentEntity( + val entity = TestPeriodicProcessDeploymentEntity( id = id, periodicProcessId = periodicProcessId, createdAt = LocalDateTime.now(), @@ -119,46 +112,49 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP override def getSchedulesState( scenarioName: ProcessName, after: Option[LocalDateTime], - ): Action[SchedulesState] = { + ): Future[SchedulesState] = Future.successful { val filteredProcesses = processEntities.filter { pe => pe.processName == scenarioName && deploymentEntities.exists(d => d.periodicProcessId == pe.id) }.toSeq getLatestDeploymentsForPeriodicProcesses(filteredProcesses, deploymentsPerScheduleMaxCount = Int.MaxValue) } - override def markInactive(processId: PeriodicProcessId): Unit = + override def markInactive(processId: PeriodicProcessId): Future[Unit] = Future.successful { processEntities.zipWithIndex .find { case (process, _) => process.id == processId } .foreach { case (process, index) => processEntities.update(index, process.copy(active = false)) } + } override def create( - deploymentWithJarData: DeploymentWithJarData.WithCanonicalProcess, + deploymentWithRuntimeParams: DeploymentWithRuntimeParams, + inputConfigDuringExecutionJson: String, + canonicalProcess: CanonicalProcess, scheduleProperty: ScheduleProperty, processActionId: ProcessActionId, - ): PeriodicProcess[WithCanonicalProcess] = { + ): Future[PeriodicProcess] = Future.successful { val id = PeriodicProcessId(Random.nextLong()) - val periodicProcess = PeriodicProcessEntityWithJson( + val periodicProcess = TestPeriodicProcessEntity( id = id, - processName = deploymentWithJarData.processVersion.processName, - processVersionId = deploymentWithJarData.processVersion.versionId, + processId = deploymentWithRuntimeParams.processId, + processName = deploymentWithRuntimeParams.processName, + processVersionId = deploymentWithRuntimeParams.versionId, processingType = processingType, - processJson = deploymentWithJarData.process, - inputConfigDuringExecutionJson = deploymentWithJarData.inputConfigDuringExecutionJson, - jarFileName = deploymentWithJarData.jarFileName, + inputConfigDuringExecutionJson = inputConfigDuringExecutionJson, + runtimeParams = deploymentWithRuntimeParams.runtimeParams, scheduleProperty = scheduleProperty.asJson.noSpaces, active = true, createdAt = LocalDateTime.now(), processActionId = Some(processActionId) ) processEntities += periodicProcess - PeriodicProcessesRepository.createPeriodicProcessWithJson(periodicProcess) + createPeriodicProcessWithJson(periodicProcess) } override def findActiveSchedulesForProcessesHavingDeploymentWithMatchingStatus( - expectedDeploymentStatuses: Set[PeriodicProcessDeploymentStatus] - ): Action[SchedulesState] = { + expectedDeploymentStatuses: Set[PeriodicProcessDeploymentStatus], + ): Future[SchedulesState] = Future.successful { val filteredProcesses = processEntities.filter { pe => pe.processingType == processingType && deploymentEntities.exists(d => d.periodicProcessId == pe.id && expectedDeploymentStatuses.contains(d.status)) @@ -168,8 +164,8 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP override def getLatestDeploymentsForActiveSchedules( processName: ProcessName, - deploymentsPerScheduleMaxCount: Int - ): Action[SchedulesState] = { + deploymentsPerScheduleMaxCount: Int, + ): Future[SchedulesState] = Future.successful { getLatestDeploymentQueryCount.incrementAndGet() getLatestDeploymentsForPeriodicProcesses( processEntities(processName).filter(_.active), @@ -179,7 +175,7 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP override def getLatestDeploymentsForActiveSchedules( deploymentsPerScheduleMaxCount: Int - ): Action[Map[ProcessName, SchedulesState]] = { + ): Future[Map[ProcessName, SchedulesState]] = Future.successful { getLatestDeploymentQueryCount.incrementAndGet() allProcessEntities.map { case (processName, list) => processName -> getLatestDeploymentsForPeriodicProcesses( @@ -192,8 +188,8 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP override def getLatestDeploymentsForLatestInactiveSchedules( processName: ProcessName, inactiveProcessesMaxCount: Int, - deploymentsPerScheduleMaxCount: Int - ): Action[SchedulesState] = { + deploymentsPerScheduleMaxCount: Int, + ): Future[SchedulesState] = Future.successful { getLatestDeploymentQueryCount.incrementAndGet() val filteredProcesses = processEntities(processName).filterNot(_.active).sortBy(_.createdAt).takeRight(inactiveProcessesMaxCount) @@ -203,7 +199,7 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP override def getLatestDeploymentsForLatestInactiveSchedules( inactiveProcessesMaxCount: Int, deploymentsPerScheduleMaxCount: Int - ): Action[Map[ProcessName, SchedulesState]] = { + ): Future[Map[ProcessName, SchedulesState]] = Future.successful { getLatestDeploymentQueryCount.incrementAndGet() allProcessEntities.map { case (processName, list) => processName -> getLatestDeploymentsForPeriodicProcesses( @@ -214,9 +210,9 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP } private def getLatestDeploymentsForPeriodicProcesses( - processes: Seq[PeriodicProcessEntity], + processes: Seq[TestPeriodicProcessEntity], deploymentsPerScheduleMaxCount: Int - ): SchedulesState = + ): SchedulesState = { SchedulesState((for { process <- processes deploymentGroupedByScheduleName <- deploymentEntities @@ -227,49 +223,48 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP val ds = deployments .sortBy(d => -d.runAt.atZone(ZoneId.systemDefault()).toInstant.toEpochMilli) .take(deploymentsPerScheduleMaxCount) - .map(ScheduleDeploymentData(_)) + .map(scheduleDeploymentData(_)) .toList - scheduleId -> ScheduleData(PeriodicProcessesRepository.createPeriodicProcessWithoutJson(process), ds) + scheduleId -> ScheduleData(createPeriodicProcessWithoutJson(process), ds) } } yield deploymentGroupedByScheduleName).toMap) + } - override def findToBeDeployed: Seq[PeriodicProcessDeployment[WithCanonicalProcess]] = { + override def findToBeDeployed: Future[Seq[PeriodicProcessDeployment]] = { val scheduled = findActive(PeriodicProcessDeploymentStatus.Scheduled) readyToRun(scheduled) } - override def findToBeRetried: Action[Seq[PeriodicProcessDeployment[WithCanonicalProcess]]] = { + override def findToBeRetried: Future[Seq[PeriodicProcessDeployment]] = { val toBeRetried = findActive(PeriodicProcessDeploymentStatus.FailedOnDeploy).filter(_.retriesLeft > 0) readyToRun(toBeRetried) } - override def findProcessData(id: PeriodicProcessDeploymentId): PeriodicProcessDeployment[WithCanonicalProcess] = + override def findProcessData( + id: PeriodicProcessDeploymentId, + ): Future[PeriodicProcessDeployment] = Future.successful { (for { d <- deploymentEntities if d.id == id p <- processEntities if p.id == d.periodicProcessId } yield createPeriodicProcessDeployment(p, d)).head + } - override def findProcessData(processName: ProcessName): Seq[PeriodicProcess[WithCanonicalProcess]] = - processEntities(processName) - .filter(_.active) - .map(PeriodicProcessesRepository.createPeriodicProcessWithJson) - - private def allProcessEntities: Map[ProcessName, Seq[PeriodicProcessEntity]] = + private def processEntities(processName: ProcessName): Seq[TestPeriodicProcessEntity] = processEntities - .filter(process => process.processingType == processingType) + .filter(process => process.processName == processName && process.processingType == processingType) .toSeq - .groupBy(_.processName) - private def processEntities(processName: ProcessName): Seq[PeriodicProcessEntityWithJson] = + private def allProcessEntities: Map[ProcessName, Seq[TestPeriodicProcessEntity]] = processEntities - .filter(process => process.processName == processName && process.processingType == processingType) + .filter(process => process.processingType == processingType) .toSeq + .groupBy(_.processName) - override def markDeployed(id: PeriodicProcessDeploymentId): Unit = { + override def markDeployed(id: PeriodicProcessDeploymentId): Future[Unit] = Future.successful { update(id)(_.copy(status = PeriodicProcessDeploymentStatus.Deployed, deployedAt = Some(LocalDateTime.now()))) } - override def markFinished(id: PeriodicProcessDeploymentId): Unit = { + override def markFinished(id: PeriodicProcessDeploymentId): Future[Unit] = Future.successful { update(id)(_.copy(status = PeriodicProcessDeploymentStatus.Finished, completedAt = Some(LocalDateTime.now()))) } @@ -278,7 +273,7 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP status: PeriodicProcessDeploymentStatus, deployRetries: Int, retryAt: Option[LocalDateTime] - ): Action[Unit] = { + ): Future[Unit] = Future.successful { update(id)( _.copy( status = status, @@ -289,7 +284,7 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP ) } - override def markFailed(id: PeriodicProcessDeploymentId): Unit = { + override def markFailed(id: PeriodicProcessDeploymentId): Future[Unit] = Future.successful { update(id)( _.copy( status = PeriodicProcessDeploymentStatus.Failed, @@ -302,9 +297,9 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP id: PeriodicProcessId, scheduleName: ScheduleName, runAt: LocalDateTime, - deployMaxRetries: Int - ): PeriodicProcessDeployment[WithCanonicalProcess] = { - val deploymentEntity = PeriodicProcessDeploymentEntity( + deployMaxRetries: Int, + ): Future[PeriodicProcessDeployment] = Future.successful { + val deploymentEntity = TestPeriodicProcessDeploymentEntity( id = PeriodicProcessDeploymentId(Random.nextLong()), periodicProcessId = id, createdAt = LocalDateTime.now(), @@ -317,12 +312,13 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP status = PeriodicProcessDeploymentStatus.Scheduled ) deploymentEntities += deploymentEntity - createPeriodicProcessDeployment(processEntities.find(_.id == id).head, deploymentEntity) + val processEntity = processEntities.find(_.id == id).head + createPeriodicProcessDeployment(processEntity, deploymentEntity) } private def update( id: PeriodicProcessDeploymentId - )(action: PeriodicProcessDeploymentEntity => PeriodicProcessDeploymentEntity): Unit = { + )(action: TestPeriodicProcessDeploymentEntity => TestPeriodicProcessDeploymentEntity): Unit = { deploymentEntities.zipWithIndex .find { case (deployment, _) => deployment.id == id } .foreach { case (deployment, index) => @@ -330,26 +326,157 @@ class InMemPeriodicProcessesRepository(processingType: String) extends PeriodicP } } - private def findActive( - status: PeriodicProcessDeploymentStatus - ): Seq[PeriodicProcessDeployment[WithCanonicalProcess]] = + private def findActive(status: PeriodicProcessDeploymentStatus): Seq[PeriodicProcessDeployment] = findActive( Seq(status) ) private def findActive( statusList: Seq[PeriodicProcessDeploymentStatus] - ): Seq[PeriodicProcessDeployment[WithCanonicalProcess]] = + ): Seq[PeriodicProcessDeployment] = (for { p <- processEntities if p.active && p.processingType == processingType d <- deploymentEntities if d.periodicProcessId == p.id && statusList.contains(d.status) } yield createPeriodicProcessDeployment(p, d)).toSeq private def readyToRun( - deployments: Seq[PeriodicProcessDeployment[WithCanonicalProcess]] - ): Seq[PeriodicProcessDeployment[WithCanonicalProcess]] = { + deployments: Seq[PeriodicProcessDeployment] + ): Future[Seq[PeriodicProcessDeployment]] = { val now = LocalDateTime.now() - deployments.filter(d => d.runAt.isBefore(now) || d.runAt.isEqual(now)) + Future.successful(deployments.filter(d => d.runAt.isBefore(now) || d.runAt.isEqual(now))) + } + + override def fetchCanonicalProcessWithVersion( + processName: ProcessName, + versionId: VersionId + ): Future[Option[(CanonicalProcess, ProcessVersion)]] = Future.successful { + Some(canonicalProcess(processName), ProcessVersion.empty) + } + + override def fetchInputConfigDuringExecutionJson( + processName: ProcessName, + versionId: VersionId + ): Future[Option[String]] = + Future.successful(Some("{}")) + +} + +object InMemPeriodicProcessesRepository { + + val getLatestDeploymentQueryCount = new AtomicLong(0) + + private val ProcessIdSequence = new AtomicLong(0) + private val DeploymentIdSequence = new AtomicLong(0) + + final case class TestPeriodicProcessEntity( + id: PeriodicProcessId, + processId: Option[ProcessId], + processName: ProcessName, + processVersionId: VersionId, + processingType: String, + inputConfigDuringExecutionJson: String, + runtimeParams: RuntimeParams, + scheduleProperty: String, + active: Boolean, + createdAt: LocalDateTime, + processActionId: Option[ProcessActionId] + ) + + case class TestPeriodicProcessDeploymentEntity( + id: PeriodicProcessDeploymentId, + periodicProcessId: PeriodicProcessId, + createdAt: LocalDateTime, + runAt: LocalDateTime, + scheduleName: Option[String], + deployedAt: Option[LocalDateTime], + completedAt: Option[LocalDateTime], + retriesLeft: Int, + nextRetryAt: Option[LocalDateTime], + status: PeriodicProcessDeploymentStatus + ) + + def createPeriodicProcessDeployment( + processEntity: TestPeriodicProcessEntity, + processDeploymentEntity: TestPeriodicProcessDeploymentEntity + ): PeriodicProcessDeployment = { + val process = createPeriodicProcessWithJson(processEntity) + PeriodicProcessDeployment( + processDeploymentEntity.id, + process, + processDeploymentEntity.createdAt, + processDeploymentEntity.runAt, + ScheduleName(processDeploymentEntity.scheduleName), + processDeploymentEntity.retriesLeft, + processDeploymentEntity.nextRetryAt, + createPeriodicDeploymentState(processDeploymentEntity) + ) + } + + def createPeriodicDeploymentState( + processDeploymentEntity: TestPeriodicProcessDeploymentEntity + ): PeriodicProcessDeploymentState = { + PeriodicProcessDeploymentState( + processDeploymentEntity.deployedAt, + processDeploymentEntity.completedAt, + processDeploymentEntity.status + ) + } + + def createPeriodicProcessWithJson( + processEntity: TestPeriodicProcessEntity + ): PeriodicProcess = { + val scheduleProperty = prepareScheduleProperty(processEntity) + PeriodicProcess( + processEntity.id, + DeploymentWithRuntimeParams( + processId = processEntity.processId, + processName = processEntity.processName, + versionId = processEntity.processVersionId, + runtimeParams = processEntity.runtimeParams, + ), + scheduleProperty, + processEntity.active, + processEntity.createdAt, + processEntity.processActionId + ) + } + + def createPeriodicProcessWithoutJson( + processEntity: TestPeriodicProcessEntity + ): PeriodicProcess = { + val scheduleProperty = prepareScheduleProperty(processEntity) + PeriodicProcess( + processEntity.id, + DeploymentWithRuntimeParams( + processId = processEntity.processId, + processName = processEntity.processName, + versionId = processEntity.processVersionId, + runtimeParams = processEntity.runtimeParams, + ), + scheduleProperty, + processEntity.active, + processEntity.createdAt, + processEntity.processActionId + ) + } + + private def prepareScheduleProperty(processEntity: TestPeriodicProcessEntity) = { + val scheduleProperty = io.circe.parser + .decode[ScheduleProperty](processEntity.scheduleProperty) + .fold(e => throw new IllegalArgumentException(e), identity) + scheduleProperty + } + + private def scheduleDeploymentData(deployment: TestPeriodicProcessDeploymentEntity): ScheduleDeploymentData = { + ScheduleDeploymentData( + deployment.id, + deployment.createdAt, + deployment.runAt, + deployment.deployedAt, + deployment.retriesLeft, + deployment.nextRetryAt, + createPeriodicDeploymentState(deployment) + ) } } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeDataProviderSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeDataProviderSpec.scala index 64aa228791e..e76cb1f6f9a 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeDataProviderSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeDataProviderSpec.scala @@ -10,6 +10,7 @@ import pl.touk.nussknacker.engine.testing.{DeploymentManagerProviderStub, LocalM import pl.touk.nussknacker.security.Permission import pl.touk.nussknacker.test.utils.domain.TestFactory import pl.touk.nussknacker.ui.UnauthorizedError +import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeData.SchedulingForProcessingType import pl.touk.nussknacker.ui.process.processingtype.loader.LocalProcessingTypeDataLoader import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider import pl.touk.nussknacker.ui.security.api.RealLoggedUser @@ -58,7 +59,8 @@ class ProcessingTypeDataProviderSpec extends AnyFunSuite with Matchers { .loadProcessingTypeData( _ => modelDependencies, _ => TestFactory.deploymentManagerDependencies, - ModelClassLoaderProvider(allProcessingTypes.map(_ -> ModelClassLoaderDependencies(List.empty, None)).toMap) + ModelClassLoaderProvider(allProcessingTypes.map(_ -> ModelClassLoaderDependencies(List.empty, None)).toMap), + dbRef = None, ) .unsafeRunSync() } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ScenarioParametersServiceTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ScenarioParametersServiceTest.scala index 792c9141fa1..4b5deae86c6 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ScenarioParametersServiceTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/processingtype/ScenarioParametersServiceTest.scala @@ -20,6 +20,7 @@ import pl.touk.nussknacker.security.Permission import pl.touk.nussknacker.test.ValidatedValuesDetailedMessage import pl.touk.nussknacker.test.utils.domain.TestFactory import pl.touk.nussknacker.ui.config.DesignerConfig +import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeData.SchedulingForProcessingType import pl.touk.nussknacker.ui.process.processingtype.loader.ProcessingTypesConfigBasedProcessingTypeDataLoader import pl.touk.nussknacker.ui.security.api.{LoggedUser, RealLoggedUser} @@ -301,7 +302,8 @@ class ScenarioParametersServiceTest designerConfig.processingTypeConfigs.configByProcessingType.mapValuesNow(conf => ModelClassLoaderDependencies(conf.classPath, None) ) - ) + ), + dbRef = None, ) .unsafeRunSync() val parametersService = processingTypeData.getCombined().parametersService diff --git a/docs/Changelog.md b/docs/Changelog.md index ef7c6979fc8..370f3e7431d 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -55,6 +55,9 @@ * [#7446](https://github.com/TouK/nussknacker/pull/7446) Small changes regarding node errors in fragments used in scenarios: * Fragment error node tips in scenarios are now clickable and open problematic node edit window in a new tab. * Fragment nodes are now highlighted when they contain nodes with errors. +* [#7364](https://github.com/TouK/nussknacker/pull/7364) PeriodicDeploymentManger is no longer a separate DM, but instead is an optional functionality and decorator for all DMs + * in order to use it, DM must implement interface `schedulingSupported`, that handles deployments on a specific engine + * implementation provided for Flink DM ## 1.18 diff --git a/docs/MigrationGuide.md b/docs/MigrationGuide.md index fd2975f560e..43052d7b8f0 100644 --- a/docs/MigrationGuide.md +++ b/docs/MigrationGuide.md @@ -40,9 +40,43 @@ To see the biggest differences please consult the [changelog](Changelog.md). * [#7379](https://github.com/TouK/nussknacker/pull/7379) Removed CustomAction mechanism. If there were any custom actions defined in some custom DeploymentManager implementation, they should be modified to use the predefined set of actions or otherwise replaced by custom links and handled outside Nussknacker. +* [#7364](https://github.com/TouK/nussknacker/pull/7364) + * the PeriodicDeploymentManager is no longer a separate DM type + * in `scenarioTypes` config section, the `deploymentConfig` of a periodic scenario type (only Flink was supported so far) may have looked like that: + ```hocon + deploymentConfig: { + type: "flinkPeriodic" + restUrl: "http://jobmanager:8081" + shouldVerifyBeforeDeploy: true + deploymentManager { + db: { }, + processingType: streaming, + jarsDir: ./storage/jars + } + } + ``` + * changes: + * the `type: "flinkPeriodic"` is no longer supported, instead `type: "flinkStreaming"` with additional setting `supportsPeriodicExecution: true` should be used + * the db config is now optional - the periodic DM may still use its custom datasource defined here in `legacyDb` section + * when custom `db` section not defined here, then main Nussknacker db will be used + * config after changes may look like that: + ```hocon + deploymentConfig: { + type: "flinkStreaming" + scheduling { + enabled: true + processingType: streaming, + jarsDir: ./storage/jars + legacyDb: { }, + } + restUrl: "http://jobmanager:8081" + shouldVerifyBeforeDeploy: true + } + ``` ### Code API changes * [#7368](https://github.com/TouK/nussknacker/pull/7368) Renamed `PeriodicSourceFactory` to `SampleGeneratorSourceFactory` +* [#7364](https://github.com/TouK/nussknacker/pull/7364) The DeploymentManager must implement `def schedulingSupport: SchedulingSupport`. If support not added, then `NoSchedulingSupport` should be used. ## In version 1.18.0 diff --git a/engine/development/deploymentManager/src/main/scala/pl/touk/nussknacker/development/manager/DevelopmentDeploymentManagerProvider.scala b/engine/development/deploymentManager/src/main/scala/pl/touk/nussknacker/development/manager/DevelopmentDeploymentManagerProvider.scala index 7a247f11d78..8d9ac57b817 100644 --- a/engine/development/deploymentManager/src/main/scala/pl/touk/nussknacker/development/manager/DevelopmentDeploymentManagerProvider.scala +++ b/engine/development/deploymentManager/src/main/scala/pl/touk/nussknacker/development/manager/DevelopmentDeploymentManagerProvider.scala @@ -190,6 +190,8 @@ class DevelopmentDeploymentManager(actorSystem: ActorSystem, modelData: BaseMode override def deploymentSynchronisationSupport: DeploymentSynchronisationSupport = NoDeploymentSynchronisationSupport override def stateQueryForAllScenariosSupport: StateQueryForAllScenariosSupport = NoStateQueryForAllScenariosSupport + + override def schedulingSupport: SchedulingSupport = NoSchedulingSupport } class DevelopmentDeploymentManagerProvider extends DeploymentManagerProvider { diff --git a/engine/development/deploymentManager/src/main/scala/pl/touk/nussknacker/development/manager/MockableDeploymentManagerProvider.scala b/engine/development/deploymentManager/src/main/scala/pl/touk/nussknacker/development/manager/MockableDeploymentManagerProvider.scala index 7627c005fb7..8ae2fa10945 100644 --- a/engine/development/deploymentManager/src/main/scala/pl/touk/nussknacker/development/manager/MockableDeploymentManagerProvider.scala +++ b/engine/development/deploymentManager/src/main/scala/pl/touk/nussknacker/development/manager/MockableDeploymentManagerProvider.scala @@ -117,6 +117,8 @@ object MockableDeploymentManagerProvider { override def stateQueryForAllScenariosSupport: StateQueryForAllScenariosSupport = NoStateQueryForAllScenariosSupport + override def schedulingSupport: SchedulingSupport = NoSchedulingSupport + override def managerSpecificScenarioActivities( processIdWithName: ProcessIdWithName, after: Option[Instant], diff --git a/engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/AggregatesSpec.scala b/engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/AggregatesSpec.scala index c4243070a9d..d875ce51b21 100644 --- a/engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/AggregatesSpec.scala +++ b/engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/AggregatesSpec.scala @@ -116,7 +116,10 @@ class AggregatesSpec extends AnyFunSuite with TableDrivenPropertyChecks with Mat test("should calculate correct results for average aggregator on BigInt") { val agg = AverageAggregator - addElementsAndComputeResult(List(new BigInteger("7"), new BigInteger("8")), agg) shouldEqual new java.math.BigDecimal("7.5") + addElementsAndComputeResult( + List(new BigInteger("7"), new BigInteger("8")), + agg + ) shouldEqual new java.math.BigDecimal("7.5") } test("should calculate correct results for average aggregator on float") { @@ -133,20 +136,22 @@ class AggregatesSpec extends AnyFunSuite with TableDrivenPropertyChecks with Mat } test("some aggregators should produce null on single null input") { - forAll (Table( - "aggregator", - AverageAggregator, - SampleStandardDeviationAggregator, - PopulationStandardDeviationAggregator, - SampleVarianceAggregator, - PopulationVarianceAggregator, - MaxAggregator, - MinAggregator, - FirstAggregator, - LastAggregator, - SumAggregator, - MedianAggregator - )) { agg => + forAll( + Table( + "aggregator", + AverageAggregator, + SampleStandardDeviationAggregator, + PopulationStandardDeviationAggregator, + SampleVarianceAggregator, + PopulationVarianceAggregator, + MaxAggregator, + MinAggregator, + FirstAggregator, + LastAggregator, + SumAggregator, + MedianAggregator + ) + ) { agg => addElementsAndComputeResult(List(null), agg) shouldEqual null } } @@ -154,10 +159,10 @@ class AggregatesSpec extends AnyFunSuite with TableDrivenPropertyChecks with Mat test("should calculate correct results for standard deviation and variance on doubles") { val table = Table( ("aggregator", "value"), - ( SampleStandardDeviationAggregator, Math.sqrt(2.5) ), - ( PopulationStandardDeviationAggregator, Math.sqrt(2) ), - ( SampleVarianceAggregator, 2.5 ), - ( PopulationVarianceAggregator, 2.0 ) + (SampleStandardDeviationAggregator, Math.sqrt(2.5)), + (PopulationStandardDeviationAggregator, Math.sqrt(2)), + (SampleVarianceAggregator, 2.5), + (PopulationVarianceAggregator, 2.0) ) forAll(table) { (agg, expectedResult) => @@ -182,7 +187,10 @@ class AggregatesSpec extends AnyFunSuite with TableDrivenPropertyChecks with Mat test("should calculate correct results for median aggregator on BigInt") { val agg = MedianAggregator - addElementsAndComputeResult(List(new BigInteger("7"), new BigInteger("8")), agg) shouldEqual new java.math.BigDecimal("7.5") + addElementsAndComputeResult( + List(new BigInteger("7"), new BigInteger("8")), + agg + ) shouldEqual new java.math.BigDecimal("7.5") } test("should calculate correct results for median aggregator on floats") { @@ -225,10 +233,10 @@ class AggregatesSpec extends AnyFunSuite with TableDrivenPropertyChecks with Mat test("should calculate correct results for standard deviation and variance on integers") { val table = Table( ("aggregator", "value"), - ( SampleStandardDeviationAggregator, Math.sqrt(2.5) ), - ( PopulationStandardDeviationAggregator, Math.sqrt(2) ), - ( SampleVarianceAggregator, 2.5 ), - ( PopulationVarianceAggregator, 2.0 ) + (SampleStandardDeviationAggregator, Math.sqrt(2.5)), + (PopulationStandardDeviationAggregator, Math.sqrt(2)), + (SampleVarianceAggregator, 2.5), + (PopulationVarianceAggregator, 2.0) ) forAll(table) { (agg, expectedResult) => @@ -240,10 +248,10 @@ class AggregatesSpec extends AnyFunSuite with TableDrivenPropertyChecks with Mat test("should calculate correct results for standard deviation and variance on BigInt") { val table = Table( ("aggregator", "value"), - ( SampleStandardDeviationAggregator, BigDecimal(Math.sqrt(2.5)) ), - ( PopulationStandardDeviationAggregator, BigDecimal(Math.sqrt(2)) ), - ( SampleVarianceAggregator, BigDecimal(2.5) ), - ( PopulationVarianceAggregator, BigDecimal(2.0) ) + (SampleStandardDeviationAggregator, BigDecimal(Math.sqrt(2.5))), + (PopulationStandardDeviationAggregator, BigDecimal(Math.sqrt(2))), + (SampleVarianceAggregator, BigDecimal(2.5)), + (PopulationVarianceAggregator, BigDecimal(2.0)) ) forAll(table) { (agg, expectedResult) => @@ -258,10 +266,10 @@ class AggregatesSpec extends AnyFunSuite with TableDrivenPropertyChecks with Mat test("should calculate correct results for standard deviation and variance on float") { val table = Table( ("aggregator", "value"), - ( SampleStandardDeviationAggregator, Math.sqrt(2.5) ), - ( PopulationStandardDeviationAggregator, Math.sqrt(2) ), - ( SampleVarianceAggregator, 2.5 ), - ( PopulationVarianceAggregator, 2.0 ) + (SampleStandardDeviationAggregator, Math.sqrt(2.5)), + (PopulationStandardDeviationAggregator, Math.sqrt(2)), + (SampleVarianceAggregator, 2.5), + (PopulationVarianceAggregator, 2.0) ) forAll(table) { (agg, expectedResult) => @@ -273,10 +281,10 @@ class AggregatesSpec extends AnyFunSuite with TableDrivenPropertyChecks with Mat test("should calculate correct results for standard deviation and variance on BigDecimals") { val table = Table( ("aggregator", "value"), - ( SampleStandardDeviationAggregator, BigDecimal(Math.sqrt(2.5)) ), - ( PopulationStandardDeviationAggregator, BigDecimal(Math.sqrt(2)) ), - ( SampleVarianceAggregator, BigDecimal(2.5) ), - ( PopulationVarianceAggregator, BigDecimal(2.0) ) + (SampleStandardDeviationAggregator, BigDecimal(Math.sqrt(2.5))), + (PopulationStandardDeviationAggregator, BigDecimal(Math.sqrt(2))), + (SampleVarianceAggregator, BigDecimal(2.5)), + (PopulationVarianceAggregator, BigDecimal(2.0)) ) forAll(table) { (agg, expectedResult) => @@ -297,15 +305,15 @@ class AggregatesSpec extends AnyFunSuite with TableDrivenPropertyChecks with Mat test("some aggregators should ignore nulls ") { val table = Table( ("aggregator", "value"), - ( SampleStandardDeviationAggregator, Math.sqrt(2.5) ), - ( PopulationStandardDeviationAggregator, Math.sqrt(2) ), - ( SampleVarianceAggregator, 2.5 ), - ( PopulationVarianceAggregator, 2.0 ), - ( SumAggregator, 15.0), - ( MaxAggregator, 5.0), - ( MinAggregator, 1.0), - ( AverageAggregator, 3.0), - ( MedianAggregator, 3.0) + (SampleStandardDeviationAggregator, Math.sqrt(2.5)), + (PopulationStandardDeviationAggregator, Math.sqrt(2)), + (SampleVarianceAggregator, 2.5), + (PopulationVarianceAggregator, 2.0), + (SumAggregator, 15.0), + (MaxAggregator, 5.0), + (MinAggregator, 1.0), + (AverageAggregator, 3.0), + (MedianAggregator, 3.0) ) forAll(table) { (agg, expectedResult) => @@ -315,19 +323,21 @@ class AggregatesSpec extends AnyFunSuite with TableDrivenPropertyChecks with Mat } test("some aggregators should produce null on empty set") { - forAll (Table( - "aggregator", - AverageAggregator, - SampleStandardDeviationAggregator, - PopulationStandardDeviationAggregator, - SampleVarianceAggregator, - PopulationVarianceAggregator, - MaxAggregator, - MinAggregator, - FirstAggregator, - LastAggregator, - SumAggregator - )) { agg => + forAll( + Table( + "aggregator", + AverageAggregator, + SampleStandardDeviationAggregator, + PopulationStandardDeviationAggregator, + SampleVarianceAggregator, + PopulationVarianceAggregator, + MaxAggregator, + MinAggregator, + FirstAggregator, + LastAggregator, + SumAggregator + ) + ) { agg => val result = addElementsAndComputeResult(List(), agg) result shouldBe null } diff --git a/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/aggregates.scala b/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/aggregates.scala index 8f1ebfadcd6..7fe0b0122f6 100644 --- a/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/aggregates.scala +++ b/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/aggregates.scala @@ -5,9 +5,7 @@ import cats.data.{NonEmptyList, Validated} import cats.instances.list._ import org.apache.flink.api.common.typeinfo.TypeInfo import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy -import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.{ - ForLargeFloatingNumbersOperation, -} +import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.ForLargeFloatingNumbersOperation import pl.touk.nussknacker.engine.api.typed.typing._ import pl.touk.nussknacker.engine.api.typed.{NumberTypeUtils, typing} import pl.touk.nussknacker.engine.flink.api.typeinfo.caseclass.CaseClassTypeInfoFactory @@ -17,7 +15,6 @@ import pl.touk.nussknacker.engine.util.MathUtils import pl.touk.nussknacker.engine.util.validated.ValidatedSyntax._ import java.util -import scala.collection.mutable.ListBuffer import scala.jdk.CollectionConverters._ /* @@ -81,7 +78,8 @@ object aggregates { override def zero: Aggregate = new java.util.ArrayList[Number]() - override def addElement(el: Element, agg: Aggregate): Aggregate = if (el == null) agg else { + override def addElement(el: Element, agg: Aggregate): Aggregate = if (el == null) agg + else { agg.add(el) agg } @@ -95,7 +93,8 @@ object aggregates { result } - override def result(finalAggregate: Aggregate): AnyRef = MedianHelper.calculateMedian(finalAggregate.asScala.toList).orNull + override def result(finalAggregate: Aggregate): AnyRef = + MedianHelper.calculateMedian(finalAggregate.asScala.toList).orNull override def computeStoredType(input: TypingResult): Validated[String, TypingResult] = Valid( Typed.genericTypeClass[java.util.ArrayList[_]](List(input)) diff --git a/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/median/MedianHelper.scala b/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/median/MedianHelper.scala index a4685a3b9b5..379d07aead4 100644 --- a/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/median/MedianHelper.scala +++ b/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/median/MedianHelper.scala @@ -1,8 +1,6 @@ package pl.touk.nussknacker.engine.flink.util.transformer.aggregate.median -import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.{ - ForLargeFloatingNumbersOperation, -} +import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.ForLargeFloatingNumbersOperation import pl.touk.nussknacker.engine.util.MathUtils import scala.annotation.tailrec @@ -15,7 +13,9 @@ object MedianHelper { if (numbers.isEmpty) { None } else if (numbers.size % 2 == 1) { - Some(MathUtils.convertToPromotedType(quickSelect(numbers, (numbers.size - 1) / 2))(ForLargeFloatingNumbersOperation)) + Some( + MathUtils.convertToPromotedType(quickSelect(numbers, (numbers.size - 1) / 2))(ForLargeFloatingNumbersOperation) + ) } else { // it is possible to fetch both numbers with single recursion, but it would complicate code val firstNumber = quickSelect(numbers, numbers.size / 2 - 1) diff --git a/engine/flink/management/periodic/README.md b/engine/flink/management/periodic/README.md deleted file mode 100644 index caa2148a016..00000000000 --- a/engine/flink/management/periodic/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Periodic scenarios deployment manager - -An experimental engine running scenarios periodicly according to a schedule such as a cron expression. - -When the deploy button is clicked in NK GUI, then the scenario is scheduled to be run in the future. When a scenario -should be run is described by a schedule, e.g. a cron expression set in scenario properties. During scenario scheduling, -deployment manager only prepares data needed to deploy scenario on a target engine (e.g. on Flink cluster). -scenario is deployed according to the schedule on the target engine. Periodic engine watches its completion. Afterwards -scenario is scheduled to be run again according to the schedule. - -## Usage - -- Implement `DeploymentManagerProvider` using `PeriodicDeploymentManagerProvider`. Following components need to provided: - - Underlying engine, currently only Flink is supported. - - Optional `SchedulePropertyExtractorFactory` to determine how to construct an instance of a periodic property. By default - a cron expression set in scenario properties is used to describe when a scenario should be run. - - Optional `ProcessConfigEnricherFactory` if you would like to extend scenario configuration, by default nothing is done. - - Optional `PeriodicProcessListenerFactory` to take some actions on scenario lifecycle. - - Optional `AdditionalDeploymentDataProvider` to inject additional deployment parameters. -- Add service provider with your `DeploymentManagerProvider` implementation. - -## Configuration - -Use `deploymentManager` with the following properties: - -- `db` - Nussknacker db configuration. -- `processingType` - processing type of scenarios to be managed by this instance of the periodic engine. -- `rescheduleCheckInterval` - frequency of checking finished scenarios to be rescheduled. Optional. -- `deployInterval` - frequency of checking scenarios to be deployed on Flink cluster. Optional. -- `deploymentRetry` - failed deployments configuration. By default retrying is disabled. - - `deployMaxRetries` - maximum amount of retries for failed deployment. - - `deployRetryPenalize` - an amount of time by which the next retry should be delayed. -- `jarsDir` - directory for jars storage. -- `maxFetchedPeriodicScenarioActivities` - optional, maximum number of latest ScenarioActivities that will be fetched, by default 200 diff --git a/engine/flink/management/periodic/src/main/resources/META-INF/services/pl.touk.nussknacker.engine.DeploymentManagerProvider b/engine/flink/management/periodic/src/main/resources/META-INF/services/pl.touk.nussknacker.engine.DeploymentManagerProvider deleted file mode 100644 index e3b1553f5e4..00000000000 --- a/engine/flink/management/periodic/src/main/resources/META-INF/services/pl.touk.nussknacker.engine.DeploymentManagerProvider +++ /dev/null @@ -1 +0,0 @@ -pl.touk.nussknacker.engine.management.periodic.FlinkPeriodicDeploymentManagerProvider diff --git a/engine/flink/management/periodic/src/main/resources/META-INF/services/pl.touk.nussknacker.engine.api.definition.CustomParameterValidator b/engine/flink/management/periodic/src/main/resources/META-INF/services/pl.touk.nussknacker.engine.api.definition.CustomParameterValidator deleted file mode 100644 index 202f7c5d586..00000000000 --- a/engine/flink/management/periodic/src/main/resources/META-INF/services/pl.touk.nussknacker.engine.api.definition.CustomParameterValidator +++ /dev/null @@ -1 +0,0 @@ -pl.touk.nussknacker.engine.management.periodic.cron.CronParameterValidator \ No newline at end of file diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/FlinkPeriodicDeploymentManagerProvider.scala b/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/FlinkPeriodicDeploymentManagerProvider.scala deleted file mode 100644 index 9ce983104cc..00000000000 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/FlinkPeriodicDeploymentManagerProvider.scala +++ /dev/null @@ -1,79 +0,0 @@ -package pl.touk.nussknacker.engine.management.periodic - -import cats.data.ValidatedNel -import com.typesafe.config.Config -import com.typesafe.scalalogging.LazyLogging -import pl.touk.nussknacker.engine.api.component.ScenarioPropertyConfig -import pl.touk.nussknacker.engine.api.definition.{MandatoryParameterValidator, StringParameterEditor} -import pl.touk.nussknacker.engine.api.deployment.DeploymentManager -import pl.touk.nussknacker.engine.deployment.EngineSetupName -import pl.touk.nussknacker.engine.management.periodic.cron.CronParameterValidator -import pl.touk.nussknacker.engine.management.{FlinkConfig, FlinkStreamingDeploymentManagerProvider} -import pl.touk.nussknacker.engine.management.periodic.service._ -import pl.touk.nussknacker.engine.util.config.ConfigEnrichments.RichConfig -import pl.touk.nussknacker.engine.{ - BaseModelData, - DeploymentManagerDependencies, - DeploymentManagerProvider, - MetaDataInitializer -} - -import scala.concurrent.duration.FiniteDuration - -class FlinkPeriodicDeploymentManagerProvider extends DeploymentManagerProvider with LazyLogging { - - private val delegate = new FlinkStreamingDeploymentManagerProvider() - - private val cronConfig = CronSchedulePropertyExtractor.CronPropertyDefaultName -> ScenarioPropertyConfig( - defaultValue = None, - editor = Some(StringParameterEditor), - validators = Some(List(MandatoryParameterValidator, CronParameterValidator.delegate)), - label = Some("Schedule"), - hintText = Some("Quartz cron syntax. You can specify multiple schedulers separated by '|'.") - ) - - override def name: String = "flinkPeriodic" - - override def createDeploymentManager( - modelData: BaseModelData, - dependencies: DeploymentManagerDependencies, - config: Config, - scenarioStateCacheTTL: Option[FiniteDuration], - ): ValidatedNel[String, DeploymentManager] = { - logger.info("Creating FlinkPeriodic scenario manager") - delegate.createDeploymentManagerWithCapabilities(modelData, dependencies, config, scenarioStateCacheTTL).map { - delegateDeploymentManager => - import net.ceedubs.ficus.Ficus._ - import net.ceedubs.ficus.readers.ArbitraryTypeReader._ - val periodicBatchConfig = config.as[PeriodicBatchConfig]("deploymentManager") - val flinkConfig = config.rootAs[FlinkConfig] - - PeriodicDeploymentManager( - delegate = delegateDeploymentManager, - schedulePropertyExtractorFactory = _ => CronSchedulePropertyExtractor(), - processConfigEnricherFactory = ProcessConfigEnricherFactory.noOp, - periodicBatchConfig = periodicBatchConfig, - flinkConfig = flinkConfig, - originalConfig = config, - modelData = modelData, - EmptyPeriodicProcessListenerFactory, - DefaultAdditionalDeploymentDataProvider, - dependencies - ) - } - - } - - override def metaDataInitializer(config: Config): MetaDataInitializer = - delegate.metaDataInitializer(config) - - override def scenarioPropertiesConfig(config: Config): Map[String, ScenarioPropertyConfig] = - Map(cronConfig) ++ delegate.scenarioPropertiesConfig(config) - - override def defaultEngineSetupName: EngineSetupName = - delegate.defaultEngineSetupName - - override def engineSetupIdentity(config: Config): Any = - delegate.engineSetupIdentity(config) - -} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/JarManager.scala b/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/JarManager.scala deleted file mode 100644 index bf5cab7a4f7..00000000000 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/JarManager.scala +++ /dev/null @@ -1,23 +0,0 @@ -package pl.touk.nussknacker.engine.management.periodic - -import pl.touk.nussknacker.engine.api.ProcessVersion -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.deployment.{DeploymentData, ExternalDeploymentId} -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData - -import scala.concurrent.Future - -private[periodic] trait JarManager { - - def prepareDeploymentWithJar( - processVersion: ProcessVersion, - canonicalProcess: CanonicalProcess - ): Future[DeploymentWithJarData.WithCanonicalProcess] - - def deployWithJar( - deploymentWithJarData: DeploymentWithJarData.WithCanonicalProcess, - deploymentData: DeploymentData, - ): Future[Option[ExternalDeploymentId]] - - def deleteJar(jarFileName: String): Future[Unit] -} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/SchedulePropertyExtractorFactory.scala b/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/SchedulePropertyExtractorFactory.scala deleted file mode 100644 index 209f9adb675..00000000000 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/SchedulePropertyExtractorFactory.scala +++ /dev/null @@ -1,7 +0,0 @@ -package pl.touk.nussknacker.engine.management.periodic - -import com.typesafe.config.Config - -trait SchedulePropertyExtractorFactory { - def apply(config: Config): SchedulePropertyExtractor -} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/flink/FlinkJarManager.scala b/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/flink/FlinkJarManager.scala deleted file mode 100644 index 47b33e7bc93..00000000000 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/flink/FlinkJarManager.scala +++ /dev/null @@ -1,118 +0,0 @@ -package pl.touk.nussknacker.engine.management.periodic.flink - -import com.typesafe.scalalogging.LazyLogging -import org.apache.flink.api.common.JobID -import pl.touk.nussknacker.engine.{BaseModelData, newdeployment} -import pl.touk.nussknacker.engine.api.ProcessVersion -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.deployment.{DeploymentData, ExternalDeploymentId} -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData -import pl.touk.nussknacker.engine.management.periodic.{JarManager, PeriodicBatchConfig} -import pl.touk.nussknacker.engine.management.rest.{FlinkClient, HttpFlinkClient} -import pl.touk.nussknacker.engine.management.{ - FlinkConfig, - FlinkDeploymentManager, - FlinkModelJarProvider, - FlinkStreamingRestManager -} -import pl.touk.nussknacker.engine.modelconfig.InputConfigDuringExecution -import sttp.client3.SttpBackend - -import java.nio.file.{Files, Path, Paths} -import scala.concurrent.{ExecutionContext, Future} - -private[periodic] object FlinkJarManager { - - def apply(flinkConfig: FlinkConfig, periodicBatchConfig: PeriodicBatchConfig, modelData: BaseModelData)( - implicit backend: SttpBackend[Future, Any], - ec: ExecutionContext - ): JarManager = { - new FlinkJarManager( - flinkClient = HttpFlinkClient.createUnsafe(flinkConfig), - jarsDir = Paths.get(periodicBatchConfig.jarsDir), - inputConfigDuringExecution = modelData.inputConfigDuringExecution, - modelJarProvider = new FlinkModelJarProvider(modelData.modelClassLoaderUrls) - ) - } - -} - -// Used by [[PeriodicProcessService]]. -private[periodic] class FlinkJarManager( - flinkClient: FlinkClient, - jarsDir: Path, - inputConfigDuringExecution: InputConfigDuringExecution, - modelJarProvider: FlinkModelJarProvider -) extends JarManager - with LazyLogging { - - import scala.concurrent.ExecutionContext.Implicits.global - - override def prepareDeploymentWithJar( - processVersion: ProcessVersion, - canonicalProcess: CanonicalProcess - ): Future[DeploymentWithJarData.WithCanonicalProcess] = { - logger.info(s"Prepare deployment for scenario: $processVersion") - copyJarToLocalDir(processVersion).map { jarFileName => - DeploymentWithJarData.WithCanonicalProcess( - processVersion = processVersion, - process = canonicalProcess, - inputConfigDuringExecutionJson = inputConfigDuringExecution.serialized, - jarFileName = jarFileName - ) - } - } - - private def copyJarToLocalDir(processVersion: ProcessVersion): Future[String] = Future { - jarsDir.toFile.mkdirs() - val jarFileName = - s"${processVersion.processName}-${processVersion.versionId.value}-${System.currentTimeMillis()}.jar" - val jarPath = jarsDir.resolve(jarFileName) - Files.copy(modelJarProvider.getJobJar().toPath, jarPath) - logger.info(s"Copied current model jar to $jarPath") - jarFileName - } - - override def deployWithJar( - deploymentWithJarData: DeploymentWithJarData.WithCanonicalProcess, - deploymentData: DeploymentData - ): Future[Option[ExternalDeploymentId]] = { - val processVersion = deploymentWithJarData.processVersion - logger.info( - s"Deploying scenario ${processVersion.processName}, version id: ${processVersion.versionId} and jar: ${deploymentWithJarData.jarFileName}" - ) - val jarFile = jarsDir.resolve(deploymentWithJarData.jarFileName).toFile - val args = FlinkDeploymentManager.prepareProgramArgs( - deploymentWithJarData.inputConfigDuringExecutionJson, - processVersion, - deploymentData, - deploymentWithJarData.process - ) - flinkClient.runProgram( - jarFile, - FlinkStreamingRestManager.MainClassName, - args, - None, - deploymentData.deploymentId.toNewDeploymentIdOpt.map(toJobId) - ) - } - - override def deleteJar(jarFileName: String): Future[Unit] = { - logger.info(s"Deleting jar: $jarFileName") - for { - _ <- deleteLocalJar(jarFileName) - _ <- flinkClient.deleteJarIfExists(jarFileName) - } yield () - } - - private def deleteLocalJar(jarFileName: String): Future[Unit] = Future { - val jarPath = jarsDir.resolve(jarFileName) - val deleted = Files.deleteIfExists(jarPath) - logger.info(s"Deleted: ($deleted) jar in: $jarPath") - } - - private def toJobId(did: newdeployment.DeploymentId) = { - new JobID(did.value.getLeastSignificantBits, did.value.getMostSignificantBits).toHexString - } - -} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/DeploymentWithJarData.scala b/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/DeploymentWithJarData.scala deleted file mode 100644 index 6491b9506b8..00000000000 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/DeploymentWithJarData.scala +++ /dev/null @@ -1,25 +0,0 @@ -package pl.touk.nussknacker.engine.management.periodic.model - -import pl.touk.nussknacker.engine.api.ProcessVersion -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess - -sealed trait DeploymentWithJarData { - def processVersion: ProcessVersion - def jarFileName: String -} - -object DeploymentWithJarData { - - final case class WithCanonicalProcess( - processVersion: ProcessVersion, - jarFileName: String, - process: CanonicalProcess, - inputConfigDuringExecutionJson: String, - ) extends DeploymentWithJarData - - final case class WithoutCanonicalProcess( - processVersion: ProcessVersion, - jarFileName: String - ) extends DeploymentWithJarData - -} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/PeriodicProcess.scala b/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/PeriodicProcess.scala deleted file mode 100644 index e89deab2320..00000000000 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/PeriodicProcess.scala +++ /dev/null @@ -1,21 +0,0 @@ -package pl.touk.nussknacker.engine.management.periodic.model - -import pl.touk.nussknacker.engine.api.ProcessVersion -import pl.touk.nussknacker.engine.api.deployment.ProcessActionId -import pl.touk.nussknacker.engine.management.periodic.ScheduleProperty -import slick.lifted.MappedTo - -import java.time.LocalDateTime - -case class PeriodicProcessId(value: Long) extends MappedTo[Long] - -case class PeriodicProcess[DeploymentData <: DeploymentWithJarData]( - id: PeriodicProcessId, - deploymentData: DeploymentData, - scheduleProperty: ScheduleProperty, - active: Boolean, - createdAt: LocalDateTime, - processActionId: Option[ProcessActionId] -) { - val processVersion: ProcessVersion = deploymentData.processVersion -} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/PeriodicProcessDeployment.scala b/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/PeriodicProcessDeployment.scala deleted file mode 100644 index 88f620eff9e..00000000000 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/model/PeriodicProcessDeployment.scala +++ /dev/null @@ -1,52 +0,0 @@ -package pl.touk.nussknacker.engine.management.periodic.model - -import pl.touk.nussknacker.engine.management.periodic.{MultipleScheduleProperty, SingleScheduleProperty} -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeploymentStatus.PeriodicProcessDeploymentStatus -import slick.lifted.MappedTo - -import java.time.{Clock, LocalDateTime} - -// TODO: We should separate schedules concept from deployments - fully switch to ScheduleData and ScheduleDeploymentData -case class PeriodicProcessDeployment[DeploymentData <: DeploymentWithJarData]( - id: PeriodicProcessDeploymentId, - periodicProcess: PeriodicProcess[DeploymentData], - createdAt: LocalDateTime, - runAt: LocalDateTime, - scheduleName: ScheduleName, - retriesLeft: Int, - nextRetryAt: Option[LocalDateTime], - state: PeriodicProcessDeploymentState -) { - - def nextRunAt(clock: Clock): Either[String, Option[LocalDateTime]] = - (periodicProcess.scheduleProperty, scheduleName.value) match { - case (MultipleScheduleProperty(schedules), Some(name)) => - schedules.get(name).toRight(s"Failed to find schedule: $scheduleName").flatMap(_.nextRunAt(clock)) - case (e: SingleScheduleProperty, None) => e.nextRunAt(clock) - case (schedule, name) => Left(s"Schedule name: $name mismatch with schedule: $schedule") - } - - def display: String = - s"${periodicProcess.processVersion} with scheduleName=${scheduleName.display} and deploymentId=$id" - -} - -case class PeriodicProcessDeploymentState( - deployedAt: Option[LocalDateTime], - completedAt: Option[LocalDateTime], - status: PeriodicProcessDeploymentStatus -) - -case class PeriodicProcessDeploymentId(value: Long) extends AnyVal with MappedTo[Long] { - override def toString: String = value.toString -} - -object PeriodicProcessDeploymentStatus extends Enumeration { - type PeriodicProcessDeploymentStatus = Value - - val Scheduled, Deployed, Finished, Failed, RetryingDeploy, FailedOnDeploy = Value -} - -case class ScheduleName(value: Option[String]) { - def display: String = value.getOrElse("[default]") -} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/service/AdditionalDeploymentDataProvider.scala b/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/service/AdditionalDeploymentDataProvider.scala deleted file mode 100644 index 3131908a091..00000000000 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/service/AdditionalDeploymentDataProvider.scala +++ /dev/null @@ -1,27 +0,0 @@ -package pl.touk.nussknacker.engine.management.periodic.service - -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData.WithCanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeployment - -import java.time.format.DateTimeFormatter - -trait AdditionalDeploymentDataProvider { - - def prepareAdditionalData(runDetails: PeriodicProcessDeployment[WithCanonicalProcess]): Map[String, String] - -} - -object DefaultAdditionalDeploymentDataProvider extends AdditionalDeploymentDataProvider { - - override def prepareAdditionalData( - runDetails: PeriodicProcessDeployment[WithCanonicalProcess] - ): Map[String, String] = { - Map( - "deploymentId" -> runDetails.id.value.toString, - "runAt" -> runDetails.runAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), - "scheduleName" -> runDetails.scheduleName.value.getOrElse("") - ) - } - -} diff --git a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/service/PeriodicProcessListener.scala b/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/service/PeriodicProcessListener.scala deleted file mode 100644 index d3068aac568..00000000000 --- a/engine/flink/management/periodic/src/main/scala/pl/touk/nussknacker/engine/management/periodic/service/PeriodicProcessListener.scala +++ /dev/null @@ -1,61 +0,0 @@ -package pl.touk.nussknacker.engine.management.periodic.service - -import com.typesafe.config.Config -import pl.touk.nussknacker.engine.api.deployment.StatusDetails -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.deployment.ExternalDeploymentId -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData.WithCanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.PeriodicProcessDeployment - -/* - Listener is at-least-once. If there are problems e.g. with DB, invocation can be repeated for same event. - Implementation should be aware of that. Listener is invoked during DB transaction, for that reason it's *synchronous* - */ -trait PeriodicProcessListener { - - def onPeriodicProcessEvent: PartialFunction[PeriodicProcessEvent, Unit] - def close(): Unit = {} -} - -trait PeriodicProcessListenerFactory { - def create(config: Config): PeriodicProcessListener -} - -sealed trait PeriodicProcessEvent { - val deployment: PeriodicProcessDeployment[WithCanonicalProcess] -} - -case class DeployedEvent( - deployment: PeriodicProcessDeployment[WithCanonicalProcess], - externalDeploymentId: Option[ExternalDeploymentId] -) extends PeriodicProcessEvent - -case class FinishedEvent( - deployment: PeriodicProcessDeployment[WithCanonicalProcess], - processState: Option[StatusDetails] -) extends PeriodicProcessEvent - -case class FailedOnDeployEvent( - deployment: PeriodicProcessDeployment[WithCanonicalProcess], - processState: Option[StatusDetails] -) extends PeriodicProcessEvent - -case class FailedOnRunEvent( - deployment: PeriodicProcessDeployment[WithCanonicalProcess], - processState: Option[StatusDetails] -) extends PeriodicProcessEvent - -case class ScheduledEvent(deployment: PeriodicProcessDeployment[WithCanonicalProcess], firstSchedule: Boolean) - extends PeriodicProcessEvent - -object EmptyListener extends EmptyListener - -trait EmptyListener extends PeriodicProcessListener { - - override def onPeriodicProcessEvent: PartialFunction[PeriodicProcessEvent, Unit] = Map.empty - -} - -object EmptyPeriodicProcessListenerFactory extends PeriodicProcessListenerFactory { - override def create(config: Config): PeriodicProcessListener = EmptyListener -} diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/JarManagerStub.scala b/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/JarManagerStub.scala deleted file mode 100644 index 4ded31dd0d1..00000000000 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/JarManagerStub.scala +++ /dev/null @@ -1,38 +0,0 @@ -package pl.touk.nussknacker.engine.management.periodic - -import pl.touk.nussknacker.engine.api.ProcessVersion -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.deployment.{DeploymentData, ExternalDeploymentId} -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData - -import scala.concurrent.Future - -class JarManagerStub extends JarManager { - - var deployWithJarFuture: Future[Option[ExternalDeploymentId]] = Future.successful(None) - var lastDeploymentWithJarData: Option[DeploymentWithJarData.WithCanonicalProcess] = None - - override def prepareDeploymentWithJar( - processVersion: ProcessVersion, - canonicalProcess: CanonicalProcess - ): Future[DeploymentWithJarData.WithCanonicalProcess] = { - Future.successful( - model.DeploymentWithJarData.WithCanonicalProcess( - processVersion = processVersion, - process = canonicalProcess, - inputConfigDuringExecutionJson = "", - jarFileName = "" - ) - ) - } - - override def deployWithJar( - deploymentWithJarData: DeploymentWithJarData.WithCanonicalProcess, - deploymentData: DeploymentData, - ): Future[Option[ExternalDeploymentId]] = { - lastDeploymentWithJarData = Some(deploymentWithJarData) - deployWithJarFuture - } - - override def deleteJar(jarFileName: String): Future[Unit] = Future.successful(()) -} diff --git a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessGen.scala b/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessGen.scala deleted file mode 100644 index 5d1062d4bb6..00000000000 --- a/engine/flink/management/periodic/src/test/scala/pl/touk/nussknacker/engine/management/periodic/PeriodicProcessGen.scala +++ /dev/null @@ -1,38 +0,0 @@ -package pl.touk.nussknacker.engine.management.periodic - -import pl.touk.nussknacker.engine.api.ProcessVersion -import pl.touk.nussknacker.engine.build.ScenarioBuilder -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.CronSchedulePropertyExtractor.CronPropertyDefaultName -import pl.touk.nussknacker.engine.management.periodic.model.DeploymentWithJarData.WithCanonicalProcess -import pl.touk.nussknacker.engine.management.periodic.model.{DeploymentWithJarData, PeriodicProcess, PeriodicProcessId} - -import java.time.LocalDateTime - -object PeriodicProcessGen { - - def apply(): PeriodicProcess[WithCanonicalProcess] = { - PeriodicProcess( - id = PeriodicProcessId(42), - deploymentData = DeploymentWithJarData.WithCanonicalProcess( - processVersion = ProcessVersion.empty, - process = buildCanonicalProcess(), - inputConfigDuringExecutionJson = "{}", - jarFileName = "jar-file-name.jar" - ), - scheduleProperty = CronScheduleProperty("0 0 * * * ?"), - active = true, - createdAt = LocalDateTime.now(), - None - ) - } - - def buildCanonicalProcess(cronProperty: String = "0 0 * * * ?"): CanonicalProcess = { - ScenarioBuilder - .streaming("test") - .additionalFields(properties = Map(CronPropertyDefaultName -> cronProperty)) - .source("test", "test") - .emptySink("test", "test") - } - -} diff --git a/engine/flink/management/src/it/scala/pl/touk/nussknacker/engine/management/streaming/FlinkStreamingDeploymentManagerProviderHelper.scala b/engine/flink/management/src/it/scala/pl/touk/nussknacker/engine/management/streaming/FlinkStreamingDeploymentManagerProviderHelper.scala index aaa2604efe2..0b9b58fd450 100644 --- a/engine/flink/management/src/it/scala/pl/touk/nussknacker/engine/management/streaming/FlinkStreamingDeploymentManagerProviderHelper.scala +++ b/engine/flink/management/src/it/scala/pl/touk/nussknacker/engine/management/streaming/FlinkStreamingDeploymentManagerProviderHelper.scala @@ -2,13 +2,14 @@ package pl.touk.nussknacker.engine.management.streaming import akka.actor.ActorSystem import org.asynchttpclient.DefaultAsyncHttpClientConfig +import sttp.client3.asynchttpclient.future.AsyncHttpClientFutureBackend +import pl.touk.nussknacker.engine._ import pl.touk.nussknacker.engine.api.component.DesignerWideComponentId import pl.touk.nussknacker.engine.api.deployment.{ DeploymentManager, NoOpScenarioActivityManager, ProcessingTypeActionServiceStub, - ProcessingTypeDeployedScenariosProviderStub, - ScenarioActivityManager + ProcessingTypeDeployedScenariosProviderStub } import pl.touk.nussknacker.engine.definition.component.Components.ComponentDefinitionExtractionMode import pl.touk.nussknacker.engine.management.FlinkStreamingDeploymentManagerProvider @@ -20,7 +21,6 @@ import pl.touk.nussknacker.engine.{ ModelDependencies, ProcessingTypeConfig } -import sttp.client3.asynchttpclient.future.AsyncHttpClientFutureBackend object FlinkStreamingDeploymentManagerProviderHelper { diff --git a/engine/flink/management/src/main/scala/pl/touk/nussknacker/engine/management/FlinkRestManager.scala b/engine/flink/management/src/main/scala/pl/touk/nussknacker/engine/management/FlinkRestManager.scala index 3fef55f145e..3f22e5a8d17 100644 --- a/engine/flink/management/src/main/scala/pl/touk/nussknacker/engine/management/FlinkRestManager.scala +++ b/engine/flink/management/src/main/scala/pl/touk/nussknacker/engine/management/FlinkRestManager.scala @@ -1,9 +1,18 @@ package pl.touk.nussknacker.engine.management +import com.typesafe.config.Config import com.typesafe.scalalogging.LazyLogging import org.apache.flink.api.common.{JobID, JobStatus} import pl.touk.nussknacker.engine.api.ProcessVersion import pl.touk.nussknacker.engine.api.deployment._ +import pl.touk.nussknacker.engine.api.deployment.scheduler._ +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.{ + AdditionalDeploymentDataProvider, + ProcessConfigEnricherFactory, + SchedulePropertyExtractorFactory, + ScheduledExecutionPerformer, + ScheduledProcessListenerFactory +} import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId} import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess @@ -11,10 +20,7 @@ import pl.touk.nussknacker.engine.deployment.{DeploymentId, ExternalDeploymentId import pl.touk.nussknacker.engine.management.FlinkRestManager.ParsedJobConfig import pl.touk.nussknacker.engine.management.rest.FlinkClient import pl.touk.nussknacker.engine.management.rest.flinkRestModel.{BaseJobStatusCounts, JobOverview} -import pl.touk.nussknacker.engine.util.WithDataFreshnessStatusUtils.{ - WithDataFreshnessStatusMapOps, - WithDataFreshnessStatusOps -} +import pl.touk.nussknacker.engine.util.WithDataFreshnessStatusUtils.WithDataFreshnessStatusMapOps import pl.touk.nussknacker.engine.{BaseModelData, DeploymentManagerDependencies, newdeployment} import scala.concurrent.Future @@ -71,6 +77,21 @@ class FlinkRestManager( } + override def schedulingSupport: SchedulingSupport = new SchedulingSupported { + + override def createScheduledExecutionPerformer( + modelData: BaseModelData, + dependencies: DeploymentManagerDependencies, + config: Config, + ): ScheduledExecutionPerformer = FlinkScheduledExecutionPerformer.create(modelData, dependencies, config) + + override def customSchedulePropertyExtractorFactory: Option[SchedulePropertyExtractorFactory] = None + override def customProcessConfigEnricherFactory: Option[ProcessConfigEnricherFactory] = None + override def customScheduledProcessListenerFactory: Option[ScheduledProcessListenerFactory] = None + override def customAdditionalDeploymentDataProvider: Option[AdditionalDeploymentDataProvider] = None + + } + private def getAllProcessesStatesFromFlink()( implicit freshnessPolicy: DataFreshnessPolicy ): Future[WithDataFreshnessStatus[Map[ProcessName, List[StatusDetails]]]] = { diff --git a/engine/flink/management/src/main/scala/pl/touk/nussknacker/engine/management/FlinkScheduledExecutionPerformer.scala b/engine/flink/management/src/main/scala/pl/touk/nussknacker/engine/management/FlinkScheduledExecutionPerformer.scala new file mode 100644 index 00000000000..c312c7834db --- /dev/null +++ b/engine/flink/management/src/main/scala/pl/touk/nussknacker/engine/management/FlinkScheduledExecutionPerformer.scala @@ -0,0 +1,140 @@ +package pl.touk.nussknacker.engine.management + +import com.typesafe.config.Config +import com.typesafe.scalalogging.LazyLogging +import org.apache.flink.api.common.JobID +import pl.touk.nussknacker.engine.api.ProcessVersion +import pl.touk.nussknacker.engine.api.deployment.scheduler.model.{DeploymentWithRuntimeParams, RuntimeParams} +import pl.touk.nussknacker.engine.api.deployment.scheduler.services.ScheduledExecutionPerformer +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess +import pl.touk.nussknacker.engine.deployment.{DeploymentData, ExternalDeploymentId} +import pl.touk.nussknacker.engine.management.FlinkScheduledExecutionPerformer.jarFileNameRuntimeParam +import pl.touk.nussknacker.engine.management.rest.{FlinkClient, HttpFlinkClient} +import pl.touk.nussknacker.engine.modelconfig.InputConfigDuringExecution +import pl.touk.nussknacker.engine.util.config.ConfigEnrichments.RichConfig +import pl.touk.nussknacker.engine.{BaseModelData, DeploymentManagerDependencies, newdeployment} + +import java.nio.file.{Files, Path, Paths} +import scala.concurrent.Future + +object FlinkScheduledExecutionPerformer { + + val jarFileNameRuntimeParam = "jarFileName" + + def create( + modelData: BaseModelData, + dependencies: DeploymentManagerDependencies, + config: Config, + ): ScheduledExecutionPerformer = { + import dependencies._ + import net.ceedubs.ficus.Ficus._ + import net.ceedubs.ficus.readers.ArbitraryTypeReader._ + val flinkConfig = config.rootAs[FlinkConfig] + new FlinkScheduledExecutionPerformer( + flinkClient = HttpFlinkClient.createUnsafe(flinkConfig), + jarsDir = Paths.get(config.getString("scheduling.jarsDir")), + inputConfigDuringExecution = modelData.inputConfigDuringExecution, + modelJarProvider = new FlinkModelJarProvider(modelData.modelClassLoaderUrls) + ) + } + +} + +// Used by [[PeriodicProcessService]]. +class FlinkScheduledExecutionPerformer( + flinkClient: FlinkClient, + jarsDir: Path, + inputConfigDuringExecution: InputConfigDuringExecution, + modelJarProvider: FlinkModelJarProvider +) extends ScheduledExecutionPerformer + with LazyLogging { + + import scala.concurrent.ExecutionContext.Implicits.global + + override def prepareDeploymentWithRuntimeParams( + processVersion: ProcessVersion, + ): Future[DeploymentWithRuntimeParams] = { + logger.info(s"Prepare deployment for scenario: $processVersion") + copyJarToLocalDir(processVersion).map { jarFileName => + DeploymentWithRuntimeParams( + processId = Some(processVersion.processId), + processName = processVersion.processName, + versionId = processVersion.versionId, + runtimeParams = RuntimeParams(Map(jarFileNameRuntimeParam -> jarFileName)) + ) + } + } + + override def provideInputConfigDuringExecutionJson(): Future[InputConfigDuringExecution] = + Future.successful(inputConfigDuringExecution) + + private def copyJarToLocalDir(processVersion: ProcessVersion): Future[String] = Future { + jarsDir.toFile.mkdirs() + val jarFileName = + s"${processVersion.processName}-${processVersion.versionId.value}-${System.currentTimeMillis()}.jar" + val jarPath = jarsDir.resolve(jarFileName) + Files.copy(modelJarProvider.getJobJar().toPath, jarPath) + logger.info(s"Copied current model jar to $jarPath") + jarFileName + } + + override def deployWithRuntimeParams( + deployment: DeploymentWithRuntimeParams, + inputConfigDuringExecutionJson: String, + deploymentData: DeploymentData, + canonicalProcess: CanonicalProcess, + processVersion: ProcessVersion, + ): Future[Option[ExternalDeploymentId]] = { + deployment.runtimeParams.params.get(jarFileNameRuntimeParam) match { + case Some(jarFileName) => + logger.info( + s"Deploying scenario ${deployment.processName}, version id: ${deployment.versionId} and jar: $jarFileName" + ) + val jarFile = jarsDir.resolve(jarFileName).toFile + val args = FlinkDeploymentManager.prepareProgramArgs( + inputConfigDuringExecutionJson, + processVersion, + deploymentData, + canonicalProcess, + ) + flinkClient.runProgram( + jarFile, + FlinkStreamingRestManager.MainClassName, + args, + None, + deploymentData.deploymentId.toNewDeploymentIdOpt.map(toJobId) + ) + case None => + logger.error( + s"Cannot deploy scenario ${deployment.processName}, version id: ${deployment.versionId}: jar file name not present" + ) + Future.successful(None) + } + } + + override def cleanAfterDeployment(runtimeParams: RuntimeParams): Future[Unit] = { + runtimeParams.params.get(jarFileNameRuntimeParam) match { + case Some(jarFileName) => + logger.info(s"Deleting jar: $jarFileName") + for { + _ <- deleteLocalJar(jarFileName) + _ <- flinkClient.deleteJarIfExists(jarFileName) + } yield () + case None => + logger.warn(s"Jar file name not present among runtime params: ${runtimeParams}") + Future.unit + } + + } + + private def deleteLocalJar(jarFileName: String): Future[Unit] = Future { + val jarPath = jarsDir.resolve(jarFileName) + val deleted = Files.deleteIfExists(jarPath) + logger.info(s"Deleted: ($deleted) jar in: $jarPath") + } + + private def toJobId(did: newdeployment.DeploymentId) = { + new JobID(did.value.getLeastSignificantBits, did.value.getMostSignificantBits).toHexString + } + +} diff --git a/engine/flink/management/src/main/scala/pl/touk/nussknacker/engine/management/FlinkStreamingDeploymentManagerProvider.scala b/engine/flink/management/src/main/scala/pl/touk/nussknacker/engine/management/FlinkStreamingDeploymentManagerProvider.scala index fdc3eef46ac..7081194a2cd 100644 --- a/engine/flink/management/src/main/scala/pl/touk/nussknacker/engine/management/FlinkStreamingDeploymentManagerProvider.scala +++ b/engine/flink/management/src/main/scala/pl/touk/nussknacker/engine/management/FlinkStreamingDeploymentManagerProvider.scala @@ -26,15 +26,6 @@ class FlinkStreamingDeploymentManagerProvider extends DeploymentManagerProvider dependencies: DeploymentManagerDependencies, deploymentConfig: Config, scenarioStateCacheTTL: Option[FiniteDuration] - ): ValidatedNel[String, DeploymentManager] = { - createDeploymentManagerWithCapabilities(modelData, dependencies, deploymentConfig, scenarioStateCacheTTL) - } - - def createDeploymentManagerWithCapabilities( - modelData: BaseModelData, - dependencies: DeploymentManagerDependencies, - deploymentConfig: Config, - scenarioStateCacheTTL: Option[FiniteDuration] ): ValidatedNel[String, DeploymentManager] = { import dependencies._ val flinkConfig = deploymentConfig.rootAs[FlinkConfig] diff --git a/engine/flink/management/src/test/scala/pl/touk/nussknacker/engine/management/FlinkRestManagerSpec.scala b/engine/flink/management/src/test/scala/pl/touk/nussknacker/engine/management/FlinkRestManagerSpec.scala index f379f73891b..200458a48ce 100644 --- a/engine/flink/management/src/test/scala/pl/touk/nussknacker/engine/management/FlinkRestManagerSpec.scala +++ b/engine/flink/management/src/test/scala/pl/touk/nussknacker/engine/management/FlinkRestManagerSpec.scala @@ -19,13 +19,7 @@ import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus.Proble import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId} import pl.touk.nussknacker.engine.api.{MetaData, ProcessVersion, StreamMetaData} import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.engine.deployment.{ - AdditionalModelConfigs, - DeploymentData, - DeploymentId, - ExternalDeploymentId, - User -} +import pl.touk.nussknacker.engine.deployment._ import pl.touk.nussknacker.engine.management.rest.HttpFlinkClient import pl.touk.nussknacker.engine.management.rest.flinkRestModel._ import pl.touk.nussknacker.engine.testing.LocalModelData diff --git a/engine/lite/embeddedDeploymentManager/src/main/scala/pl/touk/nussknacker/engine/embedded/EmbeddedDeploymentManager.scala b/engine/lite/embeddedDeploymentManager/src/main/scala/pl/touk/nussknacker/engine/embedded/EmbeddedDeploymentManager.scala index 785d01b97b8..9603698ddf5 100644 --- a/engine/lite/embeddedDeploymentManager/src/main/scala/pl/touk/nussknacker/engine/embedded/EmbeddedDeploymentManager.scala +++ b/engine/lite/embeddedDeploymentManager/src/main/scala/pl/touk/nussknacker/engine/embedded/EmbeddedDeploymentManager.scala @@ -257,6 +257,8 @@ class EmbeddedDeploymentManager( override def stateQueryForAllScenariosSupport: StateQueryForAllScenariosSupport = NoStateQueryForAllScenariosSupport + override def schedulingSupport: SchedulingSupport = NoSchedulingSupport + override def processStateDefinitionManager: ProcessStateDefinitionManager = EmbeddedProcessStateDefinitionManager override def close(): Unit = { diff --git a/engine/lite/embeddedDeploymentManager/src/test/scala/pl/touk/nussknacker/streaming/embedded/RequestResponseEmbeddedDeploymentManagerTest.scala b/engine/lite/embeddedDeploymentManager/src/test/scala/pl/touk/nussknacker/streaming/embedded/RequestResponseEmbeddedDeploymentManagerTest.scala index 0d28fae5775..d3011301eef 100644 --- a/engine/lite/embeddedDeploymentManager/src/test/scala/pl/touk/nussknacker/streaming/embedded/RequestResponseEmbeddedDeploymentManagerTest.scala +++ b/engine/lite/embeddedDeploymentManager/src/test/scala/pl/touk/nussknacker/streaming/embedded/RequestResponseEmbeddedDeploymentManagerTest.scala @@ -7,20 +7,9 @@ import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import pl.touk.nussknacker.engine.api.ProcessVersion import pl.touk.nussknacker.engine.api.deployment.DeploymentUpdateStrategy.StateRestoringStrategy +import pl.touk.nussknacker.engine.api.deployment._ import pl.touk.nussknacker.engine.api.deployment.cache.ScenarioStateCachingConfig import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus -import pl.touk.nussknacker.engine.api.deployment.{ - DMCancelScenarioCommand, - DMRunDeploymentCommand, - DataFreshnessPolicy, - DeployedScenarioData, - DeploymentManager, - DeploymentUpdateStrategy, - NoOpScenarioActivityManager, - ProcessingTypeActionServiceStub, - ProcessingTypeDeployedScenariosProviderStub, - ScenarioActivityManager -} import pl.touk.nussknacker.engine.api.process.ProcessName import pl.touk.nussknacker.engine.build.ScenarioBuilder import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess diff --git a/engine/lite/k8sDeploymentManager/src/main/scala/pl/touk/nussknacker/k8s/manager/K8sDeploymentManager.scala b/engine/lite/k8sDeploymentManager/src/main/scala/pl/touk/nussknacker/k8s/manager/K8sDeploymentManager.scala index 8357f2a449d..5c83b0b9aa1 100644 --- a/engine/lite/k8sDeploymentManager/src/main/scala/pl/touk/nussknacker/k8s/manager/K8sDeploymentManager.scala +++ b/engine/lite/k8sDeploymentManager/src/main/scala/pl/touk/nussknacker/k8s/manager/K8sDeploymentManager.scala @@ -389,6 +389,7 @@ class K8sDeploymentManager( override def stateQueryForAllScenariosSupport: StateQueryForAllScenariosSupport = NoStateQueryForAllScenariosSupport + override def schedulingSupport: SchedulingSupport = NoSchedulingSupport } object K8sDeploymentManager { diff --git a/engine/lite/k8sDeploymentManager/src/test/scala/pl/touk/nussknacker/k8s/manager/BaseK8sDeploymentManagerTest.scala b/engine/lite/k8sDeploymentManager/src/test/scala/pl/touk/nussknacker/k8s/manager/BaseK8sDeploymentManagerTest.scala index 05b006824bc..b81fdaf74d3 100644 --- a/engine/lite/k8sDeploymentManager/src/test/scala/pl/touk/nussknacker/k8s/manager/BaseK8sDeploymentManagerTest.scala +++ b/engine/lite/k8sDeploymentManager/src/test/scala/pl/touk/nussknacker/k8s/manager/BaseK8sDeploymentManagerTest.scala @@ -9,8 +9,8 @@ import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import pl.touk.nussknacker.engine.api.ProcessVersion import pl.touk.nussknacker.engine.api.deployment.DeploymentUpdateStrategy.StateRestoringStrategy -import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus import pl.touk.nussknacker.engine.api.deployment._ +import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.engine.deployment.DeploymentData import pl.touk.nussknacker.engine.{DeploymentManagerDependencies, ModelData} diff --git a/engine/lite/k8sDeploymentManager/src/test/scala/pl/touk/nussknacker/k8s/manager/K8sDeploymentManagerOnMocksTest.scala b/engine/lite/k8sDeploymentManager/src/test/scala/pl/touk/nussknacker/k8s/manager/K8sDeploymentManagerOnMocksTest.scala index 16b645f8582..309a7bc25fe 100644 --- a/engine/lite/k8sDeploymentManager/src/test/scala/pl/touk/nussknacker/k8s/manager/K8sDeploymentManagerOnMocksTest.scala +++ b/engine/lite/k8sDeploymentManager/src/test/scala/pl/touk/nussknacker/k8s/manager/K8sDeploymentManagerOnMocksTest.scala @@ -13,9 +13,7 @@ import pl.touk.nussknacker.engine.api.deployment.{ DataFreshnessPolicy, NoOpScenarioActivityManager, ProcessingTypeActionServiceStub, - ProcessingTypeDeployedScenariosProvider, - ProcessingTypeDeployedScenariosProviderStub, - ScenarioActivityManager + ProcessingTypeDeployedScenariosProviderStub } import pl.touk.nussknacker.engine.api.process.ProcessName import pl.touk.nussknacker.engine.testing.LocalModelData diff --git a/nussknacker-dist/src/universal/conf/dev-application.conf b/nussknacker-dist/src/universal/conf/dev-application.conf index ddf3c4c27aa..f795c859b07 100644 --- a/nussknacker-dist/src/universal/conf/dev-application.conf +++ b/nussknacker-dist/src/universal/conf/dev-application.conf @@ -159,16 +159,15 @@ scenarioTypes { } "periodic-dev": { deploymentConfig: { - type: "flinkPeriodic" + type: "flinkStreaming" + scheduling { + enabled: true + processingType: streaming + jarsDir: ${storageDir}/jars + } restUrl: "http://jobmanager:8081" restUrl: ${?FLINK_REST_URL} shouldVerifyBeforeDeploy: ${?FLINK_SHOULD_VERIFY_BEFORE_DEPLOY} - deploymentManager { - db: ${db}, - db.table: "periodic_flyway_schema_history" - processingType: streaming, - jarsDir: ${storageDir}/jars - } } modelConfig: { classPath: ["model/devModel.jar", "model/flinkExecutor.jar", "components/flink", "components/common", "flink-dropwizard-metrics-deps/"] From e69c74b34fc1c298ab363f1cf497e1ac31c5f02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czajka?= Date: Fri, 17 Jan 2025 09:53:11 +0100 Subject: [PATCH 04/11] [NU-1935] Change record indexer behaviour (#7443) Co-authored-by: Pawel Czajka --- docs/Changelog.md | 1 + .../spel/SpelExpressionParseError.scala | 4 ++++ .../touk/nussknacker/engine/spel/Typer.scala | 12 ++++++++--- .../engine/spel/SpelExpressionSpec.scala | 9 ++++++++ .../nussknacker/engine/spel/TyperSpec.scala | 21 ++++++++++++++----- 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/Changelog.md b/docs/Changelog.md index 370f3e7431d..1d669514b45 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -58,6 +58,7 @@ * [#7364](https://github.com/TouK/nussknacker/pull/7364) PeriodicDeploymentManger is no longer a separate DM, but instead is an optional functionality and decorator for all DMs * in order to use it, DM must implement interface `schedulingSupported`, that handles deployments on a specific engine * implementation provided for Flink DM +* [#7443](https://github.com/TouK/nussknacker/pull/7443) Indexing on record is more similar to indexing on map. The change lets us access record values dynamically. For example now spel expression "{a: 5, b: 10}[#input.field]" compiles and has type "Integer" inferred from types of values of the record. This lets us access record value based on user input, for instance if user passes "{"field": "b"}" to scenario we will get value "10", whereas input {"field": "c"} would result in "null". Expression "{a: 5}["b"]" still does not compile because it is known at compile time that record does not have property "b". ## 1.18 diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionParseError.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionParseError.scala index 49b27a7f186..5182bd6bae6 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionParseError.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionParseError.scala @@ -106,6 +106,10 @@ object SpelExpressionParseError { override def message: String = s"There is no property '$property' in type: ${typ.display}" } + case class NoPropertyTypeError(typ: TypingResult, propertyType: TypingResult) extends MissingObjectError { + override def message: String = s"There is no property of type '${propertyType.display}' in type: ${typ.display}" + } + case class UnknownMethodError(methodName: String, displayableType: String) extends MissingObjectError { override def message: String = s"Unknown method '$methodName' in $displayableType" } diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala index 94398a81cd6..995a8b6afe6 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala @@ -26,6 +26,7 @@ import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.IllegalOperation import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.MissingObjectError.{ ConstructionOfUnknown, NoPropertyError, + NoPropertyTypeError, NonReferenceError, UnresolvedReferenceError } @@ -199,11 +200,16 @@ private[spel] class Typer( case _ => typeFieldNameReferenceOnRecord(indexString, record) } case indexKey :: Nil if indexKey.canBeConvertedTo(Typed[String]) => - if (dynamicPropertyAccessAllowed) valid(Unknown) else invalid(DynamicPropertyAccessError) - case _ :: Nil => + if (dynamicPropertyAccessAllowed) valid(Unknown) + else + record.runtimeObjType.params match { + case _ :: value :: Nil if record.runtimeObjType.klass == classOf[java.util.Map[_, _]] => valid(value) + case _ => valid(Unknown) + } + case e :: Nil => indexer.children match { case (ref: PropertyOrFieldReference) :: Nil => typeFieldNameReferenceOnRecord(ref.getName, record) - case _ => if (dynamicPropertyAccessAllowed) valid(Unknown) else invalid(DynamicPropertyAccessError) + case _ => if (dynamicPropertyAccessAllowed) valid(Unknown) else invalid(NoPropertyTypeError(record, e)) } case _ => invalid(IllegalIndexingOperation) diff --git a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala index 6291e7c8ff4..3455fad4c08 100644 --- a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala +++ b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala @@ -272,6 +272,15 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD private def evaluate[T: TypeTag](expr: String, context: Context = ctx): T = parse[T](expr = expr, context = context).validExpression.evaluateSync[T](context) + test("should be able to dynamically index record") { + evaluate[Int]("{a: 5, b: 10}[#input.toString()]", Context("abc").withVariable("input", "a")) shouldBe 5 + evaluate[Integer]("{a: 5, b: 10}[#input.toString()]", Context("abc").withVariable("input", "asdf")) shouldBe null + } + + test("should figure out result type when dynamically indexing record") { + evaluate[Int]("{a: {g: 5, h: 10}, b: {g: 50, h: 100}}[#input.toString()].h", Context("abc").withVariable("input", "b")) shouldBe 100 + } + test("parsing first selection on array") { parse[Any]("{1,2,3,4,5,6,7,8,9,10}.^[(#this%2==0)]").validExpression .evaluateSync[java.util.ArrayList[Int]](ctx) should equal(2) diff --git a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/TyperSpec.scala b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/TyperSpec.scala index f69278af040..9da82310d9f 100644 --- a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/TyperSpec.scala +++ b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/TyperSpec.scala @@ -13,8 +13,10 @@ import pl.touk.nussknacker.engine.api.typed.typing._ import pl.touk.nussknacker.engine.definition.clazz.ClassDefinitionTestUtils import pl.touk.nussknacker.engine.dict.{KeysDictTyper, SimpleDictRegistry} import pl.touk.nussknacker.engine.expression.PositionRange -import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.IllegalOperationError.DynamicPropertyAccessError -import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.MissingObjectError.NoPropertyError +import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.MissingObjectError.{ + NoPropertyError, + NoPropertyTypeError +} import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.UnsupportedOperationError.MapWithExpressionKeysError import pl.touk.nussknacker.engine.spel.Typer.TypingResultWithContext import pl.touk.nussknacker.engine.spel.TyperSpecTestData.TestRecord._ @@ -162,10 +164,8 @@ class TyperSpec extends AnyFunSuite with Matchers with ValidatedValuesDetailedMe typeExpression(s"$testRecordExpr[#var]", "var" -> s"$nonPresentKey").invalidValue.toList should matchPattern { case NoPropertyError(typingResult, key) :: Nil if typingResult == testRecordTyped && key == nonPresentKey => } - // TODO: this behavior is to be fixed - ideally this should behave the same as above typeExpression(s"$testRecordExpr[$nonPresentKey]").invalidValue.toList should matchPattern { - case NoPropertyError(typingResult, key) :: DynamicPropertyAccessError :: Nil - if typingResult == testRecordTyped && key == nonPresentKey => + case NoPropertyError(typingResult, key) :: Nil if typingResult == testRecordTyped && key == nonPresentKey => } } @@ -183,6 +183,17 @@ class TyperSpec extends AnyFunSuite with Matchers with ValidatedValuesDetailedMe } } + test("indexing on records with key which is not known at compile time treats record as map") { + typeExpression("{a: 5, b: 10}[#var.toString()]", "var" -> "a").validValue.finalResult.typingResult shouldBe Typed + .typedClass[Int] + } + + test("indexing on records with non string key produces error") { + typeExpression("{a: 5, b: 10}[4]").invalidValue.toList should matchPattern { + case NoPropertyTypeError(_, _) :: Nil => + } + } + private def buildTyper(dynamicPropertyAccessAllowed: Boolean = false) = new Typer( dictTyper = new KeysDictTyper(new SimpleDictRegistry(Map.empty)), strictMethodsChecking = false, From 470be6e61b9864cb3cba5dd75d6f046959742827 Mon Sep 17 00:00:00 2001 From: Pawel Czajka Date: Fri, 17 Jan 2025 11:40:34 +0100 Subject: [PATCH 05/11] Revert "[NU-1935] Change record indexer behaviour (#7443)" This reverts commit e69c74b34fc1c298ab363f1cf497e1ac31c5f02f. pipeline on stagging was not passing --- docs/Changelog.md | 1 - .../spel/SpelExpressionParseError.scala | 4 ---- .../touk/nussknacker/engine/spel/Typer.scala | 12 +++-------- .../engine/spel/SpelExpressionSpec.scala | 9 -------- .../nussknacker/engine/spel/TyperSpec.scala | 21 +++++-------------- 5 files changed, 8 insertions(+), 39 deletions(-) diff --git a/docs/Changelog.md b/docs/Changelog.md index 1d669514b45..370f3e7431d 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -58,7 +58,6 @@ * [#7364](https://github.com/TouK/nussknacker/pull/7364) PeriodicDeploymentManger is no longer a separate DM, but instead is an optional functionality and decorator for all DMs * in order to use it, DM must implement interface `schedulingSupported`, that handles deployments on a specific engine * implementation provided for Flink DM -* [#7443](https://github.com/TouK/nussknacker/pull/7443) Indexing on record is more similar to indexing on map. The change lets us access record values dynamically. For example now spel expression "{a: 5, b: 10}[#input.field]" compiles and has type "Integer" inferred from types of values of the record. This lets us access record value based on user input, for instance if user passes "{"field": "b"}" to scenario we will get value "10", whereas input {"field": "c"} would result in "null". Expression "{a: 5}["b"]" still does not compile because it is known at compile time that record does not have property "b". ## 1.18 diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionParseError.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionParseError.scala index 5182bd6bae6..49b27a7f186 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionParseError.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionParseError.scala @@ -106,10 +106,6 @@ object SpelExpressionParseError { override def message: String = s"There is no property '$property' in type: ${typ.display}" } - case class NoPropertyTypeError(typ: TypingResult, propertyType: TypingResult) extends MissingObjectError { - override def message: String = s"There is no property of type '${propertyType.display}' in type: ${typ.display}" - } - case class UnknownMethodError(methodName: String, displayableType: String) extends MissingObjectError { override def message: String = s"Unknown method '$methodName' in $displayableType" } diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala index 995a8b6afe6..94398a81cd6 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala @@ -26,7 +26,6 @@ import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.IllegalOperation import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.MissingObjectError.{ ConstructionOfUnknown, NoPropertyError, - NoPropertyTypeError, NonReferenceError, UnresolvedReferenceError } @@ -200,16 +199,11 @@ private[spel] class Typer( case _ => typeFieldNameReferenceOnRecord(indexString, record) } case indexKey :: Nil if indexKey.canBeConvertedTo(Typed[String]) => - if (dynamicPropertyAccessAllowed) valid(Unknown) - else - record.runtimeObjType.params match { - case _ :: value :: Nil if record.runtimeObjType.klass == classOf[java.util.Map[_, _]] => valid(value) - case _ => valid(Unknown) - } - case e :: Nil => + if (dynamicPropertyAccessAllowed) valid(Unknown) else invalid(DynamicPropertyAccessError) + case _ :: Nil => indexer.children match { case (ref: PropertyOrFieldReference) :: Nil => typeFieldNameReferenceOnRecord(ref.getName, record) - case _ => if (dynamicPropertyAccessAllowed) valid(Unknown) else invalid(NoPropertyTypeError(record, e)) + case _ => if (dynamicPropertyAccessAllowed) valid(Unknown) else invalid(DynamicPropertyAccessError) } case _ => invalid(IllegalIndexingOperation) diff --git a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala index 3455fad4c08..6291e7c8ff4 100644 --- a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala +++ b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala @@ -272,15 +272,6 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD private def evaluate[T: TypeTag](expr: String, context: Context = ctx): T = parse[T](expr = expr, context = context).validExpression.evaluateSync[T](context) - test("should be able to dynamically index record") { - evaluate[Int]("{a: 5, b: 10}[#input.toString()]", Context("abc").withVariable("input", "a")) shouldBe 5 - evaluate[Integer]("{a: 5, b: 10}[#input.toString()]", Context("abc").withVariable("input", "asdf")) shouldBe null - } - - test("should figure out result type when dynamically indexing record") { - evaluate[Int]("{a: {g: 5, h: 10}, b: {g: 50, h: 100}}[#input.toString()].h", Context("abc").withVariable("input", "b")) shouldBe 100 - } - test("parsing first selection on array") { parse[Any]("{1,2,3,4,5,6,7,8,9,10}.^[(#this%2==0)]").validExpression .evaluateSync[java.util.ArrayList[Int]](ctx) should equal(2) diff --git a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/TyperSpec.scala b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/TyperSpec.scala index 9da82310d9f..f69278af040 100644 --- a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/TyperSpec.scala +++ b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/TyperSpec.scala @@ -13,10 +13,8 @@ import pl.touk.nussknacker.engine.api.typed.typing._ import pl.touk.nussknacker.engine.definition.clazz.ClassDefinitionTestUtils import pl.touk.nussknacker.engine.dict.{KeysDictTyper, SimpleDictRegistry} import pl.touk.nussknacker.engine.expression.PositionRange -import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.MissingObjectError.{ - NoPropertyError, - NoPropertyTypeError -} +import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.IllegalOperationError.DynamicPropertyAccessError +import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.MissingObjectError.NoPropertyError import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.UnsupportedOperationError.MapWithExpressionKeysError import pl.touk.nussknacker.engine.spel.Typer.TypingResultWithContext import pl.touk.nussknacker.engine.spel.TyperSpecTestData.TestRecord._ @@ -164,8 +162,10 @@ class TyperSpec extends AnyFunSuite with Matchers with ValidatedValuesDetailedMe typeExpression(s"$testRecordExpr[#var]", "var" -> s"$nonPresentKey").invalidValue.toList should matchPattern { case NoPropertyError(typingResult, key) :: Nil if typingResult == testRecordTyped && key == nonPresentKey => } + // TODO: this behavior is to be fixed - ideally this should behave the same as above typeExpression(s"$testRecordExpr[$nonPresentKey]").invalidValue.toList should matchPattern { - case NoPropertyError(typingResult, key) :: Nil if typingResult == testRecordTyped && key == nonPresentKey => + case NoPropertyError(typingResult, key) :: DynamicPropertyAccessError :: Nil + if typingResult == testRecordTyped && key == nonPresentKey => } } @@ -183,17 +183,6 @@ class TyperSpec extends AnyFunSuite with Matchers with ValidatedValuesDetailedMe } } - test("indexing on records with key which is not known at compile time treats record as map") { - typeExpression("{a: 5, b: 10}[#var.toString()]", "var" -> "a").validValue.finalResult.typingResult shouldBe Typed - .typedClass[Int] - } - - test("indexing on records with non string key produces error") { - typeExpression("{a: 5, b: 10}[4]").invalidValue.toList should matchPattern { - case NoPropertyTypeError(_, _) :: Nil => - } - } - private def buildTyper(dynamicPropertyAccessAllowed: Boolean = false) = new Typer( dictTyper = new KeysDictTyper(new SimpleDictRegistry(Map.empty)), strictMethodsChecking = false, From 033a652098dd28dc724f3cd6bdce38cc08d718f4 Mon Sep 17 00:00:00 2001 From: Pawel Czajka Date: Fri, 17 Jan 2025 12:55:34 +0100 Subject: [PATCH 06/11] Revert "Revert "[NU-1935] Change record indexer behaviour (#7443)"" This reverts commit 470be6e61b9864cb3cba5dd75d6f046959742827. --- docs/Changelog.md | 1 + .../spel/SpelExpressionParseError.scala | 4 ++++ .../touk/nussknacker/engine/spel/Typer.scala | 12 ++++++++--- .../engine/spel/SpelExpressionSpec.scala | 9 ++++++++ .../nussknacker/engine/spel/TyperSpec.scala | 21 ++++++++++++++----- 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/Changelog.md b/docs/Changelog.md index 370f3e7431d..1d669514b45 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -58,6 +58,7 @@ * [#7364](https://github.com/TouK/nussknacker/pull/7364) PeriodicDeploymentManger is no longer a separate DM, but instead is an optional functionality and decorator for all DMs * in order to use it, DM must implement interface `schedulingSupported`, that handles deployments on a specific engine * implementation provided for Flink DM +* [#7443](https://github.com/TouK/nussknacker/pull/7443) Indexing on record is more similar to indexing on map. The change lets us access record values dynamically. For example now spel expression "{a: 5, b: 10}[#input.field]" compiles and has type "Integer" inferred from types of values of the record. This lets us access record value based on user input, for instance if user passes "{"field": "b"}" to scenario we will get value "10", whereas input {"field": "c"} would result in "null". Expression "{a: 5}["b"]" still does not compile because it is known at compile time that record does not have property "b". ## 1.18 diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionParseError.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionParseError.scala index 49b27a7f186..5182bd6bae6 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionParseError.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionParseError.scala @@ -106,6 +106,10 @@ object SpelExpressionParseError { override def message: String = s"There is no property '$property' in type: ${typ.display}" } + case class NoPropertyTypeError(typ: TypingResult, propertyType: TypingResult) extends MissingObjectError { + override def message: String = s"There is no property of type '${propertyType.display}' in type: ${typ.display}" + } + case class UnknownMethodError(methodName: String, displayableType: String) extends MissingObjectError { override def message: String = s"Unknown method '$methodName' in $displayableType" } diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala index 94398a81cd6..995a8b6afe6 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala @@ -26,6 +26,7 @@ import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.IllegalOperation import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.MissingObjectError.{ ConstructionOfUnknown, NoPropertyError, + NoPropertyTypeError, NonReferenceError, UnresolvedReferenceError } @@ -199,11 +200,16 @@ private[spel] class Typer( case _ => typeFieldNameReferenceOnRecord(indexString, record) } case indexKey :: Nil if indexKey.canBeConvertedTo(Typed[String]) => - if (dynamicPropertyAccessAllowed) valid(Unknown) else invalid(DynamicPropertyAccessError) - case _ :: Nil => + if (dynamicPropertyAccessAllowed) valid(Unknown) + else + record.runtimeObjType.params match { + case _ :: value :: Nil if record.runtimeObjType.klass == classOf[java.util.Map[_, _]] => valid(value) + case _ => valid(Unknown) + } + case e :: Nil => indexer.children match { case (ref: PropertyOrFieldReference) :: Nil => typeFieldNameReferenceOnRecord(ref.getName, record) - case _ => if (dynamicPropertyAccessAllowed) valid(Unknown) else invalid(DynamicPropertyAccessError) + case _ => if (dynamicPropertyAccessAllowed) valid(Unknown) else invalid(NoPropertyTypeError(record, e)) } case _ => invalid(IllegalIndexingOperation) diff --git a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala index 6291e7c8ff4..3455fad4c08 100644 --- a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala +++ b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala @@ -272,6 +272,15 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD private def evaluate[T: TypeTag](expr: String, context: Context = ctx): T = parse[T](expr = expr, context = context).validExpression.evaluateSync[T](context) + test("should be able to dynamically index record") { + evaluate[Int]("{a: 5, b: 10}[#input.toString()]", Context("abc").withVariable("input", "a")) shouldBe 5 + evaluate[Integer]("{a: 5, b: 10}[#input.toString()]", Context("abc").withVariable("input", "asdf")) shouldBe null + } + + test("should figure out result type when dynamically indexing record") { + evaluate[Int]("{a: {g: 5, h: 10}, b: {g: 50, h: 100}}[#input.toString()].h", Context("abc").withVariable("input", "b")) shouldBe 100 + } + test("parsing first selection on array") { parse[Any]("{1,2,3,4,5,6,7,8,9,10}.^[(#this%2==0)]").validExpression .evaluateSync[java.util.ArrayList[Int]](ctx) should equal(2) diff --git a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/TyperSpec.scala b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/TyperSpec.scala index f69278af040..9da82310d9f 100644 --- a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/TyperSpec.scala +++ b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/TyperSpec.scala @@ -13,8 +13,10 @@ import pl.touk.nussknacker.engine.api.typed.typing._ import pl.touk.nussknacker.engine.definition.clazz.ClassDefinitionTestUtils import pl.touk.nussknacker.engine.dict.{KeysDictTyper, SimpleDictRegistry} import pl.touk.nussknacker.engine.expression.PositionRange -import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.IllegalOperationError.DynamicPropertyAccessError -import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.MissingObjectError.NoPropertyError +import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.MissingObjectError.{ + NoPropertyError, + NoPropertyTypeError +} import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.UnsupportedOperationError.MapWithExpressionKeysError import pl.touk.nussknacker.engine.spel.Typer.TypingResultWithContext import pl.touk.nussknacker.engine.spel.TyperSpecTestData.TestRecord._ @@ -162,10 +164,8 @@ class TyperSpec extends AnyFunSuite with Matchers with ValidatedValuesDetailedMe typeExpression(s"$testRecordExpr[#var]", "var" -> s"$nonPresentKey").invalidValue.toList should matchPattern { case NoPropertyError(typingResult, key) :: Nil if typingResult == testRecordTyped && key == nonPresentKey => } - // TODO: this behavior is to be fixed - ideally this should behave the same as above typeExpression(s"$testRecordExpr[$nonPresentKey]").invalidValue.toList should matchPattern { - case NoPropertyError(typingResult, key) :: DynamicPropertyAccessError :: Nil - if typingResult == testRecordTyped && key == nonPresentKey => + case NoPropertyError(typingResult, key) :: Nil if typingResult == testRecordTyped && key == nonPresentKey => } } @@ -183,6 +183,17 @@ class TyperSpec extends AnyFunSuite with Matchers with ValidatedValuesDetailedMe } } + test("indexing on records with key which is not known at compile time treats record as map") { + typeExpression("{a: 5, b: 10}[#var.toString()]", "var" -> "a").validValue.finalResult.typingResult shouldBe Typed + .typedClass[Int] + } + + test("indexing on records with non string key produces error") { + typeExpression("{a: 5, b: 10}[4]").invalidValue.toList should matchPattern { + case NoPropertyTypeError(_, _) :: Nil => + } + } + private def buildTyper(dynamicPropertyAccessAllowed: Boolean = false) = new Typer( dictTyper = new KeysDictTyper(new SimpleDictRegistry(Map.empty)), strictMethodsChecking = false, From 0214a5ba132f8f6bb3509cfef0ad604c55c12b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czajka?= Date: Fri, 17 Jan 2025 13:58:43 +0100 Subject: [PATCH 07/11] [NU-1926] Typing of indexing of map (#7434) Co-authored-by: Pawel Czajka --- .../api/typed/AssignabilityDeterminer.scala | 16 ++++++++++++++ .../nussknacker/engine/api/typed/typing.scala | 6 +++++ .../touk/nussknacker/engine/spel/Typer.scala | 12 +++++++++- .../engine/spel/SpelExpressionSpec.scala | 22 +++++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala index 8e4cec7ee42..0c016839665 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala @@ -32,6 +32,9 @@ object AssignabilityDeterminer { def isAssignableStrict(from: TypingResult, to: TypingResult): ValidatedNel[String, Unit] = isAssignable(from, to, StrictConversionChecker) + def isAssignableWithoutConversion(from: TypingResult, to: TypingResult): ValidatedNel[String, Unit] = + isAssignable(from, to, WithoutConversionChecker) + private def isAssignable(from: TypingResult, to: TypingResult, conversionChecker: ConversionChecker) = { (from, to) match { case (_, Unknown) => ().validNel @@ -223,6 +226,19 @@ object AssignabilityDeterminer { } + private object WithoutConversionChecker extends ConversionChecker { + + override def isConvertable( + from: SingleTypingResult, + to: TypedClass + ): ValidatedNel[String, Unit] = { + val errMsgPrefix = + s"${from.runtimeObjType.display} is not the same as ${to.display}" + condNel(from.withoutValue == to.withoutValue, (), errMsgPrefix) + } + + } + private object StrictConversionChecker extends ConversionChecker { override def isConvertable( diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala index 1aff3f9a890..343af1e201d 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala @@ -40,6 +40,12 @@ object typing { final def canBeStrictlyConvertedTo(typingResult: TypingResult): Boolean = AssignabilityDeterminer.isAssignableStrict(this, typingResult).isValid + /** + * Checks if the conversion to a given typingResult can be made without any conversion. + */ + final def canBeConvertedWithoutConversionTo(typingResult: TypingResult): Boolean = + AssignabilityDeterminer.isAssignableWithoutConversion(this, typingResult).isValid + def valueOpt: Option[Any] def withoutValue: TypingResult diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala index 995a8b6afe6..23344aab195 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala @@ -224,7 +224,17 @@ private[spel] class Typer( // TODO: validate indexer key - the only valid key is an integer - but its more complicated with references withTypedChildren(_ => valid(param)) case TypedClass(clazz, keyParam :: valueParam :: Nil) if clazz.isAssignableFrom(classOf[java.util.Map[_, _]]) => - withTypedChildren(_ => valid(valueParam)) + withTypedChildren { + // Spel implementation of map indexer (in class org.springframework.expression.spel.ast.Indexer, line 154) tries to convert + // indexer to key type of map, but this conversion can be accomplished only if key type of map is known to spel. + // Currently .asMap extension is implemented in such a way, that spel does not know key type of the resulting map + // (that is when spel evaluates this expression it only knows that it got map, but does not know its type parameters). + // It would be hard to change implementation of .asMap extension so we partially turn off this feature of indexer conversion + // by allowing in typing only situations when map key type and indexer type are the same (though we have to allow + // indexing with unknown type) + case indexKey :: Nil if indexKey.canBeConvertedWithoutConversionTo(keyParam) => valid(valueParam) + case _ => invalid(IllegalIndexingOperation) + } case d: TypedDict => dictTyper.typeDictValue(d, e).map(toNodeResult) case union: TypedUnion => typeUnion(e, union) case TypedTaggedValue(underlying, _) => typeIndexer(e, underlying) diff --git a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala index 3455fad4c08..c59145cb0ad 100644 --- a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala +++ b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala @@ -906,6 +906,28 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD parse[java.math.BigDecimal]("-1.1", ctx) shouldBe Symbol("valid") } + test("should not validate map indexing if index type and map key type are different") { + parse[Any]("""{{key: "a", value: 5}}.toMap[0]""") shouldBe Symbol("invalid") + parse[Any]("""{{key: 1, value: 5}}.toMap["0"]""") shouldBe Symbol("invalid") + parse[Any]("""{{key: 1.toLong, value: 5}}.toMap[0]""") shouldBe Symbol("invalid") + parse[Any]("""{{key: 1, value: 5}}.toMap[0.toLong]""") shouldBe Symbol("invalid") + } + + test("should validate map indexing if index type and map key type are the same") { + parse[Any]("""{{key: 1, value: 5}}.toMap[0]""") shouldBe Symbol("valid") + } + + test("should handle map indexing with unknown key type") { + val context = Context("sth").withVariables( + Map( + "unknownString" -> ContainerOfUnknown("a"), + ) + ) + + evaluate[Int]("""{{key: "a", value: 5}}.toMap[#unknownString.value]""", context) shouldBe 5 + evaluate[Integer]("""{{key: "b", value: 5}}.toMap[#unknownString.value]""", context) shouldBe null + } + test("validate ternary operator") { parse[Long]("'d'? 3 : 4", ctx) should not be Symbol("valid") parse[String]("1 > 2 ? 12 : 23", ctx) should not be Symbol("valid") From 4026065bf1cdefedf3932e55caa79804ceece97d Mon Sep 17 00:00:00 2001 From: Piotr Przybylski Date: Fri, 17 Jan 2025 15:19:00 +0100 Subject: [PATCH 08/11] Add icon for IntelliJ IDEA (#7473) --- .gitignore | 3 ++- .idea/icon.svg | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 .idea/icon.svg diff --git a/.gitignore b/.gitignore index 06465313969..9b80a58f1e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -.idea/ +.idea/* +!.idea/icon.svg target/ *.iml .*orig diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 00000000000..d2b45f3d311 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1 @@ + From 0ff5d03edc90a2fe408c87d470cc33eb595c31bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Cio=C5=82ecki?= Date: Sat, 18 Jan 2025 10:13:16 +0100 Subject: [PATCH 09/11] [Fix] Passing Flink Job Global Params (#7324) (#7470) --- docs/Changelog.md | 1 + .../engine/flink/api/NkGlobalParameters.scala | 30 +++++++++++++++---- .../process/ExecutionConfigPreparer.scala | 19 +++--------- .../NkGlobalParametersEncoderTest.scala | 3 ++ 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/docs/Changelog.md b/docs/Changelog.md index 1d669514b45..c008dee443c 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -59,6 +59,7 @@ * in order to use it, DM must implement interface `schedulingSupported`, that handles deployments on a specific engine * implementation provided for Flink DM * [#7443](https://github.com/TouK/nussknacker/pull/7443) Indexing on record is more similar to indexing on map. The change lets us access record values dynamically. For example now spel expression "{a: 5, b: 10}[#input.field]" compiles and has type "Integer" inferred from types of values of the record. This lets us access record value based on user input, for instance if user passes "{"field": "b"}" to scenario we will get value "10", whereas input {"field": "c"} would result in "null". Expression "{a: 5}["b"]" still does not compile because it is known at compile time that record does not have property "b". +* [#7324](https://github.com/TouK/nussknacker/pull/7324) Fix: Passing Flink Job Global Params ## 1.18 diff --git a/engine/flink/components-api/src/main/scala/pl/touk/nussknacker/engine/flink/api/NkGlobalParameters.scala b/engine/flink/components-api/src/main/scala/pl/touk/nussknacker/engine/flink/api/NkGlobalParameters.scala index ddd7fbc38e2..ac9e0bc9960 100644 --- a/engine/flink/components-api/src/main/scala/pl/touk/nussknacker/engine/flink/api/NkGlobalParameters.scala +++ b/engine/flink/components-api/src/main/scala/pl/touk/nussknacker/engine/flink/api/NkGlobalParameters.scala @@ -17,6 +17,7 @@ import scala.jdk.CollectionConverters._ //Also, those configuration properties will be exposed via Flink REST API/webconsole case class NkGlobalParameters( buildInfo: String, + deploymentId: String, // TODO: Pass here DeploymentId? processVersion: ProcessVersion, configParameters: Option[ConfigGlobalParameters], namespaceParameters: Option[NamespaceMetricsTags], @@ -63,13 +64,21 @@ object NkGlobalParameters { def create( buildInfo: String, + deploymentId: String, // TODO: Pass here DeploymentId? processVersion: ProcessVersion, modelConfig: Config, namespaceTags: Option[NamespaceMetricsTags], additionalInformation: Map[String, String] ): NkGlobalParameters = { val configGlobalParameters = modelConfig.getAs[ConfigGlobalParameters]("globalParameters") - NkGlobalParameters(buildInfo, processVersion, configGlobalParameters, namespaceTags, additionalInformation) + NkGlobalParameters( + buildInfo, + deploymentId, + processVersion, + configGlobalParameters, + namespaceTags, + additionalInformation + ) } def fromMap(jobParameters: java.util.Map[String, String]): Option[NkGlobalParameters] = @@ -79,11 +88,12 @@ object NkGlobalParameters { def encode(parameters: NkGlobalParameters): Map[String, String] = { def encodeWithKeyPrefix(map: Map[String, String], prefix: String): Map[String, String] = { - map.map { case (key, value) => s"$prefix$key" -> value } + map.map { case (key, value) => s"$prefix.$key" -> value } } val baseProperties = Map[String, String]( "buildInfo" -> parameters.buildInfo, + "deploymentId" -> parameters.deploymentId, "versionId" -> parameters.processVersion.versionId.value.toString, "processId" -> parameters.processVersion.processId.value.toString, "modelVersion" -> parameters.processVersion.modelVersion.map(_.toString).orNull, @@ -95,9 +105,11 @@ object NkGlobalParameters { val configMap = parameters.configParameters .map(ConfigGlobalParametersToMapEncoder.encode) .getOrElse(Map.empty) + val namespaceTagsMap = parameters.namespaceParameters .map(p => encodeWithKeyPrefix(p.tags, namespaceTagsMapPrefix)) .getOrElse(Map.empty) + val additionalInformationMap = encodeWithKeyPrefix(parameters.additionalInformation, additionalInformationMapPrefix) @@ -107,8 +119,8 @@ object NkGlobalParameters { def decode(map: Map[String, String]): Option[NkGlobalParameters] = { def decodeWithKeyPrefix(map: Map[String, String], prefix: String): Map[String, String] = { map.view - .filter { case (key, _) => key.startsWith(prefix) } - .map { case (key, value) => key.stripPrefix(prefix) -> value } + .filter { case (key, _) => key.startsWith(s"$prefix.") } + .map { case (key, value) => key.stripPrefix(s"$prefix.") -> value } .toMap } @@ -134,7 +146,15 @@ object NkGlobalParameters { for { processVersion <- processVersionOpt buildInfo <- buildInfoOpt - } yield NkGlobalParameters(buildInfo, processVersion, configParameters, namespaceTags, additionalInformation) + deploymentId <- map.get("deploymentId") + } yield NkGlobalParameters( + buildInfo, + deploymentId, + processVersion, + configParameters, + namespaceTags, + additionalInformation + ) } private object ConfigGlobalParametersToMapEncoder { diff --git a/engine/flink/executor/src/main/scala/pl/touk/nussknacker/engine/process/ExecutionConfigPreparer.scala b/engine/flink/executor/src/main/scala/pl/touk/nussknacker/engine/process/ExecutionConfigPreparer.scala index 34c9140ac1a..debf951005f 100644 --- a/engine/flink/executor/src/main/scala/pl/touk/nussknacker/engine/process/ExecutionConfigPreparer.scala +++ b/engine/flink/executor/src/main/scala/pl/touk/nussknacker/engine/process/ExecutionConfigPreparer.scala @@ -56,30 +56,19 @@ object ExecutionConfigPreparer extends LazyLogging { config.setGlobalJobParameters( NkGlobalParameters.create( buildInfo, + deploymentData.deploymentId.value, jobData.processVersion, modelConfig, namespaceTags = NamespaceMetricsTags(jobData.metaData.name.value, namingStrategy), - prepareMap(jobData.processVersion, deploymentData) + prepareMap(deploymentData) ) ) } - private def prepareMap(processVersion: ProcessVersion, deploymentData: DeploymentData) = { - - val baseProperties = Map[String, String]( - "buildInfo" -> buildInfo, - "versionId" -> processVersion.versionId.value.toString, - "processId" -> processVersion.processId.value.toString, - "labels" -> Encoder[List[String]].apply(processVersion.labels).noSpaces, - "modelVersion" -> processVersion.modelVersion.map(_.toString).orNull, - "user" -> processVersion.user, - "deploymentId" -> deploymentData.deploymentId.value - ) - val scenarioProperties = deploymentData.additionalDeploymentData.map { case (k, v) => + private def prepareMap(deploymentData: DeploymentData) = + deploymentData.additionalDeploymentData.map { case (k, v) => s"deployment.properties.$k" -> v } - baseProperties ++ scenarioProperties - } } diff --git a/engine/flink/tests/src/test/scala/pl/touk/nussknacker/defaultmodel/NkGlobalParametersEncoderTest.scala b/engine/flink/tests/src/test/scala/pl/touk/nussknacker/defaultmodel/NkGlobalParametersEncoderTest.scala index 7ad5e7528a1..8ebb8aa889c 100644 --- a/engine/flink/tests/src/test/scala/pl/touk/nussknacker/defaultmodel/NkGlobalParametersEncoderTest.scala +++ b/engine/flink/tests/src/test/scala/pl/touk/nussknacker/defaultmodel/NkGlobalParametersEncoderTest.scala @@ -12,6 +12,7 @@ class NkGlobalParametersEncoderTest extends AnyFunSuite with Matchers { test("global parameters set and read from context are equal") { val globalParamsWithAllOptionalValues = NkGlobalParameters( buildInfo = "aBuildInfo", + deploymentId = "1", processVersion = ProcessVersion( VersionId.initialVersionId, ProcessName("aProcessName"), @@ -27,6 +28,7 @@ class NkGlobalParametersEncoderTest extends AnyFunSuite with Matchers { val globalParamsWithNoOptionalValues = NkGlobalParameters( buildInfo = "aBuildInfo", + deploymentId = "1", processVersion = ProcessVersion( VersionId.initialVersionId, ProcessName("aProcessName"), @@ -44,6 +46,7 @@ class NkGlobalParametersEncoderTest extends AnyFunSuite with Matchers { val decodedParams = NkGlobalParameters.fromMap(params.toMap).get decodedParams.buildInfo shouldBe params.buildInfo + decodedParams.deploymentId shouldBe params.deploymentId decodedParams.processVersion shouldBe params.processVersion decodedParams.configParameters shouldBe params.configParameters decodedParams.namespaceParameters shouldBe params.namespaceParameters From 0a840b4dae451147e591b91cf01795716b61f89f Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Sat, 18 Jan 2025 17:06:45 +0100 Subject: [PATCH 10/11] fix (#7457) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix * rebuild backend * revert * resolve processDefinitionData reference issue * remove FE sorting of processDefinitionData * Sort classes on BE --------- Co-authored-by: Łukasz Bigorajski --- .../FragmentInputDefinition.tsx | 10 ++++------ .../client/src/reducers/selectors/settings.ts | 11 ++++++++--- .../ui/definition/DefinitionsService.scala | 10 ++++++++-- .../ui/api/DefinitionResourcesSpec.scala | 15 +++++++++++++++ 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/designer/client/src/components/graph/node-modal/fragment-input-definition/FragmentInputDefinition.tsx b/designer/client/src/components/graph/node-modal/fragment-input-definition/FragmentInputDefinition.tsx index c0aa32d74b1..02226d4e2a6 100644 --- a/designer/client/src/components/graph/node-modal/fragment-input-definition/FragmentInputDefinition.tsx +++ b/designer/client/src/components/graph/node-modal/fragment-input-definition/FragmentInputDefinition.tsx @@ -6,7 +6,7 @@ import { getProcessDefinitionData } from "../../../../reducers/selectors/setting import { MapVariableProps } from "../MapVariable"; import { NodeCommonDetailsDefinition } from "../NodeCommonDetailsDefinition"; import { FieldsSelect } from "./FieldsSelect"; -import { find, head, orderBy } from "lodash"; +import { find, head } from "lodash"; import { getDefaultFields } from "./item/utils"; import { FragmentInputParameter } from "./item"; @@ -26,11 +26,9 @@ export function useFragmentInputDefinitionTypeOptions() { [definitionData?.classes], ); - const orderedTypeOptions = useMemo(() => orderBy(typeOptions, (item) => [item.label, item.value], ["asc"]), [typeOptions]); - const defaultTypeOption = useMemo(() => find(typeOptions, { label: "String" }) || head(typeOptions), [typeOptions]); return { - orderedTypeOptions, + typeOptions, defaultTypeOption, }; } @@ -40,7 +38,7 @@ export default function FragmentInputDefinition(props: Props): JSX.Element { const { node, setProperty, isEditMode, showValidation } = passProps; const readOnly = !isEditMode; - const { orderedTypeOptions, defaultTypeOption } = useFragmentInputDefinitionTypeOptions(); + const { typeOptions, defaultTypeOption } = useFragmentInputDefinitionTypeOptions(); const addField = useCallback(() => { addElement("parameters", getDefaultFields(defaultTypeOption.value)); @@ -57,7 +55,7 @@ export default function FragmentInputDefinition(props: Props): JSX.Element { removeField={removeElement} namespace={"parameters"} fields={fields} - options={orderedTypeOptions} + options={typeOptions} showValidation={showValidation} readOnly={readOnly} variableTypes={variableTypes} diff --git a/designer/client/src/reducers/selectors/settings.ts b/designer/client/src/reducers/selectors/settings.ts index 382290a60ea..8177dc4d240 100644 --- a/designer/client/src/reducers/selectors/settings.ts +++ b/designer/client/src/reducers/selectors/settings.ts @@ -1,10 +1,12 @@ -import { createSelector } from "reselect"; +import { createSelector, createSelectorCreator, defaultMemoize } from "reselect"; import { MetricsType } from "../../actions/nk"; import { DynamicTabData } from "../../containers/DynamicTab"; import { ComponentGroup, ProcessDefinitionData } from "../../types"; import { RootState } from "../index"; import { AuthenticationSettings, SettingsState } from "../settings"; -import { uniqBy } from "lodash"; +import { isEqual, uniqBy } from "lodash"; + +const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual); export const getSettings = (state: RootState): SettingsState => state.settings; @@ -17,7 +19,10 @@ export const getSurveySettings = createSelector(getFeatureSettings, (s) => s?.su export const getStickyNotesSettings = createSelector(getFeatureSettings, (s) => s?.stickyNotesSettings); export const getLoggedUser = createSelector(getSettings, (s) => s.loggedUser); export const getLoggedUserId = createSelector(getLoggedUser, (s) => s.id); -export const getProcessDefinitionData = createSelector(getSettings, (s) => s.processDefinitionData || ({} as ProcessDefinitionData)); +export const getProcessDefinitionData = createDeepEqualSelector( + getSettings, + (s) => s.processDefinitionData || ({} as ProcessDefinitionData), +); export const getComponentGroups = createSelector(getProcessDefinitionData, (p) => p.componentGroups || ({} as ComponentGroup[])); export const getCategories = createSelector(getLoggedUser, (u) => u.categories || []); export const getWritableCategories = createSelector(getLoggedUser, getCategories, (user, categories) => diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/definition/DefinitionsService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/definition/DefinitionsService.scala index 92c5d4b8b4b..2f8d59055ab 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/definition/DefinitionsService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/definition/DefinitionsService.scala @@ -10,7 +10,7 @@ import pl.touk.nussknacker.engine.definition.component.{ComponentStaticDefinitio import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap import pl.touk.nussknacker.engine.ModelData import pl.touk.nussknacker.engine.api.TemplateEvaluationResult -import pl.touk.nussknacker.engine.api.typed.typing.{Typed, TypingResult} +import pl.touk.nussknacker.engine.api.typed.typing.{Typed, TypedClass, TypingResult} import pl.touk.nussknacker.restmodel.definition._ import pl.touk.nussknacker.ui.definition.DefinitionsService.{ ComponentUiConfigMode, @@ -106,7 +106,13 @@ class DefinitionsService( UIDefinitions( componentGroups = ComponentGroupsPreparer.prepareComponentGroups(components), components = components.map(component => component.component.id -> createUIComponentDefinition(component)).toMap, - classes = modelData.modelDefinitionWithClasses.classDefinitions.all.toList.map(_.clazzName), + classes = modelData.modelDefinitionWithClasses.classDefinitions.all.toList + .map(_.clazzName) + .filter { + case t: TypedClass if t.klass.isArray => false + case _ => true + } + .sortBy(_.display.toLowerCase), scenarioProperties = { if (forFragment) { createUIProperties(FragmentPropertiesConfig.properties ++ fragmentPropertiesConfig, fragmentPropertiesDocsUrl) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DefinitionResourcesSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DefinitionResourcesSpec.scala index e79e236834e..d62d72a92db 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DefinitionResourcesSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DefinitionResourcesSpec.scala @@ -10,6 +10,7 @@ import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, OptionValues} import pl.touk.nussknacker.engine.api.CirceUtil.RichACursor import pl.touk.nussknacker.engine.api.definition.FixedExpressionValue import pl.touk.nussknacker.engine.api.parameter.{ParameterName, ValueInputWithFixedValuesProvided} +import pl.touk.nussknacker.engine.api.typed.typing.{Typed, TypingResult, Unknown} import pl.touk.nussknacker.engine.api.{FragmentSpecificData, MetaData} import pl.touk.nussknacker.engine.canonicalgraph.canonicalnode.FlatNode import pl.touk.nussknacker.engine.canonicalgraph.{CanonicalProcess, canonicalnode} @@ -103,6 +104,20 @@ class DefinitionResourcesSpec } } + it("should return definition sorted data for allowed classes - skipping array because list should be uses instead") { + getProcessDefinitionData() ~> check { + status shouldBe StatusCodes.OK + + val allowedClasses = responseAs[Json].hcursor.downField("classes").focus.value.asArray.value + val allowedClassesRefClazzNames = allowedClasses.flatMap(_.hcursor.downField("refClazzName").focus.value.asString) + val allowedClassesDisplay = allowedClasses.flatMap(_.hcursor.downField("display").focus.value.asString) + + allowedClassesRefClazzNames should contain("java.util.List") + allowedClassesRefClazzNames should not contain (Array().getClass.getName) + allowedClassesDisplay shouldBe allowedClassesDisplay.sortBy(_.toLowerCase) + } + } + it("should return info about editor based on fragment parameter definition") { val fragmentWithFixedValuesEditor = { CanonicalProcess( From 53ff3f796802dfad336ffd2eb0063a1be9ce859e Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Mon, 20 Jan 2025 14:13:12 +0100 Subject: [PATCH 11/11] [NU-1986] provide currently selected and deployed items visual adjustments (#7477) * NU-1986 provide currently selected and deployed items visual adjustments * NU-1986 provide i18n * Updated snapshots (#7478) Co-authored-by: Dzuming <9945753+Dzuming@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Dzuming <9945753+Dzuming@users.noreply.github.com> --- ...ctivities should display activities #0.png | Bin 18694 -> 18704 bytes ...ctivities should display activities #2.png | Bin 33407 -> 33474 bytes ...ctivities should display activities #3.png | Bin 30540 -> 30196 bytes ... to note and display it as markdown #0.png | Bin 72354 -> 74032 bytes ...es should allow to drag sticky note #0.png | Bin 69916 -> 71356 bytes ...cky note when scenario is not saved #0.png | Bin 71149 -> 72065 bytes .../ActivityItemHeader.tsx | 79 ++++++++++++------ .../activities/helpers/activityItemColors.ts | 20 ++++- 8 files changed, 71 insertions(+), 28 deletions(-) diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Activities should display activities #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Activities should display activities #0.png index 92071466a54c1e3a3f65e4ae92e5a9582ceee569..96fac7c2011f2b8df769bb2d46ab094e61dca186 100644 GIT binary patch literal 18704 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 literal 18694 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
  • @P4GA>Y3x zB`rEiimGhB&jdC)0=?|S8O5o~^`njX&mKPSp6i;8#KTrqo zLL?Y0UgYNl|3GD64M7pTG(6uZ8)$ghgCyIhTQW6jp`6^V?-Z0A9?Pm-KuSn!j#$mt z8NH`#!%$1+;+d>YOi)cOFWpY7(*(w@b%oBHaN0gAE9KzhcU4=cU>d=;y>vol@E8<1 zIX)%V7|}5v6dR2SsTE8~!8a2BcrNb!#QWVBrngna#0d8hd97puuMN@y)J7#8uEi9h zJ&zdC*M|*Sce7tqK!^GGCdd3SxPQ*2E6^IqO3N9Irf86PT!j& zVatWxNzv!*us(W5sZ!oL3(A$y_}(F@2TsdROOW>?(9wFLB%ra;VbWgP%nAhCthXD> z`MT%q1nKz=!l|{#eAZeVZLo>N2OxtVuBY)lBAtaEPuepS30>c_sU*%!td48TUn-oaQA`tc2V}>4>xpo|dnhROo}=<(7NhxyMf)9W#X?57 zdci{Y1fQcdgsHZ+*3EGD79f-YdD?nZDfwyRt@)3zo<403iq3vs)A5e)aWQ~5sU||gSPr}1T3YhMZ7Q>>_^3o>l`KS5kr|O8xcWd+soYmptGf-(4c?<@%0<#`} zxiKwH+vdtaSah^!obB=@peQU98IKwbK>nguG$mC6*whr#D|{A#eZpQeufU-pr|0s? z;$Z)O)f|WJ_69+D_V7YkyT1sgR-Fuj2UDeG>-v7Lo3uV5CH?zXekiA)q`0FH0dqZR zeemB}s3 zr1AfZX8XHK5@nt2&J_X!hk`=qm5Ba#Htz4b{qNY~pWn)c`Au@i3kZZc;NR=-55Hl9 zfOS`?^lxe7fX#Ew0FwT|KpmbIlR%r%&tRwODms5M}CRvT2VHQiux*-h#z&6a9aV~e7XtOJgBW>N8cEo z>zPy7Z5!gX8v7aMs0sqL$X$=QjRi4z8|Ek3-eOQuoZ(zGgTD8=C$V&5-Xgc7vwh%E zMzoJm(dYU6^CE~>zR0k{V_=6j4BS?$%RGR6Zo2}q|F09W>Z z?~A#gl9MAB_Bdl0wBy)`zFz$D;s(j^?pzfeNc}oabuZ72pn|U0dR}FbF+=Q-O509g zNA6pv6x|ogDD`Ki6flL9`OYTHVh&T%rLHr_6>Dx*k4@^hd>vSwtIN`EfAs6rOF@tl zui1$)a9NK_Z{SB6o0=lqVt6yM&2|AN;O=?g7FVRJkXmDFw6d+y6@y$VLD^X-=ua)N z>!hEwVYvyAdX-yQ&LL8Jd#ERy@S^c5cm4?Bfe3L~^|Gt@0;jd16oaB#Sv407%01z7 zoBq)KYC4KA;*m_PVfu^9&VXW~_z=LyTGbqr8F#dJ-SA2cHZyV(ruaV88GyO~kl$zm zWCu0Jq|dXzLO9WViigEii&$A6{3a`q?*7ZEmF%KI8K*poP z40h5TZO|=8Q797RXLQnFmA<(zI>%t5HR2B!-k;)=M4EvD*|ZHerGvqwj(;YrrTamX z5VYYP4P4z;f4LIX{jeIgi;EYP;NlOg@wsrr`^CN6!b6anRWbrtI()$8ivUB_Zovha(@Ato>aJ!Qz z2-j^7J8A$n0T0-_68RbU4!$f#+`-blpahuRp2&xf1@32fNPQ$?c1RGm>WSmj7OMS_KkU6rUNcesV6!6O6=Y`j0HOb#O=|mi* zYABvZ#Z~|+0YP)~e4?|mbmwbPhzzaRqiWtg7g2^!_GzNNx&1lP++g+&%A$ZnCJBx( zIm_*>#=7Je3NET-AxM8)Bjs_hDiurLL`|Dr=CtM!$8`QRPp;C2yVPR%xq}0OQYoRj zT4vJC$kF3?yDLL_#(uTWK36rb`TcFd7s~DN_fgdxIoC$EB)F`9i_0T4Au{aT>bJq( zP8E84I_sN5A_T?JmS{H+x^jCU=06E<_7gVH#Anjdfq5M}XF^YJVUY4QeR?z#nc(67 z{(Wia(?eVab(;+3>=9#=rQJ?i_SKmN{<*m{5?&k_j2u*EzeOHTd-~%sa5=!4wvdO1 zrju2gIzC=vT0g78lah3Vu1*-{7ov;H-`SITojHL!IBbqP26jfT*e}1vFVN!rE|>Ux zY<)OS+|-mS6}pF)El-1t@Ij;D;fah8_WtmdouEv=i5?=u(4zXyb!ba&R1kpIZ?0lJ5;*LnDEH~Zxad|cedwZTq-s}m`g?aAIi>9j0ldq0O{~ff#S{R5ikDVa$_!dCtUQlM=1!1qN=gGD zMIqs_ONxxt4=&K$XNWT?aosU!Y%6NmZmTfsHK127d|&XkCM#c~Br07}0aQj=%@9gY zxk4LZFye_cFWyl{K<@+ z<{SjRdZ$Q8&{E1+f91j2D=-+5CAWc%r5NXNXn`D3KHi73)D$y@IB!(+r-`9jj28OC zm*x@t{81?>ot@E-bRKPt+GdLd#0?d`Mc2+#3v)XfztRaTGu!{6$&PmO2*5CCsE-zH zw(IO~VONeU^x|rY8yjakav-q?EP4`{fagCusE+OH3(m;MC^PQ#h-035&&13e)hm4M z4yiOj%G;O*vD&C_a7t`TiEOG(uqY|xyZ-4&Adrb=8_`*V_fA1c^IvzIj8TfcczaNX z2tcPr9mCaYJ>9llY?)oIzjt0e=9C+$uAaPPY>G{(4D1+k=$uI#-FVKYu*n$O9swvyf5_1d{*)%)`(-Qzt0OWUH7m3{^$kvl#nQ#ZxT zU49%vohOUUFjQrsz|g{ANAKM2}{e!In0pFbXR`7Nfw>$^4ThFQ^r^dk{gZmsS* zt$IhcZt9a803%qLEzuXPERES0fyhKA!e4!#fA1_3hO z{?ZeBB}`*_jnM^Nih_-bz;tn2Of=ko-h!99mDG{D4mq=z2MF7`?ou^$v?VO~K}MYh zkNIqMEp{Yd5w+}5-H-Xng!D_|)o!(RuFcWw4@nz6#~EaKU+my7{Mmb?c|>5Sr@YDE z(W|ENy}nP&26x=twEy&^Ycl2|O>q)=27o-m=h&q?Xn0TN1+IDRlF5e&dS-z}`A;GVJ*~PUgRp#j=kb3;$;0 z*EddGwvuU9RN!>=6~gS#3cd}q804KG$SY%sfk5qjby=33(0|jLwA}CNgf~sVQLwQS zE8{VF-FbB|TJFmTI3_rF-wq5MyK`|z>a}ps(o3Ch=|Lz+`*WoyxuHHp3yp@4qW4e|cH5m6f`$^&Inh&*N6C5^LR%WT5QR+WU&foeAXD7kOHAi=p$un%)k#Wa26vxZ+)4Fj4a;F)0eG0`YQ8WB zPaaSR;29hW7ii^K?K<`WxGC6OuRXhXQoA>TGC7$}4pehhtc7Sz4C%t#X^3UDb#mKe;tY;%vq+iR= zA?kg<0-~NWGkK1sP*l+!ls!+WDkpK)<>i`$yol>4*4uf;L1-PxcoRO^vv=22Ffj-3QuBGEGhmhtIcv z)uFZlO0Em#o<2}4rfG46rGW5mbhtmIK-naHeRaL#a=PbAisFdvnKIA5kkl7EZ$7sl5}nRG{wvro`~R z%U00R>FD&F46oB7;SThGh{|7B9Bg9}UYC!@LRU$stlslZy3pk}^?E;(kkO{kK#sQ# zkx@KijIamluEy7Q=)PpJ3L$;1EF!-l#4pDC{X=R9RqH3W#bfLiMBQ|5n)Q>*Aq zZsbQm({K?7loShe^xwzS9r5=zj-~=TV&}4A)__O^6(6>%4V26;Pri+pJuY( zJ~_C64{B?GC^R1U$-9`+lMZ+)w5NTj4QwVIEHCu;4s3aW%8pyIGSepcH;~RKXF7@NZ)C%>dJ3I)lA_R=qhLwU*}fI5AB>Olq=@}+;EU;>52w%FD1~jO5e&WPb3hMx zVz6cb_;rphg?^h~%}?A~S6I(lNQ&6x>G zR41_Wg`18*soMs`g)oKo!E{SW!x0-}#09X*44^RJxY3-P=~WD5Dtq2&#efzeDNjoS z4+tc_S;M8ax2OHU6L0?ZyoF6i^ue1Mv9gTA38*$8;fnkm7k(opvZ9FnSi(3~etfG! zOj175`?>G#x@yVRgwI2GX$O$>3Ja%3XX6ypWM|x@c!r*aAQ(x@(Qo9wVm}p**!RKA8Pkb0dW(*AW-2`f{JE^^=>Jw+?k#5DB0Ki@3Oiq>o zA?Ph_K!X9T*;3XIF0kp0svv;4{H7W0e=ioZaRf*?NNwJU8#>T46L91cd?)};Tjd3L z2QnHNlR;Wzw1zwNzzD74_VJYTrDf4vFW?VBeH1bJqp7_1U{fFb4p_Z{BHQMu1zPMV z23QY`jJ&iFkU4>G({b_euzmP~db7Kmk2*h@3tYTn={h-6QdDgG(wn8Eq5_fDDw|W! z{iHIVUa3cYYz6ii=~Cld`o&iohLOGgj=4}W{;%np91vPy9-mU4Bxko?BC(cPZ5A$K94=vsBBs>14>I(&X5bfpu@oNQzKYNjR! z)FwgIz;T&re&STvp}e|C9TWee>SKPs6-0)gTm6I0TfW_~mP@ze^xhi?Q*Jq;Kla3T z_n4vOXHmm~#XzMxrdq-2*T44nu?P7F1?KU}PEoiasA*c6uhkG;wR_gGUYKX~CIpyh zLT36BKpFfMsQcC&Z?6;_!+4=4?YA8r9rH`vlKb6#C_z8oMth-P zAP0!kJ9~T0mKP&VZJLxkj%(5N^zQ04V3Q5iQUSgL180!jl ze$TjUr#d~2o<}Cc$%8WcB<>VhJZb`m#jCTgXu95x95DG-Oq}!ljE*0mCX?189&)C+EgFXE67;z`7FjIe%Clzcx;GC(q78=Tp>{`Q>JOC zP?K~55Y+&^v{jg#Gh>kP$%q=V`T3E#Y)(sR4AbMD8|~u@_8Lv`K0d-nZX&EW|66S?@csu_i^!%GmN`?d*8>#%T9627*PSM#|>|W z!}arY=)P=MPPe|TbWPJpgNdEKzh!kJ+hTa}qIF_R0&>O8?PDJhK?a-YjQ|zhV(z17 zZOxXSknOwK?sluFK%MZ0DgW`xjm{?{My2pm%n(Nw^pI)cxx6HVIXR75L;T(uO8<*m zy0GTNfq(YFNk80ds;PqoFD2x*f4WnTytAdHrvSDmB{8S|&_-|BUN$;_OP}%b0Pg7- zsyTWJR_yNilo${lNQS4($^czA)0Ayp(3vw;v2(Bm+|#zc!>^-9PoK)OM12Z#zMgof$QJ=I&=ZyBn!zALPnr2f3uCjDTLi=SgIpMyFwBoq*tKp zI3$kUdjJjWX?0NE47vV>!Y9<8Y<*VjZHtz^67~ur6R?hlw&J)?&c$geAT%v>?^GQ$ za&u#RsX8!97`6!tjIntTU?o`k2%B!|-{Y|U@!5wS8osJaS_lLnGy9zXpMm};XvF`J zH^v1l*?+NL|9V6*x0^UzW`fJy-9e~YT4uW` z$ld@HP{OfC2E9(;3IM0EZe&q6)mHT^G;l8Jb;=c!WO(x8@@mCUfif{UxoxgWVMp*V z@efd719Hkq8}1z+wkU8Nl<$laSYEbB-9w#5(Ba)gF41s{=+}mK_bs`s+Ffqy!BgKK zG3!{(lt+M?8SH3nbyNetEXBjw0(o7ILf)qthe96U(|cJ@{o!TlLk4Qd4>e*MmHTuu zxJ-o~-?cS~klYXXmThrLSoi>g(*GsuonVbu;+ckw`1izr&RbyLdxX`DK@s{1B4xZ> z*RT&wfhx(|iaL*v#(`LM~k(J@s_gq`CdBH!;vkOUuC%6mc2+{C>yC4Mr$U|5W= zR)}b;^fn@+nc1XZO~bC|v>Y`m$o#pfjX@^RF=EZXi-(V=oGnj{r?!;eqW-hylh--M z>_k~*XmRmNXne)5NLeLiLV;BZ=$wH8?iO=ebMR*Q375^dOv{5q-l&YMuxg$|f(*=S zcFMgO>_y3!DXd?+%HNDB~l0~;3*5oips@a#0876-<8iRaP-brG!5(ige&sm z=O{8PQNTAv@D~m?86r(AfRNpsFHJFUaBZQtK0^dblAcP|QGbb|QRDrjnwZO;a-V?N z^q^S8I6Sp0i|I5lkY6_>Zi|VMQk9Y7w;cz-MN}Ylk_5IJej|y#aU3^E*3H4Gn`2hk z|G5qylOBW_CG4GVkhSUa5K)M}PU8Q#MKggFwGOv?>SXQ89dy_`XH3)1xe}-6ED6c2 ze3mT|rcXGY7!QtAB05)H_pvPehUWuT$L1byZy#ynJmt>NhcTO)^epvMt{{~i%|;Qx zQAZ98ut9Qbo(bRPcB=i1;^DF~-XGneTNk&Ok#&Jg(;dnsY6=~lp3lzE9z{S;kSj59 zaTH2Z_jy~~QzTPUT#>>GLM`h_&L_dLr9>P&zD>Sl%S)qW#Ur0x+=R#Uc` z5ZTqfdM5Sk@|OMC?=kyyr>2+@n4QLsxH8=ex$&0d9t?QiW@i$wl?LZDqbFiWJPx6KXH+@J)?G zv&2E$_80tFtri`f)?9>vSzVmvx~NE5{Qjctd0=v~8WajYc_JI<)g$LR-7A?2Z4#Wa zX^T&aX>64A*k9~>f3Jyu?xK2s{6f@Lb9MZJh9x-+2@Jh+4nJMVm$~_z+Q9U`PlBFWv2#tGg&}w*7`s z+4v&ksWZ9FG0l#-mZkg-U9klT^dGy&_d|+DN)Pg^1bP=16t4Yj*Y>QQa)!c2OT;`) zd%r5kZ$AKb6RnrfNmIZK)*Wpom$UljCdbp?+o)-{KnANE$*yDH$=%uxu@^X8Fw`h> z5tQtLPe@!=X#~q2+3n6!_w!1O!u!N2q27T(d0u+)7^rV*{Hi1K@P9_U?G5%RtD z;pD!~qQb+7k5?{|CxvF3%qY$ErL_z_yg9UtyNFh4FoM`H`bdi1Ej! zTa?3ZBM0@z{e1?G+iz!3m(N4G#FlV(m<;8vWaLLC?~^;6*ztOKPKggb4;w2fi{zd@ z<1Z^+o}Z|4XAom=XFmuM#vvx|=~CrA@Uh@gHMwl>K_eFoIuP-BCd3>Umbl+RcrHL| zG#?XBbRuPY#dYlUy#C}d*v_NhO+4W~ZzA%@zQIjgch}a%jZHJDD2K&Ff6l!Vxq@f0 zBuHt5z5H$=4jQ$wP_h^aW%Cwgv7^>|W;;s5u0^#Dfo*Y^Rx{bYmqU zkAZI>{LFegmDPXs$1iAEHZFD!pT9CL@sl=?jXXEn#C?EH)V5p;)zX7v*MkFjO?opa zz3&fRU+sZ&7JW4+?SxQZzl8keda4 zS&BZtOEaM8o$I6XBhto7Gg~491A`N0YiEl1jI5V#ePSogEzK`}zZLbv8@EJSPnO2| zqpv%b;zM^IkwXu7xph5hynIH(x{=D*HxT0AMLr(R8(8>izHj|%cy)9Uwa3>@a#Vw0 z1S1}C2KAMJ_3x;Gx#}o0=%j;HdwV0BjFVw2P4`zgVITm!a;p1fHLlgT_LGu70K+^U z9Jz2kPJVlG+KZTj2=t~)yzJ;t;0QLGZK?Y~s)?#s!_{D{Jk15kSii?B?kC<>%VvoY zA^1B{@dc#m=&gY8mNRtWdG9x~h(gGx0LRrniuTtD@-J}boz70mSj@4=VXN6FrMB0% zM+#>k+x~J%9T$EfM1odWUl+1V&1Hpd5aUX+tFZCv6U--Q_r@x`fH17~W1tFYkvRMa zA3KUZ4$GP+;XjLQ{v~`8k@sz9l{>w3jxcd=G-{Z~azV(Zz8 zT`A5&f+M`>*Nr2K9Q-EnGIX;C-q9_yVti2J{;<5+0x9OSX?Ne%^_&S49!KSN>uq587ZTu}4)$~4;|pfL$I>?|$n zBxL_O)kfp=ZYC~o@b{d%-J7n{7-(4lEWo&wmnAhm1%4AY^3`eC=lqENf+f1`{Q0vr zDQC7PU;w(2n1eyLw+6{AnR;-ZxRqXu39vd*k2-B5)b)zy$D8i{z#@1e2ZWHxHXEG; zvi0+)_AP{5d%VA#YEpNLUwV6~^3zjuT6Y%+p0Pm>Anhg>K{W9CDTh;xNap;VpNwX;B9UZv6t2^-#^$wzkNmnD@5VAMVyxzGJNi zZlu)a^0~_<j}`*XPm@ml-He>{iSu{kk;G(qX2o2_x_01 z;^vY&nQD_(NKzdDUIC9C?u=+Wo=ef>mtPQD=IBS2MPz=2VGBe&{-dMYKW^6eNa9-_ zx1+f}R4Auo#et2Deecfg#Z7fxUBuPpIUOUtsjWHP>|EK%ygL~T%)&E{uiWwp0kIqe za__^dTfFD7@$qFo*WQmqg2%PB9=o07m%F9F`ahDiX~B;fqoSgyX`VkDmVJixjE(Jc zM~4)b4I({1A8mJ6(-J8VL8WPVnnDA<$!NWo;qmC+9f*Vk4iQmjdyjieZ0v0^AxkFk z)YL8ZW05pM%=kIeAlEISfOmrbP6a@9r8*D@48`IWSXJ7!7~=wW?J_(<9%8}_o3!LR z{*MNFqaUBj@kQfay?n1?`$7MfD83~Vtzm&*Bn=kYvtjLLgzqEUv}8@tU9xQ;5nnvV8*B6;*iB5 zO>hF9idf%is}CJN{!Y2IjpABEo|kg{dG@sHeG-I`evQ)^#<#>ic%Y@3Y&UhI#`V+Y^<%Wi(3^k$3fx*#Sd2Md|gKDkY>xNp# zSG;fh+Y7v!dxoP*s6xqqj}sAhP?7mwY>Yt1NErru(aBN%`DR&F2lRU#)htisXt;?s z@e&g>pV#|sSsAkJS+&cG2+s+XaAa!H3CwC~l~$Cc|A}s{D93Ux{-O6>Y(k9SNyByS zGG=sHIfHk;(ulvXXs3LYGP`1291fNKv@dNF3k;NWbOfYrO%Z~m zh{z~e+15scJG-)%?1dldl)Y|J-elDu3b>|*JmMJKba26PUK7!5at#^(K#=j{$4|qZ z-;c1$%0v+k_rK+QX=|tYVLC%h89@Yvr+@p@_8rShv&pZi++bf%6nRHv!&w6j9lQ4y z{;)pqG|Bl*wKWHh7f<9)vL;x4AyH!-!9WOJOHD`MwIIUb*D$>Cv`(78-)zrFeFp>o z_dWTH6#5tIfoiEQWk<(@-m=eU%C#H2aL2zxI9v}B4_uZi6@awwe7>IuYx|{cD1tgc zTE@2L@2LqQN+=mAa7f#dI>*vE$0Wy$_2boG({Cdms>Ze0n`s>LL3{iNYm2dSZCPz( zzAmioS#5R)Jyuo%)gpE3S}xc~19^IW#+o(m=%l`~T6OM0S8GWJv{L)&_3+3z`GN`r&4m0MtQ!hM8_up21$)Y2Ff~$h*~nChyO1s zglMRlf2{0KUa^sgx(-Ox2U^Xkv+BMxA)Y8RQ1E}TEvlEuy1`1TNG+~s=j^PvHM(?j zbIbutx#ntYT)WIk6o3f#_WJ7)Slf|Nb1+;uV}D7p2nEKcACTy_1=21rS&kEHOUX37 zkFlfcJ?8f0{#5H&@Qs?AcntdXo?SYvQBd!4w@M%=v|dJ!g=2!%^$0DTkZWMEc|^6z z>!m@*Nn1xpFjSt;&z?AK|0vDrD=a64lC@&vasJh)aNx_V4fc_Ap`4M={efG1KFn{^ zZW2+Gko7v^4tXQo&xtWe(xkE^jH+#4BgY1au@DmOARr?PozA=WnT|pEkMg6bn95y` zA0Z(j{hla!&7BfXtJGt2zQ-5GcpK*b!qauX0cUb*3Jn9J|A&!AS9f=oSj5|#gI3Wu ztmTI%4n_xyj4{+p{jCAwg*r_kNgVs%zVclwpb+y)&Qyk#mX|LbEFIt6UB4$bG0ra4 zYbEYasq^2Ou8u|U+r?OwB!lRmrkK|*d9ls zqJq!JBq_C9QzqsOH0h3FN@Ov>2K^Hr%TrKDP9ah&H-=A5Ek?#a;h&%1o2@(lQep0F zw;|1z-w|d$*x*iJVNp0)Gm`dFuDuS8u#;wanO2^Ke`(q_Pq^txkdvovtCB$ncJ&W@^)uqC1*Bin~Ep>!nV`o6J(=FvO_KcRsB z3~r~L$IqTUBd5wi$G`{&dztLDfol|~&tf{PdvyGQUh9gWrpA?*Q9yvi+?-BRDcUBk})*v8rUz2iCzoBt{olLvAl%u+4O;N+Y{C-jI1(-@tQ&=Q0h zFRN9J*2S}5|DnciUNK+MKU3_U?9CgTvHp}hhpo|=4LuXfi0C3#nH38iWV+|(KkUoV zmR)~e#{VfHLGk8IT)t9oR6>Gx#_wu-DtxdZC=%IBnY+79a~-&e^71wlR##W`qf%4b zqacc=p*#gvf7JcvSi0t$qh9Ud5mpu&_0d##R16nI3kzOp>9YhwLqol&w6qAYP$N&_ z7qWDkx?^Zo+H>Xj#3g%QadIY$J$w3eWpA(L_YhxMW##H*S%}4g*V7afx08>O4Q@)} z($cTFxL$}1rPn&rd6S70+S=P^JMBtPQo2aLYJu3P(x!Yyn$X$ zN7_$Urv;G-2|b%5D$F(DZM!q5_PgrT11no&XvNwuny06untkdH?yebtZ{vU{!K{F7 zcAUH1u2l`Hzm)r#%g4wlg7_lrmoBr-w(`@LOmOm95?tOst**0ow>gtd_gL-0*hVeB zcfJ7&t-8#z6=qzNQmTJSOEYFky0hpDJ4*oH4W6f+Wa0IN&tF=}7ENognJK2QniT8XZ7KetRaeW%*OKDc4y>xiA9nWAE2g+T9uvFUnluMZOl`8`m0cnFO9@O^4(Qh)uL z0p2Z&Dk!kJGfR@=^6pwj&d*Qehv|qHacr8kZhvwb9@}qEw{x%f0MuyJ>Vub2(IYPC zz)u@q9x) z>bY$DfB_Z~M@F;<0$b8^rqvMHEBnWFtw$+GGCiH?Y%I`8rV13TR_QhXdS3NW zt%0u?r5#c;tthdrxh=$^DvTJrdsl4GmwUb(Pn90HJ=58KMDrgeF+;s*jQJ z(8$9Bc{j2W!GY(0O2|N~6DtBo+lM6Rf`KYo{~dSv4_GxIiB#wInADegd%Lq&{-4mC z|ET&uvZ23KgZ_;*ZBWFXo(ds=$@*7N>)$}@5;mKuBD_0~hPHCFsAFkW`C5}mKxH4A zAg_R*%HVfBqMcm}SG5oLw}-Di_Ydu6H%${_?Yb{}pdfc{PF0wPWjWi#M#0Ub$pJV#g=I z1;^RIrpZcGLoG5XzVoY2r7Z^;KB}{d|LOgwgh_ZTXC_hMRX$?g&>U!4!^prh8PEM&X+ko3ILNryo{l|-~(O+|8emqN&g_n*z`P}RUM_$*;35hgqd{TyE zwSC{SjhKx;k}P9J9oRUxPj#c;M{jWbhQ5|%!s%L@^o}x#mf7tZ>yz*T8hZ{)p7qQ= z!m#q6&6dA>1JkJtPl8DGIC;32cck_>6=< z$TE`b`}b_yO|w_AJ@I;XKOAR~5m#)ZJ6~*6?Osn-JE9^6(SW50pqubXc;odnsdCR~ zWt>k~bin{N*hqLH5N3%^z}+hY|FWCeM`3QZxQ}e2GFy zC6lS?=1ujUb5yhf-eBEx)(ysugF{x2!8NU7cz<0Zqwa%8e2Fa1&)iW6AK2j3%xR4S z#TT~<&+hI%gZAkKPUw&})BnVHp?M|wlJ3)ZAHm_}_kHn!t_K!BR-on6xk*E=MAzVF zV$0DHh8HLS4Q>7jo?_!-bp*a|sJ6M}jxUCb`Wn~2VC!fr=yDI z?(Q8Biv^TF#S*=qckb{X7%uc2`F(6^q`0#2+cT%i@7Yec#uLIPXiHLhYn&bhFi z+t2k`#fi5mK$DomTpelU3<4;f65HJ3*<;ig+4n$ zMM-ZwWUU)@O1*!5(}+2uY2L_$H!!el_g}bMT==e16;8w5y}qz8ikMX8$G`Id?LtnMd;%zZw|ITD}RF;61?rrui!Xc%?MF5@Fa1!v<-hwSIijd(b-&F)| z#L&m@->Y6mcSaZwhr0gUnM|Uaus*u|iI`B$iA@27FqAoX+q-3K<=X>>g?2e9c?!JaRn%Eq*N9dGE+sM?`k^?5P93m6I;@aV%?z|@n zYq<7ccOs`e-5%GS%ijID5AlB)*BE)rkM6t+R?^QPOpp=lLT+!F@lBjxKeN;|mZJln6Z4ij%|{^XNFlz$Q` z^}sAxCbl7T!5eG0yQMu}F?|Ee5A8=rh)9U7ZFRozBa~h(pCT~>W&Ug#X$=#LJvds_ z6C^7U64b}oershgwWR|<4XhDz&5}Gu>`<3I=oY<3!{_~+ z58w0})H%5)p%slRvhqgafgwy2@Q{$xE*WM2=XZ~qVc@;4&i5q1>zrFNU5HSK`9uPo z)n%88-sFP)rh9h6Yc9t{LMG3mKqIvG z*{IlGWC6<(N;n&Uk=pKx_HrOj;Oyu++%tC%5*21OIKwcOx*{~$$e69hYq_9VIINv* zBxrViuFUMX+*w`AY0p6xJQbpR$QxI3=f}iP)KTmipmsM>0=DzWga0}r8LgVK;hwnv z#QOdYHphYuJcAyr^p0100Gf2WUsFxcSp2Af_24fe;FL);g~Q|>!Q@S0v0*?q=Z^&g zIg;~F*y3I9$@ z!?i8IDEJOER!TS(@DaRW zLtP4+7x$r}sV#v$y3EWQ0cq6^>|dx@aFKIEMF%%svR7l+7~y7LG4i92r5ofVzO`lh z>uSs)AZ3YGNC3Z+ZZ=Vi_<>*oq5zdrP-tVBeDY%=UWyRy1cioYF>5_XPIryIq7L@X z*BR(nq*nQA0&rZy_9^e)(OA-jGGjzT3IlDEza!XoKA?9)$5DU+T%4ZId0b901DPSP z;r@%#_S?)Wy)`(v(_R1AEDBdwS9;B|RB!LcNF&|l@eiz{83r+R_4F@|@7f6N`ySK7 zhbQYdS79dB8NL7*GXWsIQrAwe>`;=R*sNz@Zx!CSvObJNWqe{F5O@&d|A>YC?*{qL z1o~Ide-kbHC(R83XiV`O{uWkGG!hux)C;w&9|p)dAW_~;+#60cPTde}g2Tj|bBa4U zGPdJW`~Km=y9l)98n)t5Z>z8?J?oWUeYxg0rRJ1ems7d(M}=+ib0(0U^={WvfxDW! z3k&5O!D*&tU;pnu7#E&{ZyG97Gk{zCn{0_gbncm3GZwf?o@Q_MHb`^*tB52W^vUb) zr&}^LcjjCY@H>xHIe(j=Jlk2Hqa1;nA#1_I?^J6wWK}+ywY6O!|wAu^&fDQ>=I*3$7yg!-CJI4xr4qv^NqRc z7mbp)M;|Uuaw2f+ztgqv%>Lfw2%I?=qSlvtYEq}4o9{C5s~wsrBB0YyRFWJgTRd|m zR)ICY;4yc{d7<7}l6K#nlaj4#pyr!%=)lUVP= z{(t@^glG`0m~!!87m9ZjEBT!`dCG&LGbObDY}PtpPL_Tm^L%WCcpG!fIzraXyUc!ukSHf#MlF5K5>xUR9rN;5w;+ho2Q zW`9$6H$+@G)9L}I+|${HcVkiPH8$I9IO}oAb&kmZ4In~j{uv5^(t(S%&cwbyc42Ug zbNm5AA9WqBneZmJ3?Q8S%Q&jTrb8;x(AmIhZxo`JLIdBBLhA)%J8tFJ3#Q{fPSGq8*cJ z@=+iU5x|1s@)6Zidu}cKv!xZ3=xUD*LIDyrL3L%?{GWwvQqa+n9g6}?mTL-o#mkP3 z@;q9L?=lFCnghS28d&yLb{jL`D7Pv!JyjldO2D?k`H!0swTxJRRf`TL7 zt!a-&pA~Dx#CAk?A!#!)Zr*fFL8^pZOTh=Q9tgpd2<9 zu8WfR=3zCL8wNJX=bnaKs!g>`0E~DUx%fV7uzdC?gS?*ly?>8zD3vwa^Css$Vp4r7 zGCEDj*oqPh>wvzqnB-f|!aG6>!qQQVL%H5aLev{fB{!#sSvS=zd-gf<(RY)aNRzJ2 zuQHa!Vw&sX!Bcg|wgKYRTv(mJQuzA<>={>|mMl-6J~=rd2=obi66Z$8Z^jk1@)^wNqlWtPS`@=O2n`Mhy$@AmpCC6rxn9( znD_hbK{_|-XGCig1_Jtam;GF3@@cSHDMb(u0mCPoa#iE)PFysoo~%2Z6Qcv}zz+1H z0F`l3iLmwu0hNXc4zhZH_K-d8Z-}~y_&SoRn|1YYHx3cmlr`=VL+6*|)UI^}^X@M3 z4Wr*ZS66sIoSU%t3!wXdLOTBw=Y&AU!U4X6^=L%6qdSoQATb$IbDbPIl8YDbVokb? z?z04_k&BX=J~U}-Q7@?_R|gzgTpEyQ+)ef#f1Ix7Ki?BrRG!q*7O5NmU^?}aCC!t| z=l-WtQPS`aQv?2MJ2<~QfTgQzYHgNJbaETK8>3rwo-bq5(FE@KvthkR^pN>@75pcS>Rjaq3yFS4WC zVJunJ|K+$$O!>_Z5U(Kjgm-#$I>l^b1(3l(RYbp-Lmnmdp%*A>Xr?$~f0Ln1$5TaKT5U z8V>!a068@*PA9tZ@*^KrODNmt=BkNtVXKM9-TnRV12;f`ni&A6eBTh%{M-u!ZN<%~ zc|&6(pdmmVnPBzMa2-PM8)Lb=ws>V&p>CPUnJL?)Bc(Iq1F=)DfVZP13O-hzVo~vF z11C%Z?SEtHs~ZZ{*iP|x4RsGp{e*-iwR5aQ!v{U>10~?et?*u5U=kA#PS0Z)H~TIk z0+^SNX?*8`bU)swy0}&!vjJ^Y7n-DsE-fq5Kb{5$q;taFt*Bgqs1juaL3_{$beKC# z(SUb~VztqG%k*P(?;Z z_j>PB4&`aly|rHO0pYbJc92K%+u=rl4Y#2=+P3`JL@vpn_3DzU=HBdan)!JF`}(w`YYgb z7(fpTDhOeXrE6^eZoQt~Gr*y%rxzX+R)H-8Li)jV1<#ivhjM4&0tR zOkkKe;V5Sp%6VNkYS9;#Ggk5=!;qYaaQlH(k}*IGI<2*eg|ax&J3AIVac9jfEsw{5 zDuO%PL{(WSLalG`;rx8Qef3x53XpRs)_P+_b4G@moK-L|qN@pr+mE^3mXT9tsI7M$$|AU z`#io_&u(i+WV+I#WxmM#^<4leeTbl|g{TX7nj> zTl?!afuX;s*e&Lj+a-M#{55gx0Wa-e=~CT4W%&A5e>y%*zO4iQxxIa@vsXNjd$Di^ zpk=F@i&lEx*|~>P2LR($Jr6R!W)Rd8xMO~f>dc{NseYlyhVP1-7KnCFS^J~WOLU(3 z?fAF{x7{Bvw#Qm}hP@?KteDO3-b6(y(9k!yJFMS^TiTXZy(W9(&m~YuMok{tGF&HS zGQU5G144CVWCjVxt+YUVMO#iYgFz1})&D7-JV79BgMOX$ZM*!>g8jcBNCEx*Kjmot z6W009Q#t=NL5iCE?b~tsucs7D1>%|9;84q;RmgwJ+IZy~uaxJ3*Aqkg8R~&a>2>dP z1Al(_28cMiyTfX*B$m&vtdz{w9Z$J`T6Xk)oal+*=i@%K9!$4nP0ccjhI81zLGiO} zzsUpUbg`eaL6RZf$_+F_5s<^_{90eNTAzriK>qT__v{?|Ybz``p+^faHBmbWd?q?( z>X;hI@q_#unbgi%%>k(CJqVai-sT7h)=Rr-zhAjx-<&O`06~-f8|+`1wN4g74bx+T z=_^}dx*(|Jm+xh%!(CzinHFe3*ybF^AQ-8bFB`XsDR#VawGb3o+xaD;_CekW#hVVe z@FnM+)v|T6Yr9`{MY&279otM6;yh?>K#j@&wEf;W64L~+ z8pn5Q1f9h8Xh(!_clRM`5Jz;4$NCf1)D#aKItQIlYHJ%6`9GENs5PeH2?*$dxhFBP z?bDRDE5CusMGCkXkS+7Ta{zA8#8kEl4un_$aPW;bVanK=E_-omrXmy&Ucli!RciDy z|DPuF8i>941;WvJI-&Y&(@-D4#)A#^8jUx`k7Nuijk*`T<_?c+o;d7GLoo^)jOqCb z<+NHnmFZwc5+*slrVn`3rnnwv%_?O&Lt>!Iusfm&VAanq_SE&{Dle&O9lZiJ?c-SV zwlsmSVg`NWrtsuVWOgJr$bofqbbPHM7!_py=KK-_eEZv=&0|EQ8lOxbA`NGvMPnOW z1!IPF5Dn+;hrvR@Mq##}N8)anc^q3&feqx-hj?&f!rO?9@H53c)o@9b$5XY(f0%pC zb%3>VRrIMHqtPkGTFL1*nw_u*3R8BCj;9nr770L60ExO}TWi_n)HKxOD`PkwO;{aO?<5mudsX>@iM%v)(W%_y{pFiyf9bm7#7{i_ zQbbPH)wR0(W4l{6Og$8YawXi{Gu|j*SuKPaj^D%zx3>*{lXk>m)ag8#lKg!*&y$Nk zS|lw_B10*a(m6>}UIxyaUAPL_L>eRr;2e*pkL-ZZ$aGfr$@PJbjsA3ac=secYK
    VZH0HJooQ^su)WbXD}a%m~it`kB&pgDzg>`!u6)*dmUW}vjC|Q zH6H9=fTID)-8oKS`I-pT;mxwYRvr{rJ4f4=kG=%&({sTblW|dCTbC^7J{D?W#zg{| z+3kya%Ap|RT#-YlMV5k`GCDTahnAh;$@AyW6&=kKvz$(8rII-DW3&98AqI8-=d^AY zC<+Fn2SKMpXEVj3u9qVKICa66VLB?g%2|&S6V)^u-J*TJ8!Fb_pLDK*%mKv``OoXt z6FCy?cESsF5f3x_EI6Z8IOjJrkY8b!OH_tAgjpAy}t0eu)(4i6b_{pJ7@ zq!S#G!~I*9xnJ)`;jG^dy#2GY$<|b!E$If3NsZm>!0L;qJ@E{FF6Gqq-yZ_$m&zUU z{WwbYO;=#D{y_=GfBxJa?tE7i1TLL+_@oYsc)(x;W22J-8nn<;<7E(0y#=l8fx`oc zKmm9vDLXs+vZ{(T+eZ^VAE$nj24{RT1AoAs3sHC)RP%1zILyYObVJ$&3j*=S{~|`O zDgC!%bS^;5xw6G<5Y?g^T2T^*ppk*WhgMbxlQQF+X;WIYcCq=KH-bdjN>_HlN>xoAkd4k3>}?uq>P4lLf7ZtVyTvXV91osI z;5iq6>kh#0SFMDY9=m)EYuY!qY1)IOzQq{?bcr-$pYPr2_I}+ZbX2Z~g@uLYeoy-F z0SFyWKog^u!Cfe#p`o!Y4=<8^1M#8p(BG025>@2<{;yO0|CTiMcM=lxkDsah&k#r( z6R5-U1q;tfeO>VR{MvxV#`xm;OLS6uqvTBDq$3UsB02zPBh*-rpEo>PIMwp6N#x?U zGE#kWqC%YA_QPS}{NZr|SIxE58g@ngJcRzzodah-QM^3gGAJlQcc~c9W7rhwwD*g!0i&)eXR-ZiBL$;jS!Cu@5V*<(dDztWJE4$%`HzXEUR|73|g~oVUTji)G|E$ z*qRCT?QLU7{f)<)H{}WC&_`^*q&mKfued#ghXKWXXcthBKgB;}28OX?@)L=+x>bNX zgVWl+9RUaAF(67!)*1-#)&A^ko)6!xLn*b}6;(8nygr!f`dqJ6%oZfxO|M-R4j{?5 z>7T~v4?53CU6Jbbwz7n)J32A;2Vq6z{Lh}XBUuU>5Uj6{%jJYTB(8{pgDFCci+i&C za-;pHYY7SEojb>o)x>9zkzc;ld3kf!$zSW_1`qt?Y`cIRm;=SW`Qm}Wy9Vm@<+hXq z>c+(K1dyV^c5eQQrzlC^$TX-b9zMklB^?k#OaYSjQ^OQBFRwpY8#6t~Gqj+c)x%84 zbm%APISH>}5A4}2Qk`UMu=P;><;=q*6y~kexLo4TINb*2o2?x^(5Yn>gStI3q(I1FaF2-7{tkZP$KwJ#@7YIPsPwcua%<#=aBi5zr50` z`GS#^TBV+>)0*(r!WS9s_l%v^5aBYzK##}NDUY@yNFX-3N75a>7y^P(7%c^R7_dbV zrJl|Z#5821qtHt>>wD7ls@6m{FRXmTG?dq`Swoz(u8ybL@E+CIH+ z@SZL1MANdxQsRmY?)9Aji<(ZAaVQuwE*98k%3?jkj94yq*knzHEP2k4bHjJlhhZg; zNKmeP!};J=_S8eupST@nXeI|oMST_RM1^BD=*MWh@pu}Xp=)U*vXHv}P@wLx(-*mt zgU4RYL|KuqVzl-(%wdb`seMDL>w@%h#2mcoeB(<=8LUGlEq|AJca{Pes)bAnz z^MqVZ{xbgO8@3R9MDn-InNt?iy@^k)cR3&>Ba!Er&Ix>7ag0Lp!)wK9z5fCO?0yhH zP2f3}th_0UH{L(fA9w_P0G?zT_KS{X`3#RL!RJ`P7Ym(e$M*`td_ z5yv=A^{d9=8%mE`qKU^omu5Q%P8OQorH@GTPoZfa2=gGnhxPNy{W(v7MLfCTqo;z8 z5ooD(9cQs0(NUWvR`x4uYzL|vF#sl4Z=U#Unf`Zvk<_v52NtuI7g{y5k@pZsY=H?FRIGneM-aD^Nd z9nE>4%nG^OB*q7hkCip#CU9&t^(`I`kS&F|h8*xl{5Xt4W#7Q}ozm8kjF10jG8C**MA+5VzN znH_cqF1Mkv%_|JO(Mj>bnbO2Ultf{(%+(D?3O(p_`VDq9d;8A|YCuGZhx<;Nio>nbguQi8tBARCgZ3 z>b#`yAsN>=5S~JT%y~)P$np*33@I`{6hk_P=mZ?rB6-=>gjrENM7&jtEckUFe<46l zG1i@dtzYiqOHCH6!WHS)SjP=>Biq4G+DTY|TcwD*SV4kKNT zeU7G}X&%h$k_&^dQ*S8Bh+Hwi{LkEikfDCZ!!X>p3FakNC_}Z}l`pX|k9k9F{|pq) zV9PSFLJ?`_5ucRP6clxH*PI~5IfghDuTh1;Z2Q*Ma%*-r`RMCbzg)&s~n? zR(h+|^ofk|{5BKFD2x#ffFTohrW;OJ;nPkj1uBf2=MbxhB%^;d;urYCGH)iNR$Z+{tO z42nzk?BL)$KKXpJ2>H)&p#=?|x6Ml>c@TV{p(}Y+vTe&YO2ZLw)YCv1-qPM%ZMT}W z>c|%^A{nw?-?KgKCo^6sme+x-gK56vVv8C=VZ|4& zsHH`OX^p3G-`Z7bdIFbXcmXE^f^=zk6cfD@00$``Yk()y8x|laRe~k(!K~ zTcEWL3*mw*WRlh&(sac&u8z_f99!_)-UHQNhb<87>cb-S^3x-{0*X9zh6cptp41K-sMz2x1t zt)@L!X3^`powYl5l&U`!1D1E=+1@#O-W7e(I0-4YJ&EA&8^{jLG3c~VE}7~237@m# zeh}ZJzg)z@vVR)SY4fVZTV(%-*)AMPcDAuVlgp87s)r%1xcKga3!tSvx1%<`KV`5h zFL$|Yr?SzC2c364^Ai}3Nt1!X>p)6j3j@ay?6`?aUPDqc4mw~lNqzfC4RVRQg(rjR z`Q7;&+#GUf9LsQ>rCdMJIESu*sVpn_87h9xFU3o|VdIk8Iee~S*K){5Jyk)V^Rqad zhjL*+Yo(LfYxaer;?A1iLW6{3HQd5?cM{_s-?C5==O>c!C5G>p>Zr%CHu-G6tl{E& zc0c8I{@v3-wEZzx=kAZ--b&V$qD^WjYdW=_UdTZD%=Xy`r~kOcHG&T*1DCMSc8D^o zNqa$?C1IB0OmrkI+qzw-h=CkVQDTU^L`B=T82Lvm)LDw}kR@oa(R}13p8a1H#Y3-8 zrnb&yCGzH|*oOTL!^64olxWY(Y5DX-d z)CWWDcI35=o3PRDDi+rk+~UjmCWXNU0y+UTNiBNO!M#Mz@YH8>Y7JIT?kD_FxC4?z zJ=3b<=`+jFTj5yO{moS`&fE#<&S$y8!J%R?{R2A-MMX4!Frk-al!7NB`K*R;BAr0B zH$Wq9nPB=J1MZxpf3JZeMkQWOzQ(8Za1!!4XWlQOkn&u#jf#p&u;~p0yN3_;W*N`> z?M8_o@ah*TtkQOS32dZ}#ioZ)b z5tCS`_ndI-ugFqRQAMUr#Hwh!`_!t2e!YcvISIl`p3HfP_%ap3S@Aj%;$Al{hd|r2 z_gy191^Ew7MQcUOeorl@yd?kYORsC%mtp=d6nX)`9yft{3U_RjTuAyXL7`-mPl~SfBBBCE1?)2~8vYbob!<4V%r< z=I`WG(O@KzDht8`-n^7DxRU$Ye3NSOJ)RM~?b!?(hd~JE(zSjeaB028!+bZZpF5{+ zrd}owpFd?Jc6$VtvR7z?vg~Mu9p+~6sPT>}t&7h&d?jD5YPkFr6w!^@F?Kdmdl0p2i*$q6{4Wc9tjU=Ralx;}Bn)_Z%G;rD3FLpD2G70iPqi zTAU}a5iis9X9VQU+vwTOjI^NE(-3WyZhZg9s>Yu70WOSCF7#8Gk1Hr^H^;tadgeQ` z-x%R)&9I)z1Hpi&&dr_eEy@%-yz5btN7Bc|lPSN1#sjT~!Ag>N{QT-KuOOdbx1s8F zcVU0CoVNks)*HLFb}UA1i2?6D&C@x$>iftXm!q16!G1^AyUl>hm9!0;{o9DL^6xPL z*&kLb?i0nl&WklY*RXl-lT=)O#}5pVzV@-!;Sa5`X2>>q?;O6b_G?Bm4*i^$k?3v0 zrJp2ppap7MOxMR&q*>{AD4-tZOf0N6?YoD1UguwK#>`yyCOv2FSZX&RUaw*rSWZpj z>-hX91IOLOx$0dr)qk?MrEOh?dv*unQXSa>rsPGHO`eoyGdNj-zOJ;)mN8Ndi>O$GlGQu_HAK zi~!FVH{B2-hOd-=v{t-5sLFGKK>`Y$KaKb$_*gpbS_@=r0)8&Ss96}KtK z2pQvXRL$iqLn`7rgne7}ibN_H#?|pL(NQ|qUKLjSgk`8dS<)@62t){IQJ3tc&u2$N z=}->|93+f4RyDt*8jsSID~h0a|M8vgQ*#E0tfl$CmXZl7Xnxf#S|g#~+?|O|iWZTO zKnh>^rYUmjs;Ujb~fB!Tk=PS*IS8`uHnk9EK;ySd27X`_||GOCdtQahI?&Y zcO}k;g{@PaJ}zd}P;n+MDmk>>yW(fpKNPTa4kDW~v@wKlL*J&~b~?VYDjOfh_-o03 z6cEPyl(Jd(r)j^-TlXmQ;>x$gbsY<1>wGMDjNDgxK`%k$xJnRS~JQF#Iyw@gqCZ@rEhcy0SUH=V_K>r9x{#vaU9Ur_Zx6CD>wuZEH>|;7_*>saY4TKd2WJy>8^jfpz((jewu~i7x?8B8MPy|u&+OIAKF&C zKA7^5c3uy`USGGegaES{eHPTTL0+NfWKadV;tWn+m`yd=zg-Ptv^EFI))rumRmSeo zK0`-IM92dB*ivJ&SRQRjH+g3TDSHUH3@u0)A)vrk3%=8qx#N9>SD7l&l1)NJ29a+o zJ7>ou12l9GGC}(`L2?XI!r~4`B0|9Jz}9HjcLaMjIM@jdWnvK*J`{Q{d=P~2qJ~XK zxKtF)JU1UnK~3%RM*A=I!iwB+N#S>ZdD$kD6HYvP9f-hyj$Vk~hM%9`qNuFA=*m)x z^HCwSpv7oL~WAEf5{JIsJ~86eI;rW>Vx|P0g+75 zfXurh&dr+z4zks1LkN$4s=B47Xih$g32toB4E`W}FZ>afg$Qclpf~HIbuI=sb9=Pw z%M~RqV)~Zg`p7)=y6@pVfe1K0oNy}0<4b_?oAo_Sd*#uN>p|u%Bm6N~l2sU(7@79~ zzK^WT$1Y%)=`>`5-hqP{=*_@(7Rx(cL8F-XKXS+4eGGnBGPcgOR}VkoM^88PHoFz} z6`~+hH3#_O1E%6O6&YcuC^4BRr ziSI>6L;t$WjlwmhcJYRFI4>V1Q_(x{o1ze7r);&cKrvqV1(@{0K za{wveCwYTspp_Q4f}*Cke7+?sVOD>N-KM*~HF(CBQ~6dxh>L3qG}|i&xMI4P7cqm{ z@up}fI`oG1P1m2+G65(LC!d@Kfe6i*Qw^bCc+C`>;tz9wdo6+u!ruJigy|SYA$o<~=i^ z-nEF0#eeAbfYvZk#6bkl92B1-lTMQcO&sW`*-G@ z`OeHaGvCa7pZ{$3+AE&*toy#7>%OjQ+2Zx!&hxp&PD=RS*Wv)!zRM>fA=BbwF!F`ub>;$nYJA4ia zzstUql$1Iaic%OzLnNXG2DpFFNY-yXlu;fdcwBu+b=}U=|7=)lblG@msq;#cY%maG z>8`|R?BQdyWDyGsMsiD(-T8288cz%%YB5iYyO7&tnYVRrPslt6{e+OnzNBFYAtkmr-y65ToUF)T^-&7CwV zbq+Pb;o0&6GJ0es*;F7{zSt_rq*UNA6Bgk3bc*A(kWEXeC$}Gmd5gbP^FYiiRz>sk1 zH9WwGxaB*oPTC0A-S{>8Vj2T8RumdY&IyKC&SR^!AC1yRr;CQa0J{oD2mh*arH*qH zxb{r^#c)UX+IP^D(-EI21Or;yALyd<^=}YgO4ny!?Wyy!6l4-Z*$omSK*`9+(y?Y) zA&gVAPdhlH{YFK8#3+`I10zCQ+TCZAH62h1EKWNq`wp-S2vfnN6!cYQw2`0B-P6^ph{bX${dJnYbp*y5vf!f_=`LekjB_5plNor6bU2XSMM0+2 znxnpGuAY!M2%og$4)^O=UD0b~ejERhbw)jA`~#YeZY}H0p4V}|^SnCx$D$U6ok9ni z!6*bgLV1r~a3qxzL6vM47xz9?SGQ5~>UQUzUdMv@W+}4Ao2oE+C!&vXzSnuAl&@Op z)iBG$y1qT6uWwL%zF;jkGW^$r^jAV+r|`rmoE*I6a}4{>g6|*1BYYF|?_%_y?DpT; zE0nk7%K_C)!i-}S(Rsn4{DCgx41W-YkkK>8HduF}oxF#GdTowiS+Z)#%nYO+wHMoZ za84zc^G@;B-;J-9`DnzAH%ENkE|BzfMBULo&hby>0u;xbAIa$N8{&HwHMW%-ZTMg3 zDX#4sEN|@}9y9o2IDM=$Fz7RCNfW-fiXL(qb|TKa={Zbuo3{6T?EYF-@~}$mbi2s; zPUooS{JoOx&8LTs?HyBbUZoAt()ezz@$VYC(lv(9JykkfZ`Op74)55G2nm=|B3c3o zq>O%xuIer49|VvEmQ@5pGCe z508jY@PBy0VleT-9|!ft+8lxbOsgmMIC>~wgFhNI-u}?-PgE5bPqFc~1#Y2oYj~g- z-5R}mf5U>Fk#rKjjIJDO!=rknr;Xjq5ppN{qh8>iwGB;M3#ZSD^j8~)eiy2FfcgE{ z)EX3DNpWd%#haG44;8!K$US!BPXg6q@ML3{1Qd@fn97IHdjewUMDGsC@6QAW8eGkg zBB9m>ug!J{p6s8J1z!?hSi{6T2g5^o&t4poBo)jVaRl@Gu4%St&`WCxG{*#!T;tT9 zlV(}py!mUEI>7XEUf@?(SC^SjdcJv`d!&;r)7{f!0ClXexC}N>3KY(N&yu^WBw#n= zEw)BpvHz;mMol_WzHi9ufZs`dBe0Yx`I*~ki5(YI0JcVw-rVx|i8@zwkgm(|e(4t} z>mQevum>Y(6uAY_DA&Eb^H+B3X#r6H;^WFZg!+EQP1LQ5l zG3|C4qCs|~{O%bqx*`t3SeDiIQH7cV!L!Z3^W;4U!aD~Wrtdwu#MPhp(SUMcM$c(6 zn<^7>et2^77O5V;Tki(1K}K_$$R#2aZ1=l@{lQ^XR+mri-!^#8SHVUUXtlf_u~CPH>*-!cOzImL96}lS zJ7&bk+zV=h@kEv6_$(H*xO|AgBH{C+UV(k|*EcSMJKR>Di^5kWOWCN;PR`n3ut;0H z!UO5{68Mt=_icnm!Y8pF{Oi@79nD;haujU(9K&syBaRfBdK=Qeiz&n;c6%QT1gX(F zz+_)xrMXoZtSsz1Z{oo4p@W+~iMDp?6i{MczT|Hq=c7Of$XI#SA(hlFPHLy!vLVCm zwuNGgSFj)}$BO%icgD{@c+omk)}Cfz(Oc9;k`{Mzl3ST5h_|;oKncm^+_fiywpmHb zO?oN)WxrNU%W`lcuSa7tx_k#}YjbmwdF@&0m(e$19ufY0$rStuF)S@%`3A`}rJxuw zYBuO@v5Ax*5StG;NYmL;ac8{N**lQRt69O8!!1A zh7(IzXB66;XM8x140inWkC1iyh0oU&`xrr6qiZ^5#2^himX=oY=+(-D!?OpD$)&H| zqOcp2DU&_;w9p_@kwS~an0jpj++yp)iz|oYDj_$1b(HEJt;NNpQ9gIrrF#iP zG%_68q-)L+!h>`2X;i9TfXu>CB(oFx?RRP%x}^|EMv#f#%-sCIY9Iu|Np^*WliGet z3N>x^_lv*(;7xdPP=ZFnEfJK9V?4$DV-_)3`*6BJ*kz{l>7|ASNCkr5l|&lzmc1i& zigieIc(A6)?obdyV5wqRUpuNg#w%Rd*?AX_8#JVlqY$h*N@u&VApza`yP%=DaPDgT z{waq^WNIoIO)sszo2y<;*dP9;%rV-U!bh83H8daVzNcGky=_@l#Z`ReqcSm}Yp_zGOp$TDwy*S_=*7_L19p1;FusBkt2v-1gx@Vgs#5u6*ufY0+Bdr26(Lh@&gjt9I^19b7)0E z?5NA5%^B>71j`!oafp&iN4I8Tc}(WX943CnU5?;Nq?01O!__^g+o{|ud^r2XPR;Wyi6V&E|!?U(oSmXjl z_5`*|;G&1~;?7E!C)49nhxRvMOd?2!>-jTv2vi4H1)j4hAG&q>uG}bY-w`70N zn%`*t`+G8-iOEUU6eUBd0p+%{Swt4XUq7`f9~<^VJ|F8PAF-B}sFW?uC%kcWZzi?> zyq+XXe0M`EMflrbV!~2FLJB}aC{HsJONKQ}yeHD}>HK%iK>R79AXfZ$;74pMr$l$x znuE^C_X*V)LUJ}wppKDNR9Gh6_#h}t)GH=jNhB6AFe`3$Vpxz`=u3XdmifXLDk_sA zV$eJV4L6RIOAAHX-kdNtQL=Bi9zQ*pt-Q!YnZ#hJ<)9& zoHL4Oek*B0h>IuOG6=e)(4;7L5XYcxrBDpZF+HfMPvKwiQ68hWQY zC^+C??3XrrV5*qvxcoM$Q=3`aoSod$fkjNSIcXssK+pSX_9WW#so43mRf*QEh}u0c zO8!O6_n&@I)6vCOd5UDo5Q573CUoeW%WjK4j;~pKbAB74Vvo0}L73WMCP6fB<6M?H z|Cd^LXl!>_WMc|+X>n+B^=01|^re%NlYJ^>{$K?;8uplQ7_*q^uUbT4UR}dVKJcZ? z8$0F&Yx(E1@$^_0h#MeC3eO8$?inwjerR#*>n}%XT#tO&n}`GODLgF9 zAH(tu#oawv{BOOt$ptPv&X{DWTKVOqI05*L#SfPj=7AALLGmOGd^gBRT^g$J&n+FTF;g873_Ni6d%dgWUbC%-| z)YR0dJ|qfj@0x%DFAWw+Ju0bn=@kuj!LPY&KP@?Dj%sbd(hl!(T!bKU)CZ=+%s)9E zBSbK%#kGk`xNsXk#ta?Gzi=Z=BH1(3ERA{e{!^*RE|sT4IL+;vKkgYd179LQfV_L) zq)uqWev@WdMUAA(VrV)$E3xb*vysQq1qZurvdRustJHu&`ILBNwRPg?cClyLkaWps zUh5M0D+o1cd%S}@1^R*hyas(Ke)8$1#Z)7g>mHW*8>lZ;FZ<%>dqj582iF%%rMH3c zya80^wb-ubBwA>so}xh}k;>I}*bF?UYZKigC_5uKQUmT3ju=GZ(~LLD<#yOYb(_y1 zTL8;^17ubP*v;{~&NTiYKWzjw(u19-D4 zd>fqP@0!Xx6WD>1W@|f_7yvB;{Nuj1b!-*>MjrH#?4m#WC{Sd;OJimY4^8JHb<7+;?Qhsh5plz6 z9V4fO=mBg;6y5-U7@qO>)t{Onb-;WYecOhYd};`KLwfZyHlez16sVo+$C-%5kjMajUHSYMx%%HgbN_mpzb}}&E0j2e7`_-nbx-~= zdWHTGqHXIU%2F)7GW)|2lT9O=&+J%A&o{vYMB#*@y%CPOv;l>erA+6c&L+5?6G_7j;6kU&)YoAb{zpT#{OvFOt%^QU3MJCu#K+u?CY zC^TePlm4w?qLAa;AxUE~Q`7oeB>;foRD)6fK9#C4iIKJATW4rLEl1@Sxfg;9!D}6= z)=n9ClV007%o`u2AD&pXntAu8E*e?P_`f`q#kzHS zG^Ac{hfoSlegHs9MMVXW$01Z4jQ6RtU6j?4YlEp6`9n`mKClBqeqv%GB}X-fSdljB z^U?;Hk(s#}sH<&Dx?Qi?EB8becNM>_cdu*91kvKeu^t6NqSjKcPb)6s#ly-tp-?Ih)-l zapxp=sDa?Yy-Q?8-Bhkc>_=l%_p9r&SIhnp8}7cwFwYve$&O>_taPR^(d_SfT(qH3 ziwL8wY&ZH03mv?+9B+-HGJ_*^esx2G_c0bzz%9_=ZRw&&`@GKd{VZyJFC@Ys^r7bn#Gg2>8Ob|=LkA=WGFyVG?&#(#gboR$|ElGyTf$jgE(&- z)sN}=7|9HOd>f8=J~nj+^t-!OSu+K?a9mp1(|*g!hbVay>aB}xMdS+(coJ%@{i$)R z4qLkCVyj(09UT|VU*y9ah|P`<=^OUE@XI!Yac?*h4@vtLhuyk2#^valUUqc<_E|6%d%BGcC(b zQLxjU4XkhPK~I%&>VDW(-lnK8_FP-<;&Y5uB-GjY0whrFdiSO$#n(SzL4tGNr}Xf` zq<@ABASm5;y(}jd*T}~e5Nzb3)-5WV0i$?A=QsC}#m*tm8E%=$H+)0CnZ^UleWDLv z?PA8^{8>N#dj2T?iKZ_V`_Y5(JxxLRnvaf4VWe0t&?YB4b>Z`@&*i}pM+}`_)7h7K zfxP0q#USak-gmzvlFPqD#x(sb@bga$ID9o$5RT0hbH=Y#mnEHJ1cBDB5TyuVV_!9S!n5H z2HhFix<%5`3{0MDf{VH6^4*)?d+?mNU_+)UcBsC>A|P__;Av!{*`$fFkxFPhMV#G6 zf5AW(`IJ)zoA4_X8eHzYu(QxJ^y7wC3OQL0kk%G+=;Gn=rb~*H8Es3eTkrMF9un?f zH)LdFYxlf9%xH_?DiHNKXwP+DdE|w$RPFD8W9T$M$u-3t8XGR5`iXF5?T8nVX*bJO z+ftoaz3BDSp=`P!@smqbDM>L_>u&TM`5H1Z`~J|d&<_<5P5jY)#j0o4QQ0&LJ22{B z9ZRcn7>0;SHLvlTx{F%t{5_)WM!1VO8POOg2zM`j4!uD0y6zYO_264TIuUA5V?fpq z@q6p2L+g>n6BG7`wg`u@=Nf`022Yq;Kh_DQ5CE79 z^FG_}4*$n|9rVAT_1aAbbL`Mnm7L)eA)DBda&PYQix)+Bac1(kwf)zUnnuGZjC?g^ z*#NYX@CL#`tIFLrnx;too7Yv&`~2et^GlE-yQN%+*y+~6WlZ;V_UnK9vXU7_>A$qP z*}SONn0{%~_JTZJ_iw$8h1k(Yn#K|ZCfR6lV?QI4$o$UJS|#KV}{ z(^E73-CHtm)Xji^0POc}*PH|Lg%+r{T+Z&M}BiADMY zv}9uRQvW;?p`x#^Pu>>5dPE5NK@z1dihEL^5I~AUEw_Ly#U`{%?BoNJ@^0fbt)fG` zu(-@2ed!ug>twf|K)k?wyCj8lRZ1X)1{WQ~qio!e0eR^2@5Y8qBOOT0?T7`zCusc&h6E(J zKmfW2DIRQLqML2!DZjTzJs*ff zo#D#)!;CQ)k*q)w73tY!L|`C2RS1z-qozhor32+(QF1V;%*s|8a|2VJVz=z8k($O_ z5~a~Z@M!)93=K|=(oA!OBjM4ZK+XfElO%v@?*L<5k zYw?#+r~LeUhbROenIBO}(m(+LxSg2q5HXHzoe^N;dmvYq3C9at$29%`aoP{4;+wgr zK#9=_jY{f=vdn8iS8c>$aT7!{g7JW9)JRaHY6AH){*)PP(-geb^X7(uj;+0=x?l&JEFcf8x z`FbDG`{V2pjZng5+1JC($PWq6w#R{iIRl{*-{4GV%|?^8O&zDMAbtV*E$DvG67PO zj`n3hjpK!0yg~-LJw3P{AY{7f{6fMGj!v3djLKSoT5CwazoIv0KGhG{iJ`tLCweAu zmm~to-vKWm1TC+t{Q$llxFi;e_pDG5gr)!My)82R1RDk&Ug5z$Dq&3mbyAs-0Shz_9hghitf z^Gi=hNxi~a@R9RRBDGs_jF1DvGEw*?lKJFwQCC;j%cl8g7ql~83 z({lKI(qdCfW}y`viozi^{?8kq3Q64_JxRaJ?N$E-Xcu0ph(=v-2;jQ#As||Mxja2u z(4lWMn`;O8Q>caN3q?(%Nf+I~i zt?zDyk9s2W#$ziTAj)~E_-`Kd&*{c3mTqr+czY{taaruzw86U7x^KvM&>0(`Ow4B; zXAty#qF9F;i0ez%Aaa^p83atDVC_ciB|rlO#7-cFV>^7oj~)N{wfy|@=i*!T04TnF z&sOcC_K<0%HNmoDWUuwMj&XXNDgAswb_T-e-7a#4ZdjerN!kJPhjeJq{uhVs0IRrd z2>vSmsmNuX*DiUwWnQq=sXUfzjqQkgLQl_JD!YOK3AtWO^N?Yq9o*K^@~&5waqC-9 z(9rdkyPpD8OZjM4!KJQ#5HOFvAcSJU4x&6o7c)W%HhyWrpeg;?e0vMpo*A1WERFs*{q5#Q(d{2b!ruHv03I?PE z^ve~A{~ZGlf!=UQKzpPnl2_vIrA-Q^%`2}~Br=i1C+>Ch|Cl}cN956es*ncOWdOkd zQ^;2-^>0!E^dDS;e|4<>yh{t(12Z{#4k|zxTiRLi%~B2(`fz(BulXr9to_xe3=BeG z8oV}Mw)$$l^jYDRnm^9dELPj2R|e7~?S0+paJiBx9jGWZ_4D4)_fN2`W8R?eCc`qLf7Yjl!q{ZzzOKWu!w{fzz3|+t9JL&%9n6S(9q0O;rMk&^eT-sQXk5) zGSR(cPXGqM^r62B(u}YRp1)(tzqpB#bnkuEWflQ4hXz3+{xAq+5zT zdQDAUy-F(eZgg>kinN1Sj>7DqB-5)F2iq|a$!958LsUT|En$j{7?_UHAp%=%o|bED z|3H#Nk&3q``dHh+0~iP_H@5FM0+wlVm&0F-MxF%$wG&7+&22-%_*^bnfNT2#t_`l1 z!fj6z@tUYYk0TzgjRIE@P*Iom)+7%?<#&+zJCfmBq5rV?pt*hmx_G$G=$&?HwjfT- zZB3|TNDV}T61QVU@3U<&8ynjDxBS;NuB71DlieXqXC5A)(W~5!h*^k=488hE)gJJ8 zVL@Bm4bPA~egCkv=hWz*l6TPmg1uW@-4p~`DE{%NSyKC4e^dkCo5l6zNB9#p+xNs} zk_6?;ZMfmIAh4_sv7jqgT;Tz;S&l*j`}1jb;qb7yj%!K*C^RC$-@*2kW>~fl(evqs z-i*z8Hj@?<;+@^1pZ}1U8_6?u1}bXThD0gwg!utFil_ppz@yuN$!|h{7``s?>C-1T zR|mMjp)65hcCa)8@%3m`sq|8J>BCX&8~KVa$o&cEI;~N&^w6Cng(>IWf;BMoW$62n z2PQCkVBW7_QtZ>Vgd`tiYVN`p?7PO)u%2JkBd4)%h^u zKEP@UJ^Aw|tys>VsB(|_>zFQiy53Kfa&q9!)gLaqth<}5H3B^AyTyB2;3VA#ha*;2 zMcEcG`$1(IQT;C{^Zgnj;)|Mw+Y5QVk9zG^xL~#lCevuPJn&_6!oQXU z9sjbB061*r&KZO$N z+kn^(mgfWB{~!<(bJ_SeO(`CpoxEroKs3yTT&`m!S0zh1FStPF7@ZMwiIs|dorE?X z#sMDDNKS6#olGxq@?gTIGv70V`a&P|YpyA>h{%Z1+Y_$DQ1CGz0I1WiiIh71=T~tB zhg_x{_brz_ayU&iwFVlh`10nW_asWRaO)5#C=$Sxb$#GqC}$5Nw`t3Tz6>ERqG;MswALk=YGg4F7-)K9=syi%&F zZ3D&7RW1<00gM9@DBJe!e;n`!&@-pzT;Omc)shdO944IP8z@)|j zBe4zj5o5gl&hl1yJOl?^;jxiHrSklm3UA3sg<37FR@#h#$dvh|TUQyV{);;sUE%#2 zPcmieRYCr+=PCu*MhiJUM|*zfL3L`nytUK^n-0B>1wubQ%Ev5y7PxHLwZGH{{MVYJ z?LU|aACA7I@yZfkuZW6&N&N z>VZ}9u@W9NJ1qX8Y@zh)qgt+Ty`InG4Xx_6Zt*3V^FnowU@-;Pye?VgynStg0ki;O z9!J~&3hMp^z#8n(X#GeE?Jg&(L~e8>0>Su?-2u?Q*aJXB^A6;U;Qdr+Kw+Z}AqPFR zy&j3S_11}~H|m}QXdiGHwF27x<_DIyciYR5TZa;ApY^!6QadJ9zK_;kTai@;?vc<_ z;LlshAZ4q<+zvQ>TIK}%&+x6LVU$8<<_1nyTDyZ z5KHnoH793uj5GSB&gvUb-7jzYL=B`!M1co_(?VPqG#zvF{|b=SqO}fR{yQK8^tb;7 z1N84s_Me^WFpg`I=U6j{5tt=P@w+Md(F)+aCe)Yz#V%>GZ39i-FF4>W5?PXHD56;) zhs(%~bN;V<CTZ5zbL#zPkCQUG5EktVKMq&Y zhYKW#+evectkokQvm7ajBNzDQ7 zE5M1QLZJ5sn(5K#iQ|l4-Fv`m)lFxR*i}?q{J9*=I_?DJN$;e{{TY*LOfAd#rO6a8 zoQVJ@g3~zOrzY0>x56DVv9R*(%2#Sv41}CNdpB*PW8Ubz%pTCxUmlwujBu`f37%sc zBA<`k%chW_*2i*n@9u#+iRC{>6oOg{(rEzir{|5@25bY=(utH5Y){FO#18$5&4@2R z$4Ejor?=(Jgg@!VO8F#eZE$sVCB~+!AGS2OT3@&NhePj;ORH97Y%IJsu-L6&0E3A5 z@hh6x`2qE}^5BB9$!B^qD=O?pmt>2a7HoDa+U)84+cF?SK`*xg9S?=WjkB5WaXZsK zwJ?DVC0)|zinua@)Kj_IRvfBlL$*l2A;wWM?US2YSGE??MO&tui4(i)$ zK|e{A!_oh^NN%71S=v?k=2l;GpyXJ!x(=TFFM*BTmCa)UHnSI!G&{RH8*|mqfeSes z5I8#mUjmyU*IK

    2@1>%?59dM1hVeBmK2;J-C&IuxzL0x}7eT?<+mW*qBkSBICdu zg1FH{E*;#rI&z=SaX9J5Cr7FBF)mnZN`Sq7OpJmp%cVQ&xwG-$mhh)vlWl?BdPSwB zVAb&^knbqE9e@7Ya>#49*25S>0sF5xR)>zNG2PCegpY^3WaGgmN=L2WursjQL`SzGmN>$6ugd)4NU9h4=N*C;d({c6C!Oj`D1K3F>N0snsKfxUu!Zp6+g)KbDAtpxBdS zJ_8XC%J(3k4;g(lDH`*hf;Y@S;A&eh4=!t%u~1YzOw6XoYG;^5wI**nY_8`~jI z&DwroZ(@o4U;aFN-d z)a&U?_yUDa+EJ=!&m0H*{#K_`Ql1Wk6os#-$oxecVJi}-y`0pvj1N8+rMXob@VcCQ zi;e_#f8gLUn<;sUetr#qfdfRlK6nAZp=uqXKAA$L2Ub6zB?Hdr1u&(@j|2B{PmjMV zVu%i~8T*Uc*`a|)3qsLQNipW;4C_F^))DCmke-Pf?cRhaI%@$&u zh(bq3U zx3{b`d^*yGULE7P^*~M7A)jZ*iGCIo=;(DDpz&p(i=PVwsuX^prolk^F-nHrY5i)p zCdgZ|GAdO;ULEZRg%iq)m^k-rrFDQ{zQU-xt%+^k7!;@H#u>5qh$;n=W959d%*@VYwcdHhSnCAQt5rmik@q<+%j^Ul z`Aa@|dUR>9OvUS^ah#J|K!zJI@{yjT^Q%Uf`Z(DjV^b0_)F;Db_ZDI#Wr@-VMY_2< zGuX;tVb82f3v2n#$Q}_LFDz=r^C!(=?*Su;6NlF$jTkn|`$_&i7<8WsblOlWhbM{= zuH=6tiwk=_x^yXq%qS&a8*shNm%@-~D^|wh=)cX)%hPL#301f1-+VYSvXKzoi%LGB5e}S>kql1U_K}OHA&&i%-9IY`TSS2-)%{ku@ zN@Z|u@FAyWYxuxz=sR-;=aPf)0q(O63dd6!nNnLNPbJNGsX~%jBS{VV4p{iU?h!1V zfNhJmA}W&Y@XT4_CDu-W18@Iz2%hbMARmZcRGG4Z;CSlU?!t~B-I-I0pZPQWwoG** zU#lKN18vj$pN}qv0&CqK9^)~t?Jl8z%%vHwy?guqY0X~#;g^0|?b9qA_Af@~6qD)= zp~n0C-{o?5X?Bc^`nvbz*duYv%vufcQ8sA=2~|}}BF6FRMOvsnN|y7*8Vl($GSCwS zuo_OPy~_NZgdZW-L;W~2;>b^sCeuatqa;l-KJ*W^{a2foN|GB?ugD~6M!_i0qW|q= z|J8wiyX4>P1^wGL#y>gsckW=IAv%8z?eF&cx845T4g4?ng1~I_-VX~O@^kk|bYE{} z3ds3+i8P5|P5Ly*dTzg~|9IyTZnd}#S~VC}8R-qmjS#qMb%p8WHIMJ?D8Q&j10?URTr zC2&?oKF5chC)}Y7k(6dLXRo6Z*oganlXf8A%+z5W4<3A>WJ$4_>~~x=oO-o`m11{oLI{p4?pItn9-)>v8KAYlI0| zMVKw*;5XTRe9)J0`>L=G)O|NQ5>SyYriu0sY6+N@HfQsE;|ocfn_Zn`38wm&2d*La z5L`EZ!?hD}$2$)%6uxs5gX>EiCp#v6;chC9D6rS-e+0q-QyTDx6`^P3QdlN4@CtMs><7*?Q z^R4EnVb;B>JL|E0wGeTUD%Yi;1P(y7R7AOULi4B0rj1wLt$&{P5qQWL9OCs>8*Yi< zLJfgR^A1D0^B69@4dDhgd0hv}QMrM{??@s0~a#`&tpI7tnzPArbsMm6?y z+KXuee!sVZlzX|#zciXmy91UL+^l993*hV+PW>zgrGa+5MEN1kNuXD$U=iA*!FD)YY2a&=R!guUERn0iSZ_NbVZg0=Ht$Uc!Ff1qKU_%az{q z6BL!$SapZF*t*j_btl}f%PC2s@bvL>NGKg3ZrfRMbYQH%a2dw z=OJ#Z+bGX{IXH58nG*T}*OcMUvXcDX=gH{2>fNZJy0fMIs=cjV2`Q;&wv{q@hxK}N zl~N17G1Vk!v5@zmi(O9cLBh$1p2J1=CJ4<5<&s!V!^X~(@L-C~AQfBR&gBRtPvsh2 zO3~TcrC6r{in^;Sv+tY6Tpd4>peRY7zO$G0U|BLTO=q{;`yn68p+}XvtGMGY;;^zw zkifH|Ksb%#$Lw+<@+n{SeP=$Jh)MbvCi)k~n@5rP%H_BNvf?C?U%$q{6VIj20a;0d z)7^pG-EefO@WSB`Yw3`yuu<`Y?70x3TG56r8u!JEH~Iz(1YeF0pA95JGItbL#h_5k zIT-(m%DiXh3mVsS z@qXE94aY?iqtGH@*w&VAnXLxzdIGmEosWhHSSy1lAK`T~Hc&UeyUReFTIXmI4sHpEz4~DL1#yj(fjF|*8j_9bN!HT0NVY#`c z@BGI`7i-XhMAK#0n64w-sY!Zk3OPS&XtZ{O!HAvSkwU?H5PqpF))igRWOuyeHmBS-)v0Qd9qc!-V7}R<(FxTo1sn5hfPQnpisWo;|OTndr;y6Y? zyYU8eq36~D5TWg*Z!vh{8{?In^3*}8G^;p~&v3t_rq<06(+W*b;Do_&F5>W-VD?1tSXk^1@%s}HWp9He>O?hOchB^^^egW5q| z+OYkI?w!E#W2qWA7F8<3c6R9D^(HAv)awJZ<;FUNs8ZsBQ(Zj?PX=DY1Sc$4r}MWt zQ7`NNpqIVMj6g?`q*=xGkdwS@ONo%x3)ljvir3eYwsIWZFW1AOk#Me`Mn zrA>hX5afsSr$sfuL@mFZor-)XxYt72bdoK=1l4o4c#NEU2i26p*+BM}dlAo=s)*-W z6?>~{uRw9s%K#HsdI=!}@|P;6n3#5*|6pU>((pr527w~|GD{hdEs&hNyfrc6=vDi& zHF+o!Xj*bxb>1E*gTv!=f2|@$1)lA8!#3ZUn9v3HhM@JheLO06Ro`O1ZYL}JQ7 z4lrJjvgxHeB|eJW?|O{U3a(&bX8f1kL`96cRD03so3($W?RovI zXtyT*-uwc5v*duHKFZ4@{OwG_-m^u>?=>yZa?N{tVa-(Z&b?Ihj@8^hD;StK5#x(? zg5TTbx9dtsNu<&g-mP*+nDk%aJpE|iI;b-~81Xye89i<>u85I;D#4iQJ7wZ=01VF} zm*4y-QZ_XX*5Ra=q$$6nuQfFCZU-;S=G+(-vkweH-^y__Bi?)j^Q>F~T zrTz_J$5H?7FYGv7qqia-O+ectLH=FNSr|;(AWEtxJ+64Ud9gBaNRgpXy)u2U$uy{U zB&m+xe7?WMKANj#aI`e||KGoo@a@DsOMZR+S@Fxvl)$%WT;$cXs<+8>PeqhFQth(7 zJx!b>Tbs~JEHdJPj<3&&JHxx7r;wy12PWG10zP7(pvZwQ5)AWc=sQoVD#G!vR>X*U z#0uMZ0i&2`J~T9wB3$Yodc@3+F`FsK*;j^9Fq;Vl^HXzaKfF?q;3YvYEgnl#Wx~9V zN!;0P{eY|BsTc<4nTSY{7>4Q3+$Y#i6tO}&n^g15e~!HudHb-_+B)H0@b-5I^REyl zCs#Jjp~lz47#9s(;JV)=H;|@Ek(%^E%qq|W9|25?kRJJWFr(jD9$Ra)Aq8XKqd%q3 zeT}t+toyK&%a`TbkC)^C!al@oRt@mgX7SatYS(_=U|9Tf?85<4%XuIevIDq~FUU5L zhXI1b9(0el5BLH^%OQjB;usBvC?tQ~ReL4@xC}04vtE$kPil63kY$6#grlU6bth#veyj*Gj8b(8u#S$!!KQZ zDY29fT3wc61(RMnd>E{^F-R6Z7)(1XCvP-f@Vr<#FpB~g9Ruk%ZqQ%DQ8pke0^!kVsat+^JV+vScluD@e&?;An;{rDD4#Fz-8{FfP?Pq1=q~L>aCyOs%cS({ zv$*2xq`|Z24rxPg0JpM!ApFPXMAl~{Puw0pfOg#r%-r182v?>4E7bWV?kamG_3>ua z-mP-2Qn%L1HeGAp#kj<_Wn|u6^=QyXl%Z#(N0{)?6KNMKzZJRA(ratuNny1AbPxQK N5S11w5Yqnse*khL5{Uo+ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Activities should display activities #3.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Activities should display activities #3.png index 142e3b6736b92c6d5936c1fae265505ee6ce1d45..2dab2dfdb6871e3cc496601faffc1c85122e0ab2 100644 GIT binary patch literal 30196 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 CrDE*> literal 30540 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 mMD=VoeQ7mTo_WuA%y$Tor diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Sticky notes should add text to note and display it as markdown #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Sticky notes should add text to note and display it as markdown #0.png index 7cf3a281fd85c4416ea13818ffcfe552285b54b5..2dd76fb30dc4e6ed9687f2435d6b08d63b512317 100644 GIT binary patch literal 74032 zcmcG$1yt4T*Db1|fRqwa5+WkqNQaWrB_PrbN;gu{4I&`Y4bq(=NH<7H2+}1;DjjES z^?kqZ-246SxMQ4g97AI7-`@L)^~^QrTC%;_ zsL1fh4p}c1e8aaFdulJPq%5zZBqF9GqM)iIs-t>|or#T!9iF{(>GGXXNzq47oG))Q zUDJ*6J3qWQ*h{p-FW^)EL`W$fLKBE4_5Rgev~*qgH($wG6y*1AJlHGvGP3t8DM}V( zB>a-iq+jaEFV7EZ51h_7cS)tR#ND)+sg8as5(}daUXIsz7Ko+5^SYuy-}k0)U3bE@qb$))=`zQvB+Z0ny9lw6R1o5SVJ!V1E z`6()S{W(Wo-t3zwwN~=4L)?meuFIESI&KC3eRtG3{l%-%wal@-?zR8EmKt5;sr^{T zsxHN&%TtaEDw^(XZHca5)@bsY8xpS^AFOqW98CFIHeaK>j(%PLYxs01iJ)%}Gg-@V zAML7}d;L3B3!E`cp2*UbB)@7kYPPbnL5@|N+p>nDH0xYBsnSv#%?1b4-SA!{awx@( zECGdIo%GC1TbF7(OooSsq#h~Ko+MmMiHwYLSx>9P+AbL_5RMlU@VNNFBdP4~>7v>r z=Kb@T7v|B$-;6&9-6JkfZ85IB%?@X)IsN^dJT#|iTTB`>`H$Q9%pcDw1U@(!eb2PnU}m}>tWliO?H$o@ zkDf1Lk{3PhWS4NHk&DTORq^WKQ5mBH0Seo4y`J0l8rRg(r(rz&Sk}Es+8RxlBknh@ zRJ+F?Qg+7eiWOCqd)%GAy$T56lkH=ia$X^U^>}x%+%a{q8nWtr<*fE#fBo^-sJ0gQ z{)OS<29wiklteE3H|?-gGdtLVEJ?dbT zsnNYBo)MXrb7MA+$BEsnnFdQ$_Z2yxlZld*nXZ+i;DNyLl{Ix9(|G?;V+oV;32W}8 zjbf(=(JQC!E+;L_w6ytmxEcemk-I(8euQn|7l3TH(l@Mr*oHIYXfjvkvW|uPYb&hB z?98>?&gp(1V&RVRx~1Y) zZ+cxdKZe5RIK6ygW89q6ob$Hh2h!=z@_^T61~-HlF2cI=h==wKOIH)fi8u}K)TKW3 zq@&C4?5EGnu1aQZI0{->X^i+iXYL(Mj~zlLVs|WI)KVcz37Q9tK3ksWVKxT+M!lVS z&dxPakufdwR*Z|fj2gB6Urpo;#*#l~ni!Q+QDNrywKoO+AnAgz-f-k&GP)HLt@30Z zhZv_*h`cW8G+pU@{H?f#M)lTwUQY)$8NRT3ZQP?X*S#etp*^+kK9w2{4$ic_ZENaJ zGXw!GkKhnk7_H#r;F30ZwTem%(bIaJyT6>*S5PV7yj>Tjp~Q1g(8Pb%skJecB>BAv zgRRW=&%RN=_2?3K$8CvMmso#y-*8g|l|nO=gd%6S>rIMkWO7_z>(0tvPx0;Xw{;03 zJ3nsFPgFRdxW9DB(XqX8Km2wZdU1oir3bdBgyKV$Tjx@`uY~A5XF7RA#o?>zZH7uVLmFhYmlCCvAC?R`c>+)L!hwKHiLb z9MQ@&W#6}CxY1|SgraljH?+xNY+71Qu`Qr!ZocCQQ$G+08hs8!wHFV6Q?S9G?0!Uw z6_ZCND&emTF<=%Kx0L^db-g{G+s<0Zb}!`N@Ml_@SMBF$!BP;&aqncr%{lq%wm<96 zFQiW#O3>8-p1B<_9Xw^g?5%fXixAu+{`D4FlhZtYl>--NLQ;#Orm9zy zZmP67$ApXDDn_1^@YvJ0#m@plvdvOyYVdBwV$W_#8>wJ2Vb4*R%3&rIM{tSzctw-C zKAtEIcyPOFr`4zYa4{r{gYyC9?|FrLF0?0yz-3Gx4T74^=M!!x3sP8PYe*;7V~k_g zcbMz<1rb5;!$qEDt1n_aDy}cLbGRj`He#kHAjPw%YGj%u4-s(NuMEjtUJ6pb@v{=> z$QWo4m(!+@X!*LAtotp2yp)x zhjy774u2u78Tq14I9^RXr$@1>S~wwku}G$AeQqu6edRF#eiD0*-EnHeYJ0L1ez|2` z@alVOZ=WlK9?N&0uQPB|SNszcCO1fX&-;lu-%*wdFeX7*bQPQ=FCyUdpi{Y+Wxatq z&S54BVm|2ATUB~UCkjfcO>^V1Q3LIax%Satt>W942qMH1PebiX{M+wqyPx3RmnH05 z3?k4qGdJw>XDrYzC(<%7z$Irs4wvqTNLN$h`gW{~ZNZZ$m+bk=y(pePJYXv+XgIf2 zZ_lyeaG82?Z$Ch1_YkT49rvaiKC^mw|MT+5GP@fBI3HdE8bls$j0Fan64&mNnH%e)HwFdT-2VZ_))>6m_@}0B~+KN6Aja6OY3q zHT#*H~!wsMppbz+2K;nC-!){A59@7x3wdTt68X~pKxJ6YH zHl{W2L4q}W_;&k;l;LER7o4Tf`|fKJC&b5{b!zULRDV69q{0NW(5XFZ$T7{Y9PO^G z77Pq&Su@;uv&5iXmWb5V$2{NU*&w4)dnCqxvddS|VO+j9xH7W?{}Gf0l|qjH`VQ+V zU9Sqi;DG~1UiWaslwWli^|ut56K!Auhg}QDQhdXeLyFOD&<0n)e}Ls z?GjtTCc%3M=%?ehhx=jZV2F&13wKN|sS{36>Io%P*nyyWXrHmf%KAc|gPL5XeDNlC zqMV1;hd8mBQ{Uk=(Xj^&?8Eov?+9|n$H((}JgfD;l|Pmd5{|Lb6|glM<|Je=xp@8X z==k;9nN^ z%bd@q2b^8M;r6=jQSKWWXWWMPWop>w_eMgZVPaegZuSvFde=}sW0gVcC!J*Ii4esCISLyAC8vuJ~Qzexd)_1R;HE#lGZkY zWC?5COKn+8ZdHdUki61d_Xk}ALxP04Y#`EhJ`G2!Jt=FPO}$iQ`gFhN+1o-*pUpPvt%a4C7e`Cgm^A>QXnA?t?XD>qd2~_CL_OQ= z@-5SSt;xOS_#U;(-yYKA6O}O~MRi$qF)CiJs2~Hq({XIKgJG%jopJOfS zoKUM>iCjVbd0?>HivIZ;@}bQ|5?2`|W6}pQH~%9mM#GW7Gz_xOC zJZ^aB=g--P2(Dq}`ICxk`i$_JSXOV{4xMH{;Z($nq8eW@or}&5iRH5I2VqluFOYXi z2Up2FY{?MGBVehK)neHl5d=a}gPkHy1Fb`!JI^9McAi!X=?xtY5@H~0{^#jtK_Sx04;LcUuElp%P(zHOY{hl$C#_BKnJ{fJp2qJONU-eJ&#iKrf9roed_1qu z@*ZPjEaX>0bw+}q(Ur54pR-wGLbI>~G9RXFY z&3G~-lH0oEmhiaDPECtG?kY7rD@z<=WB(&@NGw$IF||^(r{k=f9%swd1^P0Tu0AVN z4fjh?D>6Fix0mYab=#h(zIu3qLq;5m5FSU)gHGm>k{M-rh|^texyRYhYjEesbu6?y zc2;Cdz`bG;T$Y!vx&4|RSUuj~lzY|4T->-vD@L_sxPs4e@x#w>hgxw%(z$Yuarg21|mhK*e6-?K}govm3^lw`0R!)>WvjD<7Ib1A%gP(uOW!*3t9xX-dz>Mo}duLg*sAJ=+-ahx67#TOv=55D=CPtDl{E z8~x)wz!eo0)%(J=h}?FUl-k312&RvPs-i{;(HjK6t-d^XEIP~qxHOm>MNJuSW3FvX zP}fRM5Ej1vY%rFa1ncCmbi%N=Ap5i0-^!r0s zFQge68EZ6c*8D0)r>2@q6@ZXQD=V{eCScyU;bgD1V6aL|-tw~x*P1mh)ck8{z&-;| zZ$1-kStWe}$1Ialmvy;d>7GH*W|1*T0ezMoSrxr9W^qJX01;gRxI`nx1Wb94+MZVq ztGb9$F=2+uI@f$y(B~ko)g75=bqK%9a4(Q)CrSP6T6YKAP-=2(Y^C>a$8VPCBw+*++?y1-w zUvzQX5?%?vC^ZG&mbq4{>QvO6;UMY%=YSTJo~e2v2Ap0N3_AfYP*$@_D}1;5*fDD> zs}LycRpi6Dm30wN3iaOl$TuDvFKylCaaSPcJS`>78J1k>w3fM#JJ4I3P&G9@jf#f$ zkl+@!xTED|i={<^K_0fms5tCD3Ayzmx-o?rWqxa=hc(UYG-gHHB_REc?c{Td%Aw>R ze8UJ*g|YE9Bw6x0zrYHU;E_IU<7w4pF-3)2F^y&R6VE-4ERZaClH{<&VHS3qAN~5i zI+lS_%UZe>N@jx}Ad18h)-q?$d$h7=^#-B@1jWi5^Z^sVBL346dJe8?>rRe`!`w>9 zK3-LLcz9xIg;_nSdp%aSO?%et=V3d>Q-FsH1gF2s-ckEq<~+P&p`p4SuQMHxD_IHa^7H!pb# z0qKI+XH`?ho!tUbUqMZG7bAbPy?C8-57cwn>Us+I#T5JC5<&FqcQ*sN44bL$GWY@$ zNvEe_pkVn#ZfEphx%)Z1OHG_iO>G=Eq0jLzV=K$c=z^!-4hdC}Ng9t8Qpp3-OKPHl zMGUDHXId`a!rOBKDi{=uN@-w)rQ=Z@dP44rO>V{f#?^ko8bth1xvSdb_n46|*j7yr zSrJ9J~emr^k8=AZlVzZmZ z@Ufw1O9c4C?6h80FR7>$w3H>ZK3IknTl9`tGaKzl19(Oh=Lg^j`U|T zup~jK=uMl}>GX8884~(18sf3IsHDHX(7fmT&qwZY0_(iz{bx?!IzM@QT&0nYEi~?K zMdx1X+#1rW+8LVDQRfPZ;?$0Muds0sO(o$GWvj0 zFxGauhOI|&?|!SzM4LhBgn3QDp@RU@$uE82C7^+ZV1-?+Qkp(DAz1^;&wU|M4hqM} zc;SY@rNiZi>6xjdW~JxZqgIHJ$@jy^WnnGcU8!wGmlo9P%pWj4sQhL4qtEC#I%J}R z=UOa$qK8&>+*hka@dIDyW?`RN{UNa_k716fX_-|ZU@fgSE9~z*t%QAiY8KP?P;a0& zWhqn3FP|=`BLDo+)zupK2R`RL?|vGH^;rUxkeH~Uy5-aK$%r6=>K@H2TRSSNh6^57 zwsNxpdMo!{hvEA>Qw!yTiE>px##&I2|efPR1uLqvPFZ?eSGIL+aJ?C@_W)g*;n%;E!=Jud2%rmphY#j@$Et@g7 zCN5N__9v|LZzf#pvh_Mar*%dWRI-)Fq2KM|1`|N}JDxTU`1Zw#l-IAN+f}Up0l!*pl6U zl0Eq1~-uYQz{^aSf3V?H5_Vct|)E;L<0?15Z(cBbVHzvBf1 zR-1do$A=zz*M$R>qyB-B_v~>pBy>(rT)sDRm?;0Sy}%463weBHj++#@&^o@?uix8Q&iy1LmTAS5kZV~Ja_n+s z&dQz>LeISRa(9_O;`(#YVDRC{F$=zfy0cG%Fm4zPx-zHb(J`Ia?!vK)!5!6L77zze z`R8IAeYU&jjDNNG-vWy=o9x!x@YFJ)}uoVv^SBYV8)xVdjNXJO6YV zH1!D=jPElE9pvx)HivdHP|-1srKN~Ls4R6$E*!eI;M)TUn{u1Rv#dY1Wg@m?f-nTq z7LaXb9~MEu%a%C9W&UUk0Lx@~O7=?wKiKSBjmLNi)jv|d4F&el4+x4%OVhhtYunfY z8%jaLL~pq=s@C{B6-b)^aMQt(S;at(p^eqxxn}4+9Plib~a@};0%N)P{_VB z$o}OK-&!72^S|n|H1N#J+x?`)rDR#8-_O2hi`Rxb#7__S5 z$7aq#JGT`qKYGxmg7D`39d4~&X7T=qQRy@lS!TomQi^zTZp5Z&G{mXa)*PSl6gVEv zEoOUVze>N1h9ZF^dsz$#PtNks1)s?%JJP(HaS*OkpJk2!?0P5~=5q1XYscl_j@7+H z2V~xKo~12Nz0C`L5I9pn!6tNOzD-}>J}~MZ-YcE>vAW#&@pWsFO~oy2{APD2&4H5? zH(9_ArgbhW6L7WN&d$*6Mda2y_ABiFn-n3OR+IKvuB?riUHIWQytX){m$SF6yw^!j zBYyC$xvl$^b19n0mc>XQVEA`614u(Mu1j;!((-HK8arF71({Pwb960G! zi^1GQEQif|JcUC7KhI$f1-!{DFj-$+LAyw|-c^1$r-OGcJN!`5G&b8S{Sq3@X-IRI z{AE8+#X~MxB(XF`SPP)144N#ll%+pcbZVWxU^s5{>#7|Kf|~*S-i8GTRA+a8i*YqD zYvb&S-caeMrYv?THL-w*oR*SVg?$=M){yPs{AOhqc!Nc=y{0w;?mIo5jhU&b@XFkr zTxugZR)EiGGtT5>LK4DeK*0fQv>gyd{2vQh9PNrI0EFWWk_y zLS%K~BRynk8#D9g2ni}jJ`wOK9;WHpNNn*X3lx?<(=jZtUV#D)ql4Dv>Qrugrg-)9bZs#CW ziN-ot7K@>R5F%rMLIov?C7~!u1O(srkWdrwWApkBP~nAi*km-OYkpKf&(9qSU?q}3Fj zYIZm6eqZduy)o4fsq#9;c81A85qQyAnZ2R?L!Sd%=5!B zi~__cBzFlA%D{+Zfz(j?-_Sc-#=}wrPcVPXQh(V&8RR`BKZEajj+TFyp<&B1p62uQ z?>rU{*&^rCR#i{ylD*DevU+Tw3i}|f#YKPmSUsS-I(zw_2*JYiJ2RsRy!fmzp5pI) zcmva~DwHGu2ycmXljdYOwE?+j2N*XY#UBTN2N_N-E#z968vdG;J>6Y@X%F~V|G}CS z{9>wFwYz%+44I4pEnUjY4Q-Wba*IJITMvghzL<|UQ|mBg2^4?aqa6@@c>`h3t>|0? z_CK#IbvUsEO@P>2iTR9y#--hm4%-99u~`W7m&Tm(z3xdM4m7`M9p0+$eb;Sv7FBzc z+(E;owga}7_2?3Ajr}Rj_wS4aU*9!X3!Nh{9FhB1A+&CYJvtQ%ZoWu^$w@%m{U!+= zUbnM#+B?e!X18SGY`Ca{6N=(w?4;?zYR94wJ^3H`148|rN-E+_4snEuWYM4r2R3Hg zUI~r}GX~`-Lo5`(pa^9~q?3JwKlk+4iTJ-6ywPBhJI#z3g&y!+5?8$3;e;t%LQV4} z1+ea&qj_>LWF`LYgYO~V|2K>IpGNVYs_y^oaTC!$A7ImacyfFbha@oXJqa3S*zz(y zR9ygiQ7O)g*-;KmK!SWbS$qLBjU*4)2m%lxu5-Q(eJVBn_bq(=kQWwyw9SEqdY|QU ziaXB{)RgS#SmHvV4uBPaL~-6fz85fDD9r$Q+z?DwEHU$y$7IRw?2!HM9XwtPTG7dY zil#x=KhofxqROr5FC$vc4!fBk@%F2ksZ%CjqSrn26sl`=$S$jU`{k`_`rVe*FAu^C z++8>xaM`i#?fpsv{mFqT)zcRpn=p8j36c&NZ}XeW?P;c!&vJ7M5##P^Pt98eHE9ti zBVJT*PICKvTn>oFJ^=x4TT8iB``3KjI;hwtyRFKRd2TvDe&tqu@^|YxLYrrxR9o}3 zkkdYXSklZdF0o$ti5OMs3kG1-iM^+89~Bi8Zu!06R(N@jYId{>_cDyZejP^|EOD4C zIAe2=lOn1K{_`(QUYFqoh%#Kxm5ioO8()hIfatxm+baC|BS};PsuR7n2l!A>Mqpr~ z$+EJFmNXzTLP^2~h2H`yx_aq%^@W>cw9)~9J2j7P8h3vC=l1NAxwTEX1Irz#%EE&= zWneOdKY7(iPR*yHQoB;MzWSB#deWfj>^F6G#E)7^)lB?uTYdK57bSrnM+lq{tDo$? z+v|1f?(Tz?wQX`~;Xy1;IuZ@y6I*E|7q-k!D?yihRd?qgZ&&SssHt#vlZrgW?2J_usA;?-*7}381Rg+;B(w-ez8>whLCuM7cDOH zr|G^hu+8{c4;-MfWR?#h1GEEhKi)u0445=F=I76!FNrB4K2y#iUi<^t(SaB@sMz?U zw@Y;GiHT40^Q|7PtgSWWcF&9je3P6;y^i(}HMT1@)JWmt8yJksAAey(`8m~&JCXmO zn-4K*CZOBYI?ho5YLg4FNTr5wxWcioCH+RO^9;~W`J=%J3AZh-fs_1-nJGS75$Y!A z931wLHR;$1F=34TYwqK@MxF=MZ~etv2h-@#AQ^EdV$t=E1}YE7&YbnO3n}rC1E0&H zMkxoDUP+_2^94@=gu#kw_zcHtD3D>T<${9?k6f#Mjd3@FBe1-<)}N} zfNi;Y={^@iG9uzG~<&re9nk)Oi_}m#lu} zzK3@0TOp+(YgWQ1{X#1;$3MS^r$4N%4JKDBXwp>gGedd<9V?gWUZ-B_Re7*lm6=&; zsf-4>y4gvnC><)FP&nT`IcBUuto=aY*!C3tmd&>b5tx3dhaFn=jQuTOV{*x4G{M3K z%ItMV==iqhJSO^Prusm0EaO^f>I7(*tUceF;lFj zy&z{on2VK1=%_KmPb*4h^McEx8EMU+>LYS+cvj5{%@fegb!IUW*B>2ZaO8f5uL<_5 zfxI!NFaA%iH$I53eZtfSMh=VG-GYP zIB;K~U*#{iPIQJA8^5+JXlS_EET)KOZy=uRS5#Ax2mtyB+|}3=4|Ngg{1{+6;$Zi# zezB69`{g9mvSvRN`21v&POt+UR*&s{d{zEd~=NF4YU#6%J1`Mva@2!|6<-e zFc#pWVp-HYXODwV94bAtcu&c3W6l(g5kTAb?+mNpGa(wdpw9GLHGywbYHIo}KW`$b z6wxP^lc$=WL}^qx$uj{RSJF@}hZYw)P=ou`FZ=F)JKvTj!9oEFJ3m@2)CA)i46SH^ zoUBLjXmRn@oA|en;AjAwy5HR5O-sYnwl-q=0_tp7qx(rjKi895cSqN^cXkk?1R)e& z1(M;5g_PcxFtsx1JAw8b+g)&YNCfoKDC)OIe0=@}o0up6+y{-?@zoa6|*nv*@1JY#>v@5{2 z!Vxb@5e!mkv=W}v;Rp{#*my2K4Br-OO8}N+W@p)D7S+3vIDSCVf}&0e<2m+poh+@n z?6fS%_I~a{@|7g_`{ZuNn4BL-DOw}xi&@xD(sEqpa#60MW2P1U?}O#GMTn;N`BrxT z?`3_4Y}uEVj5&OZAZdL|bb|&q8z;L3g^Fi|PsT=|PjS(((m)Lv&I%8qUfQG67-+M4 z_Dq8ZKeS>Yeb6Lh{Tm~hmUXHSTtLfMya??^sd;E}IsVeHgP0vlun>AHL zfWJX)vfl&>y^+U1d$wq4*@xeL_;CAvMkMGq$IX7jl}(xZ@+8v2v4lhA(BDNux^&Cf zSm-AQGyktUu==ar^u4_vt7h;GvFP(cBy!YskIr5$;NXxver$awAYJ01s0NOooH)dW zqsfNIFb8-A^Nph##P6#)?wl`L^A2j+4jyU3e-cdS=G*-#jn0i9xQJYGp0ujtgO6 z?ZedavxPVC71d^50TGMTqc9(@hus)d(bhBG7m?pj8xc9N1LK_hgs`k0R|# zM57Ye8N3fIJtzh*)1kb1s}hQ^HY%v7#RrqqiK&5EP@HGdpjnv3x2GxLU888#QT_`q zz_kMYPkv@!UMBc22@;<5UqAm>qcAB0cz~ZS31O*0{{fKXz>Ew?7KsIjtwXAvJ)M(D z;P(no|M#VdcYi-~1g(4E5WET3|5ucViuum1EQF=gJW(A}uq+Z*d7{rJF~7DJ-mj_d zQ=1nLN*^*4q{`0@VIgUG+4vz@k>mZ?kl<74^WSIYWxvG@YSO*KbRWlwiwj!x90QQz zkGO6)IlF zDb~C}M(HyPsQf|=Exjxc*-izsF0kb0mD97&9MqK#_#k&yPtV-I8h@mS_e`U3k>D-R zX>RQ#r+p%*2`xa!Ae;!mqkFt`iD2=Cic)Znx8ZX1L!u|w8ttz+HZ#wRzb38%%>EoNweVO<;owCK$+}s#) zri))2LRaCC6UZt`@~p*8EZ)M$9pfP;u(D#dMGx-k?$5~0jq40lm}H9!g|5tUF6sza z{inpYgMz{zswrzoK!*jd4!IQoz701CNrcROO6Z^3=;@*cR=&J!eC5<_&|&xI9qQ`W zp#>!088ChQug-zj4Q-c(Mk}~v_})y=_-eH!x6w5v@lq8Fs_L*?25dw@p{NGXUknae zZf-s`9b4#v79d-3{V!JekB9i+)FK*%w*j_jXz27hQ$1EK8_kFzBY2&Vxq5MZyGTqZ zP6*!g8~cXPtp?CXX;w?51zK;QzW&vgho+>4GV^T-bup@gewSn@G)E{>2I#T8M>GOA z=VUc?gGqoKhOT+k9%<&4iB{5CW#ib!3qj$LoF2NE6*FVDC*hy zP2*;0T}4!wW0Mhw($MoP%GDamRq6Tp)!V(&=%2HGbmr8aCN^1GD zLc@)owmEBSedY0#D_~kg)pEt|!#M~^r%B!MG_6Q8tEs4{NT;DA+uQ`}<4$p^7cL;! zJ%@Gg+B|<)nnG3jZ`w)LKr=4Vx2?dsrR7pndZN@HBs$D}=b8eZAKW59=JNd(mVdU* z!@RtbMo`Ms&|ND`IB29Mx&_gglgVS|6h;~5j!+OESLmRPc z&4Lmb46y%j=Kzo?`(JOxg?+9J5j(Pnq&1dqcuE6X^ezgea*RB5J5g$+i=nbtW>xdjE;m7`K-wQ+~&{1OD9&@02S@ZN?q`%-+U`69 zLN?5fAAFz+ehpe!phd`d#*wtJun^omxIrX@=UKFGL(7A38~_AJka9V=!1W3L|F8-_ z%3CCOufU*@RKFIG4{gDKw*aLStvl_V*l3tohS01bxfv4(Mp+$1wBWO!q(tgGe*`~V zbcc-}&r^sTz#V(u+3|*1WiZY4=MM38KT3R z#6s^;Lq$%5m`H`ZB3^iOIt@uKASc2YWr8?d&Lf6fCxl5NLP@-M4!(JJ8P1bO2+AXF zg^*2Q^IFf5y$kvYlo<&Vj2v*Hi=Jlr$;#G>4>UJHIg^A*MFlc=T4t||vK>3Gvbj0pnWQcHh4R^wHlF*PeiICMZh@;Zk!H~73#QLiYyG?$3rh7(tFi!dxC zG*v5VX-LRg2zXMle2RcxUp&0;3_w(R0qgp86%hS9`O4IZ@lh|ODQ_*)fsq~y9ZXS<}2y*qT><(Y3K)3@JwiMMQ`w z6K^^q65G`)kE&wd#>+@4UCH)3J5f+s$gFMiqh+KDIy~g&$Qs<-QG#i(5E{B(-?1?? z%G4tsZ@f{&$+Z;oz|bUXVebe^9?ir=k{-n*dC#9q%F8zC2c`D?NXu&vDq4TXfR7%x zM%&undJ_XVx4@>1GUz(iMrmoy^pDKUd)}7|Ei6WOZ?j+^MYZWu2s{WHA1A~^Q%gu7 zHW8>RGAMh>U}%!97ZQG#a0sev*`>LD+^!6@6GZQ@$Ues|;}fVnkmFqlERTwR^Ei_V zr+6TsyvWNjfo{Vx2IwVz$7%IKNVZq#(3CJU+?qg*78#L*Z=pP;{ zqeTs@)#GRY>7#JS} z<;C;zlQG-uKea7m11b55996gU>1s@Zth)j3EtN`9b;_=8nU;}}DJhfm0#!FvA*!sH zr-hNQ=yTFPhexJ@^#~W246#wK(lF&eOzV6sPeIQPcSk*I#=)HjlG(C#V9@W(BR$Wq4heg zH%ANUVZrbThkMUfC#*LN?MjOTX_ zp%bVD2OAz^lcIHO#U~ZJLzrs)f+$|e&?q}HCQ{0%5?5JU?!I73#QYGNj5?87X7)YB z^@5HOdg>@nC%#)T51wWeEE*ck{yMU$tfi%6%8~bAy3J_?VakhZ5Di~(wypD+zH&S7| zFRRQwkC?N!(xS1+>$mZua2_lBUwV7p!3s!vQznJc@{smVC+qkD5i(8N03kAs-*`LA zLs}2SmsU>y%Thay9FBu^VSew}*p=3#9F2L6Vn#RUvheECaI>SESuiH2e!z$L^2~2k z)J?9whtt!s(6;#8ZsECIRCM$!TG}SB{61L`1NJ7}n@%4Bo|yn^z^c>NRZ!N55W1P# zH!WwEA3~_XkqyBeuk_@}===f7_R-lB)zSHqj&w6(ydT0K z$5dur6;=yNNk@B$z~8|L4|Cuwkb=!I_v;w-syDb#8Mk?Nu=u_zOX=zA4ml(h(PUYF z;ljc2Lq+!C;H6r~k|g{mvU#qfi>Q(CZ1sD+1F z;D$IQ^&j_cqT--lHFKy$oJ$6_A#a%5uJdY1%1LFmt4F|- zEQFc4wJ~qMaN^7_uvAvmS+`DHw`-lSsr?G4>_1HTeimN;vAB2I%_iN~?b-PH=A1H8 zwDuy@GJiK~l?V^{t1@9=3Jpe84$Dm;MM8T+Jpv0lhIWok-@yX)#<(B9`g@E&KSJ4^ zf3dau;)R(*WL%_pVL2IKL)9t`G_tLAuIvp*j;+AtI`BwI{RBAuVKAgbg!XM)H$E%E zspx**hIh+|imqGgqT$FsGrAcE+e>GR^_usi=JfOe)A;twA8pqj5938f;@45qbqDLc z8e7T8*mqVC|im9*7aHWJtD?Pn?7Zv@gx38Zt1xMDOgeQm;|@%(W>9FoqT9JorDOd zxdw<<^`2(x2bpl4oP<=V2t*|ZXZ28|{#fl-fhVeb6WXxT^!|DZK*jfA-mdIf<7V;M zr2>A9zq0LF5O2=XxEK=Yr>ub-;I zm|)K1=@~$2ja;i%O*Jik_)eG)I=g$w2&SZTc0TrQx}&H1;-0@=3B8-3P|yz66|YND zv@-G8dX?fh7|3g(11lYwcrd+YeFO8FuRitx11l1=0Izd9$EYDeLD^}gaWmcnE7Y{C zWLbFrdb+{k;li|S;ZG?U!Tp6S31|->yLS{69^Q&{Ko)ad3+Gh&zk;17Z@{Uq_%ON|x|yy(6J@4<>kgQT#Lf}tQacuT~K?NZx$7d-s6 zyd)}|+l|n({`=i#Z5KOUu{I+Na?U{QTJ;g`+fP5}vG=GJEsWj7@Gp5rSvrxvb9A~| z7oM9{88)?n0j%Nsa|2fhN&u9Iv49pbX-#ts4QV=-2b|T`4~sS5;4GLq)RsP@?2?kt zsvQ+`7s2Jr@0$*{B!pa`pf(ez6kIWK4{Aac^}+qCb#ltm^gQZBBi!rPk$vv9^MN(0 zP*aJaHHHYe=I3Fp#v$!1dl~DzvW!e+Y{T#K90B1TOG=3<6EhZCoZ@R*SPltGLQ0w| zc<~95uB344ipnYL^j^nA`jr=@>y5AmWXLc%2?k-TYJaw z7IoLvNLgd-RnOA>A>yDu{ICyc*vd4SunmB&mem2eG_CvJZ^w7`KBmG$gIy0Q6^nxO zXD0RlNPu#FkF4=a;-DZby8P$YU)WV=+(t%BXOt^F`Yz*x>$1Z%jK%4E-c2`QkKmLE zrjTom{dQEpNu=FOe}PPh7JXT&loIV)umuJSV!M8t|L4aBiO~7xw&$*$u6>tAb=}ml zV)NW#lksaa`n?X%v@Cba*loUWeFi$wwXl=Ej{HNL3nKgDWF0bWODP2HjjOc91x-Ls z(5`w%zg2sa{8spx6dNEhU^q;2TqVmW29aWC=uxIWn^_$wsVKLEL#~O6PZWjhh#l__ zI9O4DrZu(}3iuDR*LJ+$e~b*o0@@EKfmtU0juBv%nyNv1gr}-fJ^5YTkF(^r_~EF< zJdm?3XCKE-8Ho7sG3lGj1Pxwc5$E&7U+*9vAXsN+*6YZ(!XG-MA*Dhll}URo=VF9u zmfMw?a~~ID4T@^;*8qYg#~kurtUs2P%@+6~VpJLRqjqxY{WG!WQfyy3AD?<%iW!vy zGDZrIwzf~2!lp#uEvP7X6Z#Wx*L5lNHt&xg+Q4U5nShM~($;UtB>uwWcunYQ1}uRW3@tKIQ^ zF$k^FSk-h^wk9JM72fs9@DdtumJLDtI+)~lVq!^2xy3clhJMhmVyYuuNK|o4g)VQ z;*TqI&ZL}kbwrQ_TGtu))SFhGKlAg~E-q*hpf`JQ} zZ}m9|f&U#I4mod-S}0<4ozvj^FkdW9eoXYDC`A2ZdD-mRQDAmrsHL7T>OcB1ud2xF zbx{_lQ$>JDx;BJF6jmbeum&P9=t)nmXH##_Uc=$IV~M5HpAc5sjJZc zCGL+!{3*741($$8x}Gw8XqT=I0fRH-cfRRCvPj9wd7iIi_H=1dG}1?lkxp&Zc&-mdseJM3n<$$N({HeUs9 zD6bb7xK{S_T7Fp3d!FQA-)?)jod$cETw=H;1uGi(Q7hp?Ql0;O5Cn=W?}e<~S)z*S zk2a~FjE<_<>GlcMsuK($1%6SxPBH|m- zy7sqS79!s@(f@C_z**q_dlq2jHf;Xf9*zj5@db|GM+EVuwBw&>J$+wOK>|-I|Mw@& z{>G2vr%OH-7%v5pFsfKr|9m~dU>R>uJ^B50h5eK6ZKTo{|Gc!(knmI#0Z9Wn4@<-5 zCHx=7V)&%c{EJ$roh++w^tN#!$KVP4Mlf+H za16gzI#ah3FU_;@x#`_q;G2HJ@8GP|*bKF7gyxXfCIqCoMLshT_EqumbxiZB#}B{w8c61#kND*ze-z{=D)H>j58mC_w&hiEBNV)4POr z84XFnK9l8=-zC4xG`vcLuY_MUU&2dwWk4PDE0@3BbG?Jit4&{T?ttF;EGYu@s?WP{ zG6x#m_6W*${Jzk*^3_$Ke|F#EsH36@m+ z%Kq>H@;j7_*M{e_^50p1;TAl0;{>>5W+`tRtp6=Q}_hBy)i9anQzN=*D7j1}N z_l1J4?~HsI#Z?901Iz|dUvU7hLaHTSkVf!VkyxQ>dSnu|03-zN8yp%L3Le7C2Aq8? zEqNF6$D9btPRs6t)w+I9yV=i!f5TZ+-sDlB93QxjOL>}&CrCf_i@XI#SwBI!I73S5GqUDg) ziwK*mhzifjfSzTWUTn(Fo_@pWcHw`H=qaE{(jhD?h;12{SuBYBr$1Yyz8%f7%VaI7 z)E|Gr*gPDL0fm^O;~{OKC6KN1f{X1i70Ko0u7GpBU_<=JS3r}SzgUhplNZ+o8qtrX zt@Hj#DX0`4>Bsma26tL5?Ps(+4cn=8%AZf)_wD?j04E$!L=xY3TozQ|@y~uLD%wc6 z1?*2sK5xvBz|3*d#s?a{161Ba2#^z;kTUBoH2CGD{O{B79Epu%y&yadeQj!`x;wVO zj)XVX8Bj|T677zINCcj9GuQKC{^6v)H?8;A40z0X2V2|PhGK#ULQVQlsp=2r2x`6x zY{>Tux-(Fddt!j7tFxUrGGR0MhV$*>P#RR*%Q~3z{Dz|kW>FPqH~ov7t|+6`vxG?|GuAghp5YV`_SK zOo>ENWxJ3N0_~*s;L-K9wT)8NDf?+74Bpj;&4~_}ph|tT8H2_C`j= z-sjm2#YMKD*K7u(KMsso=)YaOn8MuHQjOk_lLLxw{#=slby8e-F67dj>;`d2b^W0v z+MgWMG#?7PkvL7;-x@eumtAJo7MG8{Ngj|py^xk!@TTOk^gVDZ{mjm?g?YhSWzJ?4 z%ev})x%uUa5w1abgFGAXK#Z%_z z&ZA=B29+Q#S5X&xaD)GtU$0B@`l&&;j_HB3%k1FF+Un8@+1+;IgKc}Ivn|D5x*&|?H@Kei~J3u+KO8gae>ac4EL()ASeh{l$DSo%LLG*7% zE&ZDKa(;{D0kqET-X5ON_R!{p2=_tR|6lCAWmr{R*EW1vw9;K7EiK(5B}gbJA>E>c zbc523NW%sM1VjNzX;4yXgCf%1(%tZmt=Dzm&wD@5_vibL@85GAuygIT=9+WN5$8DP zm?j?jAEQY1lLA?JzI}a?vK}%IWbs=rbW)=m2AZbr#iE|T)bANtS826Bg$=430tXwjIgX>Ch{pI;% zMFD#J&(>}zL8BpL!6^P%^tTH=$Wg-E$I~yej9EHj(%y+=U}t9YE0Vf&oDt-xlyN^8 z++37##aMQ^ZTtRx>dxgM?Im!ScKwG|i)YO&wkG`V&~z~}5rhC}X44e{`H--%?A)&%cbjJO zZ5dXE+V>4hW&5bHW_-~I(tVDaAVEg> z`1pws&xi-8Vf_zZ=yPQ{EDMrMJ#_#8I$}`t-;FFU-`!bCy|+Hf-bmwVb(1;*CXn)g zUf|Nn#Kav7hzkKU;3=;fSz?8Na0l6$<6=W%gexHsml~In8q!Y@l21 zXI8tjGvRvjMO1k8t%=*lcuM722R6P(;J+I4#L}tQeT+#7<(9xGuY%!pHa%f&>thqd z?(5=e$Y!>DVr9M>$dSktPAjjXfhPJ4ywTs^<{BWS9BrD5zKFT5D^p3-{#7F$ztZK% zDYDOZG{3+D<}p}Y`d@RU_VkNVV_}`=MF@IDhAZ9J?!^~Q7wk)#d7$qg%^J05xC=(K zzBaCF_V(5Cp7Q>4X!QrsjnKHJGW%|{dV_kjZ&e|rk!?Gh3#C>28M%2*Kk4_xutn*< zr8S2%wK~j7wY^S7gyG#uyh+)iU1@jCfIVGGUFSDbVDYh?BOwBkTH@UP(s`WO_+iewqz-Bv_c8kAHsW!3>?S9PHG=R zWG$m-gXh(hPgdDuGcB=@k7$sn6SnJm0NNxYYq_xU1ps=Ga>0{au-P}XFR$V$(5;Cc z$o2B)5TWP;4FmELU?Z~hYLjTdX6i#+I;aV1VV(DfPv@AaBzGRRQo9S-B2P(CZo^=) zi-o63L1`Krr$h>btR_(s2>7Q`hp7lI9l)!O5W*}h+mhOY<4Z_vHn*lzI2FxoU^o+R z?4DdPejW=MH$pD5sD6s}VtHC5{QtIN&6lBgn3l+UyPw;;h%36crf@)WtktcG(70{M zg0i7MYxp%R0-BXoAp{3XkbD@hHlEjX5S|a7dN}a2IQtz@v4sqS*-mwoP33&E&Y}BU zZl#wsbhpK@7^jNF4!eYbKope*C^#cY(T;7@U>(UcO^CWFTP{B zwwQWP3!GEv6DTKUZthj{inZq~1uU%9P4@YXDe2(I&}qO7pr!uRM{@4I0RKc%xUul? zd?Ui4_`FCj&wcb2pUQ2Jj)9pGkKEE|@`XgWg#G2LD5@{;CE$mVc?Rj@%|j;I4aImL zdwP1mpxf=do1zll!i%I851o;xmC{!sDQ(aRALM0sRCl)LM8-dbwIXcrR1)LxTlDrJ zDdIZUy`+?KM(&5ZrfQ>smNTzG%8ztc?6~>l*|Xd%Cra$3lOGM0P(Uv@w8VbhdSJ76 z=G?UVsC$PGJQ{uXQ?GINbM?S$62TC~D|Huef?a@A*!Q3FIa&P6g4>IITy|{{nglTN zk>IwhtJXEc^jld@*3nIb7CU z&S*{~zZ`V<2f)~?bdE}31i!=okz-q+vaHxiGxn%gCqHB$gcrgIwM)Hu#UItQb)uAu z-m(A*;4(gT0}-0|(1_YiX1j^mg~0*HDcK(z&@duxEHq;uX%Oy>%NuZmdA!x9j>`B# z25GSLUl&w>xpp`byKvaF&SeVoD>!|mC@&bDHob$^+lS6!o+Bfd)w~?_OATb@SFW4^i0eMD>BiwTJ78YRtrhb&+)&m z{vAUAGxxmjfLPdgy6QCU!_wlOX5oCKW}(!;RSDf4frRj?__Lu)L*xr-qVa5nLNTkU z-bd?|d&3Tg9KQK) zbxU5`hxFd~2P@e0$ttEKBTH0V4s|vAS*rWID{eM-Kf8v6UF(Zc(A7JAMAQ)!{Qg(4 zHMX9)Eu?a>uzi>sfE;wy+0nM!Zhuted~!pv`z}dgjm^d5>MwQIrs}cNX6Jd&y1Fe+ zl8!lDU&CkrijoRTHz?IvV%6T@D7BOaD5dP*B#o#~MxUtjTYLBZeO7&a(#uQ7Mb6fA zIr;eyr)OrgDhl!X4mM4Mru-iy&Ufn|%Pb8tmqilvoJAB_(XRfIwzDHXKWG!BvXi+m z(?%<=tBaE&5Vvu%y*|Btn8ZCLljb(hF+5UdYLRQ#|HvXHB}Gb4kMR7kr!C*0fw(U# zYP?I+fMfb?Oe6y*XP|az{KmM4L2=!It7y7U{R^k%P;RfoxjU^xd95;NWPv#q(gwsx^|j<;8_VvF$FMM1V<^Qr)>K zCeAJ^D;I>*N(K_DkOf$d*Ul-W>@ijsX2njQY^zQWn&3iOeS59OpqLkJ?&{2Uo`LTV z%J`G-wdr;)JLnrawcq+DDqIo6Zg?rDc}c;pJ&~sFvw#puP^Q9cGU=-b~Gq6(_X~kbeGU| zf9#{yCo6GzYW{FgmXm(C<~;VM7cyQUuL_FYdH9e4SHkrYn}(HX`)huL%Xo;4++%1j z4vpt^M#eZk{_YPXT3 zWuK~fx)V3AqGlUhpsCF83s!RSW1b~o%n>;`B;&3oOe2)sZv-JTGgR8pR%WAz z|3Smrv^iMH^K|!y(uDW%rRu`I(G}yq9D3 zLEN_&K3=!_ncsqv7a=NVT~0xA^7hzxjbf)~SFT(+)v9(B526+bmJQP|5&88_+7%|^ z6Kf}vWS$vHq__(^uZ4G8wEVTXy8Zghf&%Kk1Ov{=>Sd;0YC8dRg+0^i*DH4!ho3er z@BbYG;2AGy4a!50s%dC+Pk!;Tw6c=wBhsnRFrF)N+lLUQC?f%SC}5iI3C zZVs)Y*ZA~k#h!<9FtfJjQ__w8Tf4Yh+S;ZZ1qd`W2V`890$&zVK1t5xt+v6-_?K!c z-ex~R`&-?L^sU&;XSl2zYIdbJ_(szQ^$rep_+DYd9I35}UFx$ysjzC3{`boHkIOMr zI6^-ZilmexMIz5n76=f_o(-=%sf&Lb&5qrCnstcPqgG5 zGQqVcg*9usOv>(oot&b6Tn)OP;Rv2FIJ(jMz=&fZ-G>0ila)2EdCuNs_}|z5G%&hy zLtH%R{d*B5OG^dEgpuXU_r>9eSbfKw+WN~CAOFNa;O59YfpWg$qsP7Kd=-^=HRcd8 zLZ%-$jzhFKD--AdRz0v2HtzX;a8>A!Awe0F|NK`7RyNc;-Dr9oh_i%}9J8s$NB-T= zGY&}duqLB6QNcIb?#=Hz0!h)6(bvm7hT099QAUuA}GU(M@)qF2AjC4 zK^fL_u;EI7)dGOb}*g#tNM2e69g-~@=MPDDS+Gf(e3N#XT zHjyGLjICX-)S-WQg~w1yc6b;UOF?IhcU5RO8krk&N0``Ouk z3J)-hhP3Pr~zo{B(K??2kzCA~S;MW9c3Es#xo&wfIaV_dh|M=Y(aXJC@gPlu^y*I<1R zytTXCCS*651Sb2-!)f;E(rnuM)9ff;9naXusc}7Wawn$E5D0G2ZMyETrdeLQ>Q~A3 z*LFp8B3d7bl*6zDBMQ5(hDZr(_GDDvdJ+YWJayR4ea4vqbb_w%hp!0#q-0=wsm5potAib#XxvJ-!KF9WrkP16mV{y`daMm{RjV8Roy zM!<+sxUzi-FA6CzF6N#~O4xAqa%2+aP5r(mhR|u^G*LQ;rt80)IRw1b z6*;Vy^M2l*T3HM8uEg$e^3tL4XCJ+~_HtCu1DE<5j67UMHyMnGiOWY_CLW47v{9>m z|5n;luWW(R3#9t4q+`X7OCs@{dTo1(itiKomdLA1o_xEB5|6!q&X`t6S$jCIH16rZ zWE4jF(cHqSV5sXy4{eK4Slf?ZmcQLBW1s!q3hcw9k$^;-$|GX|SfN6OMw;e9@u*)V z&27pF;zu#Zb!8Ogbn~dvv5_29Nncr6HJM8zk5cV(v7Y(^v>eFK!WxLKj4S9+5!J$> z?`;s~Wxtct`Vmxq5|WYUOaLb|sCc9430AHnCBKVur2xy#Ip|}9-q!&Xpw-k*S_)uA z6L;mtD2geu)rI|$kkVu(!^1Z7ZT`imfwpWySn52?92UHyfW83{_2+L{*RlaX1t2YP z=QAO2n1l}fP3|l5*vnk_ZwH!TpA(p44l&hbUn!-l5#12)O~zxz-4t{U1zH=1mOKWyNp3|Cq97VK9|BNQ)eDJq%&*hku0uXjkhO5U z(d9#f%u2GvVO6;J9?RLuM&HUSBg0!be%wvmi(k2MsxxC79bpuU=`#$2xtsoKS?gz( zOek)nX-TK`PNu$vqXaBBb22n)ax^`)lUd|3O;%P}ECWu{8(y?GMCYBxA%?aVWG5f* zvoyzAoOPD~lVLp7$1B_qeJ9&+LrVAg9_#4o=NRw@bHui7;|m@K4SJvu@nRinuMIn~ zjpMxh3894f94yeUw)R>dFRrB@u=^bDJ9X*oU(Gy*%)rTJN0{4u3yP`eU!Ya`ve+cj z^oZGjkkG_rHuz%NK^q%nW>86Gb5D^l4Nc0n2%45(PAQA5|L=c$&;Vr1Od_!UL6H7J zfkvQ-OG2N*8L7M%@lX)(rQ?0GFR$kkAtM7!;<_QYA{nnc1KUrl+XGIVHIqv5?0OMx z%X6=MUv?chl0f<7saL)=K!7R&_^>scrahFOvezlKlT#=LLh(|&PL1DZqW+UFDAo~& zo)J51Zil?e_^<6ArtQxh=?*I#r+8y7N9`te6;vkUC6yRf>f3uZe|n@p?>Af6Ts*C~ z5V{0KrchZp!5pu~u?k&3wNQXZ^j)1O#9Nk_qv#lgU+SE3M;9G4Nw(=gq7m#zTh{&r z8cUAP7C%s#6UP(xFkL3>uPU5xtRGGL-M#9y2qcuEPtDuy)i580?evG-G`0w1&oke zJ5Y~Y*hF7%UfAYc9ti--0HBh<#AuAc9Sdk%HgR}>sq-n`@NtPFf5q9Ry_)*##G|c| zU~&0f4x!dJlsT%&ovR;pp@`keSI`o(`SNmodcq+TdfKH`kd^@Hw?F&paIjMPyX&k* zD{EXWHjh0kj_$c9|HH&VW#3RBf^*7A*CorR?{7nb-&t9CZq{NC0NCa=lZ+I=Llwe$ znupZjLXJ!TJ$3}zHzU89+}NN=Q{-kwqi=1jzgv!8{IHC$?(MY#@jt=jpc}sksxJ z*6}aCG%t{*={`YH|955|t2n~wg<7!9_rH;ilkmpdCo)Dw;sGxYLK+P{ur6&92E1I9 z_$Qg*{0~F*fH43=J$UUOQp*YAB97l_>dOnE25K*ipkz1qZ6LC8uH-^Gkx{f2>;mgx1$iTNU{M zCqCtwtuUnV#%FJ`>MtX-U9-n?A9$$wx9~W{&y|2zrnDQ;Ee7?~9T}NL{hl$YTyG^& zR(D`KcU@8jiXRWP$B!vMiXHWR_Dvu~c?Z@H{14}Y)NbWoDE4Ap3?$rUB-cH^26pQV z>8lxwX&N}wR0${P`ixvk1j=~jNoHyL-FtK$6z%fq-R6*Nw(23k|KeZT3WZ!GGqbX^ zs?0B)yDiW=>Meu>t=;%;Ovet?sgbi2mi}@lf5zrEyF*!v7+?&>^1qIfNa;o#-sAos z?&d@z5SOwU9II_0W-LYqV?+RfCCG!;Jrow2+zHkreO7Ka{H0;$tzYB~iD9G;%e6zVYgB7AU<%3Mm0#D5?G`Q!~=MLPqWZO zUWHtgqE9)7rj>@WsmZ4*sav)MBGk%BgnC~7`?>Z714d@HR8ia7P*k4*J%n4wk;rA- zm6#TZXlWr-VO{~YN-=ChP@j4(4)j#@wT9G3%L1^C%pG8U00~xPrI!vs-z4zY5^d_v z>`}EhA+1<7m)7n-XPWH*exTY+J~OPoA^QHCTceS$-&3*O&}JTH-f>z1^aeSKl*e~f z6la@~N)J|+*9R#B?uhuLY>5H3?T=T3q`+^F?XzXt4gdzzlB8tL}x7Fydgo9>ZA;@`6 zctwZI7q-h+-A_HM&j*h_@rEZ19Zu9pkhuye?l{M9d#B64B zBshd=wB6Hl&jvR}t_hsh$0#?fH&3q228V_59$pDRSoKDDX;+%^-)Qt-NH^Ww;aPrL6}4c;?!+Dr}yxIVoN-IlCBa)G8>6=W~D&C~8|5t+J}4 zuAzY(KSeKI>MOPD>8xb-#B?;fB?JuMo+R%16cQu-2BFfl8s3N#y25W_fhAK3?tvcMm)3 zkAjm)iveNmu-^1+i~O)`nZTQMsLvG>Ot-F64=g2qSrZn3ek#v8kP z)5H3DRVKZ=L_{P!Uh}$8f!EIO<%%>{_N!V)O4V+($~!h>62cyC;cjQv zvA%Yy5_jb0?W~}@n%mrJdu6o{?_6Di+umFVN;sfgb7iK32QZ56IMGSHtUZ{Q*D7ed z*CVD_n)8K9I5{-O(!$(Uz@c7vL^Q)P*}1rcH>sCT`2(9d($8bPG9*0T>eJd_m=tS3 z|I?OMB+_3k^cSosciTuCakcoq5R4I6CCQ$ZnfV&JYPiC>r8G*idB%jfSLk!p1Kf#- z?lN+R2Z~1JSp7*WO2HVp1j7u$|9l=I8XHW<(wmOHUKQcO8t2{+^Onkg{DjIcLzrypk&vo#lrvc z%9}kb=5&8(0IH45Rgr_MQ7zbNp?2Bk}Zp{Jv0k=0&;q8B9P*CI3zEe789ln zV>I7&9^$R(lB<6FGd4EfNG){qc|lz9^mgKWrqNmX$LB@jo%0^~_;m{>b)Id(5-+e#gZH}|$oVV?)j ztwXBsk!HpYvrldc)apoZe6P`q!fAxVRVC7;hl`bP7#XvL21jNVCvR7^_`*;{HHFCMLEU9MI8_m>*EglH z7(tRfmX&>#80(vw8uI=oTOce_4Gj&26?++FW#M2B?7AQ6X;(iL4}>CjusZ#~`j?H> z%8C-IpeVgABMN%oZm=BC=+1qbhQccRM*;81U{Ak*L zm2U9)XDZLiU|kfm{I#amw@Ppa?^OKz)J`5fG9wna@ururfN464c5Zo9(#|4x`7rmg z(a)cE1i_t8)4jw+NK`C^*Af8jsVvW*`2v9_(o45asq4#GXp~aw$n< zMs#ZnJ8vy3uaswJ@jE87yu3!ih<}ddaJ$mUrGdRQIa614Cgt>qV8c>tO&mS0^%uE8 zqJbA+e>=BjPT2dbP)z*S1@kWL+n#p_`o+(G(3d^yBajCD2YqX5Zsu^aJ#R@!RFr_4 z_+^IXey33KYdHl4R{puQ`HsFU8pBq^m1bs!2V-!jqrDZRflOZ_P*tfXUP}LfV$xUN zsP=G@>FObto-6RlN9GtOZk#-+Hu#1wz27N%YXzCYlcdCwz3A*FkLtwv_?KSVMXtm* ze7pS@CM;z8@Zmqn=9dtA$9cpny6y#@8 z{iGd7@`jDi1l2tQCDMmfZj2@pupMieOC3fj7tSsOy^n!y;8oc7;^dA*A~%V&vkeVb z-67AAuv^!e4StU#|3!IpkG++!ncTQ#?op?azbMcTHaF`0ij%}$@O3_|8y)2zin;c1 zj5-|O&evdDS2^?jUSh8Tl@kq6he1UrMp5R3s9c}!p~~e?7x=Di%uIQpoA7$9-S}iR z(S&ddBln)5_T-)tW+x_AI$L?x(b1U~f}88zvSC!=VAAjb&p(@Op!EDR9S*Hhl7h1e zO<(}*>x0xm-}sg?18n4Hw(OE;JG`iY!L(WFu^n+%AN!;BgQlPnJf4HAql4==FB>g& z?5;>Y$7tHo-Ue-{Q@ODBQma5?JoV&Cw%Ht_AZg<4^R{XPs3Aj;bt^FOsT=0XuKWEK zU%A*B%zeZp#v?$Woy&0}Gw+q|c~&?Gujd~U_yxtVrKXg9ndna8Rp{<9lviYLiC4p^ zFVq+NZq1!S;8HBg5m~)4*i3qN)CUipnOS6it#!{S<=||Fi^s7h19uF4r=_zeDF26} z0K0xmJn{0oLhAwKPMrguH(f-#M0~r;x~j?NOVUEMg7MkB3q~cpJa+Axo|_#W%~s=) z-A>ZB+5KkJt}e4$R>>95lp8#3=!C>#eGy`NR-+xEL)&>p4-NZvk3KB%6w!`f@Se3; z*ywCDv(l}_Y6`AsC3I6@!&&DzPHy(HQ=-_$O3u+61ZA?=e|n2gxyBXhH}Y)m7DM$+ zyPM=m+TrAaFT=aoRdQ~O=&?Q_Ek9nb4*+kX3#DowTVrqDvJ>&Z&9ToQAKriP*!7^u zZQIIfi`M2#)i1K|p$i|WBOmz>hBsM>S4So8xW2aUdsn7m$7CKu4NisSNTqOUav75d zH&wuNf1LVYG(l(87Q+3N!!Gn4#uJJ=UA7}bm8QguS7c3TJKwJB`7JtaJLQAziIooF zd)AM57Bt$7+h<;Cw|NuCy4ulra9g#=Xj5^|b|@sFflJ)>SCn52{m6io;Zm1d;);@z z?iR5@?G_DBN`;BTSg($ylQ#M|wJ?eF_FjS8V!6eE^$>-zfNgGcI_~LuRUG{>?yZ?! zTpq{j_mmY;-ocCpo}JNyuF0Q*Cy$QNc=wxn28*2uBThUQd0nsTeQZb;q^0u99=)Rv zwOxMRcU-b(M#r{Ksxl@ZP5#8;D;yWA%Vw0J{0?a)u)S3pWiQ4|nzCB6SM1#<@NdKO zQu#^Db$930uKab9Z|lF-h)Cj!>^iSmxjQcT$7Ioe_-TEouMArgN}>`P(B6Zmelt^Z z9`n)5w{DdRX&bBEGKV=|hW4Q-pUh8z^)b5KcM)y^zeoPFwT_`3_k`l)kTiUI8%=MC zm^-=HGC8vu88%lv)Qj!xT(Wv5FLZey6FQ$)Uy(j3^dc*)n_&{*(2fXxep=@@1m*)< zGBrTiT#+KGe*2=ua_ezWk=qgliRZ0LDIm!-UJF+)N=pmH$KG)riD#e|>f*vqFuE)9 zJotw{J&a!CtAsezkFQj@ndZFBvV7cRR;=ZE9+UJ##`OutQc~MDzSZgY=%kiJJHo_+ zBQ2;N#clfUTcaifTmIOO*yn`B!1g>OCMovD54PZ+FQdz@g}-$&WGJ{qZ%Iw2u>AS1 zulOkstE`hRx5IK2*c~N3MakDnPSLBGdGGU$djco&Y}Q)~Dm~n$NvVQs5|}S2`wi7Q z)AutrH$Qk4(>C;c_c=2|2{{POn`-|=q0lXC3|qngIy(cZ?#KKKM@D{_kxv5ELgA~e zsD(F{!!3Dkt}F>o)&3I|>#}`(9Q3?lJTpwMqLXCh%t=gIm9N7GygXTYI}+zlC6aUa zb+=tUn%cUHT^ioG(eaU7qA)^;Qd?HI*_@%obV3A7XLex-Mt_xu`{d;8SJ5yFyPJhj zV=@L)+Q>$NnXQ9u(yG$E+T>vUn9HP~L@l))=^s`j8fYzP`x$AKGjesH_xL9zD<_NT zadHHE3P0{WZDE+-P&A6l)wub-)z&ul;q>wqGIytj*iMo17SmhNsi~de!r1LxEU@cT zH?+HD3|B%*ISD+SK8pP3>3Gnehx|t9>-h@JlT)*bL6c8P!W=MnHt4%VF$@H`aFS$& zi4qcw?;=H83x8Z9(oy7LArfyCjZRLNy5o$sD{Ci37q~AUk^BCAXw00c>D1zi|Chdj z6cYv3hYM3<7?j0WXxT*lLrL9(u|ySYl)@1ilt9d=j)Z1e!S%uzafG@JJt4qE%$ypRiyBX z@(#t@Pq;;Y+BCLGGGMIVmb?YdR%~319bX)>t_Hut3s<0fq@uMT8eQPV9R(wv3(C@i zHI|eD_|`f`>am))kD^U|8&gW`+HYD>A*d|9+~kcb)*5@yI@nk^ZZg@l=__J|p`@-` zw2V%ugk0c;?~oO$y?x+4b)mf0tMS?I38ag1CH+KqOr8gM$3BdWj*XFn4wao_L@3f* zxZJ7p!`Q`Y92$@Q#U4legE^q;Fh8jxctlS8MM65R{0zay*8bEmonTCQYST;W8Xopk z;RKI1Nfm7cR#1O4bL;ndQjXxudrrrntaA4Y1nrtfY?@(~?H+n2pP#Ur^F8 zCe9rPPH{BOAM7W(_qy`@I^5BJ8IxbDya0apjSNUl9tryt7Q4HJ?a_+HNInuMycC zuU~q&KT|d&BqF+<9<-iQR_VVh$-dzFRHT4&`GxZ6p12bAK9kpab<#Bxc_X1p-oSmi zh+J+#+NR8&`TItBlhq#RGxJL-Hs%6s4f>yx-R>0$7g-B-ns;X%M+^{nft8@;r-9J{ z)!O$UdbN-UUj4cooDkAC-Q8Os^}Vqvs5`~NB`(*eN#{5Gy z`vU&}0a%(^z{$YG__r+I@Jz;=U25B}(cC;;dvv+_7;ml5AE~S}vp(OQ^mg8&gx*au zL^#isWbV!V9gOw6PraEihgebK1b^k7;qd&^&`qR<=l_}zMt#wGRw5-~$xqitV#m9= za^+>1eZ@{(o#s$?iMr)si$8OT{ZSklXan-m3|bJ&thbrrZxMV1CF(3Uxuzb%L?iq~ zE%Qzj54jil&>}B?^3K-U*J8MO|3ffK&;qBHKP{mCde_k9G1aPXW|3X7OXj27LIoEa zg>^*L_8hMB;-lt(5g|SWK{iox&P!N-%~$Z{eVNAQ)|P5gofqbznJnT1;s;LFKlP)H{!hjhKxs(*L}?(~XJ2jNzn;dM|uo>T<>!>ODX6b;R`_;_!aT z@y}FCzV6xJK|HapFZ-qjcS=H;g@vtgq(3H-?wh|G`poit09`3}knugtz#k_{&J63! zJj`)jnN+Bbi3xt8_Tpj-W@vTpZ9Cs-+#7F*NEz_&W8HrvB=umQh(+c8iPYuT=~$WD zthZlIUzQzHhc7nK@$ZMK6I{st>q;mgk&`NX(LxJHs3=^Hy@>kF4{_8NK6n?h-yyq% zLouE;7SZ{Uogbn9_>?xrX||B21>lL0c8^U$iGwQpl>k{p*uOtN5Vfs8U|9Fu5gw{? zL#Q}DygWyO9Yh!d>ioEZnyqVI%Sb?A8P`@GdQDU?^Y()VOstjw1tOMTf>Iem{14u| zo46b>{X^zurZ*u`s5&e;JW{^Ta@N%>tf-_o=f}#Mhb>;C9~x9cnftwTIUDLL*%i7w z*AYQ!C1k?lySq0UlQU7`29}QM#+&;;TohkokW%5_XZa&-IrFSABXss?HVf-W!APQx0{Ua`&ZfrhnK9OZxw6{dRcU=}_5oOtu;<|s3Pdw0i*agviPEk=}Z;lkZZb|a| zREA7N_-xz)jR_z>R^!#!DLQC7jMZigAI0{`07o*}{;8)sT+AUj@%*Z@Gt;e7`$U0l zQRwxmOb1akeHZ?~*&mKMo`=+^1HN(4yUkU#B}zP_NvT-NtOkq-6>Ah%R4m-TL%DY+nCg;@5QYkA7 z`yXSV{6UiFA`8XEfWJbqL+<<~EByxV_Jh>_#}`H>^I}j6&UE@M*W!;t{{1oj;I{OX z+p=biwsm}ge@W8bDk&-EO=5w?iQTw4GZ5`A_edgN9$a2oS*f(xxZT=tMh8fwu1j~^ zsOT&clX6N#Mt&^<27|0=E%6fM1~*y;u5jrwz|gKLrEXHA+Q8geFJ-5|s_E2#kV{+ZGAQ$1e1}ng;C_MCvQ22n1XdjE zBy9`frQfdk`CzAh3kwH{R1p#uzO~lBpZ6}`w991wTh1egPwT^NC$yUP?>}tr;HTu_ zdq&aak=`}w>dZNyHxjd4xYS;O9sjrJ_0#&^`NoX_f-QTWg#t|~B4+;v>PI{LlFn!;L4h+*U@niu>Th|85Yyo#Ah=nd-1fAXr?#_v9`L{6cg<*4T0) zhq2B>S1LuLTR}lI@V5d_*6_IKmtuc2G9sT!MJ!Z`l>#Tba*{>XFN4S@gXn6Y+DU*K zADQANsp2<7mX`EHVO1IbKEVArx zq1g5Cap*n8Z)MfSIq|zoLjmigADc%yV?KR*0P6xSi;9Zc)NfLdaB3?{8R2X0uQaB4 z*&$S_vM=JgXsTq7s=W-VVGHy^pC<2Z5Dbg|$k_1a6j7R>{QJl8Frq_N@}Lp{L+>k8zE?}>W{hD?AY(OZbh-G&~m;d})B% zf>J2axKwd(e#mFnsiXM-J9TS1OP zUgw=Amh7)0X-%NE>_pfGYzz;)DoUR$QD~f(B>mDT9Q(X+Y-925JAeL}8SjK=ga1OC z*qyk3MHL$f&{{3VdDle+qg?deLZ@G=y;hGZhQJZ$KhFZQx?5V>7Pgdn{VKLGqdAN0 zlhuea;81S+}||uHEnF~&i8TEPwlUIxUQF5uyC@@ZA6={bfaQS zFuDz(H#YafKKDpc$$afnyw1k<^8I@(_#z$9F+j{(UK!lK`_vtc8GC2HX{3`-`msBc zRK)4#?&fs11?|jSg#^>K>ouqLp0+U3G>Vs5S(o~&s;Zwqit1c)*G6v@J12qed_sDS zcxX*ABVNgEol!2_4Ykb#%m=#W&gP{R@x+b*^CuJXmQzzr^WCMz?LL)PNC|rK(7%;o zOKs1OE%h4x6>%x+YnPakhQ>uSpL2|plSYMmVP*?RSt^m*5bOsDoG$i7*AtT1nCvph zB~Iv67HZZ>9q90WJ0J@&Q7-gM4ciw3wI|NLet*>PydzGrJt+T~l7 zG$oGP;T1>;Fx)CF$y1IH6UW%t{(?kC>Y(Z0b9~4UNH4F>%F59)IlZC;ct6=5wNUeZ zn1wGA3 z)phHeQSI?&0wN+B7binL0XZlnG^kDqryv8}00V$f&CZ@XDRSiB42lub`3;OzKhnPv z>8y*_@u9aY&}uf=>r95tyuO*b-fbCacYrIG^np}bWQzz^(&h@)tU(LxlS9{}mqQ={rqZ-~Y(RgjB z0F;cG0uX$BzLvc8qSqX;kHCmaf(q&?u38S^3$!!iF{Ncn%-Z zutQVMX0K7wB8E!c%RD({5dnQ=q(Bo}v zJX4Ihv(G4U^EJ3{nOQd=?Z(gx-w*m>t_)Llg47!ll1<*TE`Iosd+Ic=nOdr_!StGG`pnDK*@gaACU(#@Te-e6p9JV^fli;Z zIC2LPWIQ48;BG+{Q$=s}*W2(V6L@SBXjyna+>Mbnx@rL4frFMdf{ z+f7(t&)nmwy0*6PM^!{)B~MdGC{9zaK=qZGG%Dosy2{i;xhSeuGwPAwNgA#O{-p0E zx1M$;?>68QQbv8OAYJI&MCih#7AE~IJ0fN`^BT=oc|bNqsYP%Mhqgja)R%ou5zmfJ z5~PC!v-HHk5sFTYY0Crmg@A%UCgyj_5!Jg~4KE;z2N+$5c;amveTm)ZA>=jinL$y4 zqu7Q-%2c)7(M}407KDZ>;`yuFIX-_gYMk*&i=Fr(^No|#R1cG?^>4+9m2o;jzy)c3 zkl=rjlPOB7c}{L2?SD>xf$oY2J`WOzq-+d@J8&8Rvbv_GVE3~R?2zici)o3e)gun4IU}!A zeeeyp`z$x-@*qiybcnp0^>l7*TwK1OM5(U^hexL6Ak(CE`P|#yx!~c#1a%QBLs+ zA?_AR>*ky2SWRDJ+!}6HWo2WNKc+?H$sZTEPi@P)n(f@$qdf>X88w9-*12$@yS-gu z-m@L4G9B6(^eTy5ReE-*-+wV)+zpQIMIwttZF8)bNgQn^0)Jg&q%k{2l9f1YgMDtU ziDm{Py5#PL?paq?v@}|Ol}n7@w?gs=;oErQsZQUk>nC1yc}U~pSE(tgApFE}af2z{ z;JfyI?V~0owZJhiO(Kcz{>sJD4E4p&>7S6Yq9jN8B$hOd25KB$4g@{$K1?+BE9JUs zR(?KW_{Z12RL+D?ZT&3)!}oR78X;E)Zhqz;q^kVafS060|00R5n}R_jMaAvDvTDXP z#>&S92(CvOY~w^hENWvFsR#JEuj8Xb!b*9&lhLN8SHi#aHBl>n zSc|J*-frxp2i5hax>7o%R>BSo#^~t?e_b`tlh5Tpm@F(TNZ9oil>aEC<|A=7tp z9`6_q{W8eAdI72(AGsREAd*$UlKbSoEsF)E2@)|4td5*iMX<`d3NWv&Kji1X&G`*B z02bSPExYx*(Dm2b*>;8un|a6IIBmdB9d%asndj7%Q)YksDG+Ss8jToU)2d&g#qruH zq%5~>?d=^ID((If0LH{uu{3Gr0KRLVhuZO#MdSV+Hs-KU9Fx(+soKpmzK)KLJms+X z0y?gSzkCd6pGrgf8f?bp{=mABPnlL5o42le9Pqh_z)ZAAQbC8qoSrF>s`Moar{)`JO?b95))zIXsze1sD=CLbP<;@ zHuh!vRLo8i!Sm`iRu`jWT>m}ne#$HbwtRc z{3oQe=pG*WuI>!hkNMk}m;};Xn0?x7Vj38j1ZYygv1*NSyUffiUv|!u^&6LPj@D^# zVbm;Nz8Cf`jVk;&z~NiTBU`mC-l0m{tHUiOOYaTd<>xn>sv$a%;>CyvxW(4`BZzmJanFpvE-}wz?f> z^%QB{(Fz% z&CLBr^p!SN=w)G@>m&Oc$9v)kRp3zk$bp~&Df?KC$@D%KYjzP&5|Krdl|_*A9@WY! zxuCfa1Z0EF&8^#e^65s!4-HLAvr2kNGMrf9r5UcIKY+OoQgwdr+G}%fg4s75)!en8YR&LG!IjNp=@gf;}+y3l{)8*$ddW5 z&bL^|ZJ{6o4z?_T(OYl0021kJFj{LgW7jGPEj2>{d+>McmtEp})>GcZUqpObNP90! zE<%`EwWoEb!A?xYySoZ{=jLzU(wn71D;U(4WUZP{+_*)&&i{}Gg&)AkPrQ5ha75~JT| zj2;KMFzjlHwUrWZ9~=b`QVk*!{a3s6&lywp3xF(jw2&Glk*oT#FmZK7NV~MxP^7}v z&!{E&DiH~Zx*qrySgZrgrKKovU>otq=k_p!yAk;Bb(lVJ%2 zW`|8po+R%)`|QN0=j}JCs8)bdGV3Xlmf992Sup>{r-Gh2GnVdpD$tzYiZqqqo_un{ zFN=rj@i8N4l)Ms+49myetl2{8GM6c{iI~{{R|-nkaJVU6qo=sAXU&~|;C9Kf?N%OJ z3nSy;Wx+j*5l*DS-TQwD0ORdPyl8Z@`V}w0O4FyK*1a7i@`1(}@8E@^O1$k-6Vg3!&fd_Tr$n|Id#g?F=UJ=%bd-TW? z*onJRva(kuIJG)Z;FR9zAHW${R+f)f3Gi`lG`2v|LV z{n!6K{AVBF;miF0&@t~=@iU~LAM=E$Q;umG!t^ZFSq$eIE zVEWQ=I?)o&i?{ipi>ZEi^$8K-!W?d*+I7LEa@X62@`y!6B~pxMOX8OwZ9@KZ!d0Dj zQGuf(e}ORtnQf5qZ(x4$*Tst%*WvMhAO52cUH}#JX}RxzeER?WZ6hZ1^$9h8Mg|_f zmCS`Zr;Kmz^J!hgYFP@t=znzYc7_o3gBSOGFj9O#Baa{yrt;UyH6s`KKwK~m=D8EI&Zp#hFg6>71xD? z{qdI{TnmP$WKum3)t+F#&d%q*kVYkCs>LGzTHS4-3D)paH{$h+A&ONa-&n?6 z7qA_Y(yqD!7BqPSed-*;7xJcn)f(enz8cGN7DaF-|E-smNC3)B6JC~iNpSl?1?uU3 zXrTOUB9;r;LWBX3kGz<98}rRO9n=#`OA-qt74h({DE=ioMgUK@S&=dskgj~ea{nUB zuXxmxq8-zJJE3~<_B9-@kZr@~QAVB>MrfMlt5F|fCh=lx_s1yFp62LApU&x}@tH z%f0vei*wHP{c*Ui?bgLyb3Sv<@r-egdz9XA>>a+V-#Xyj?Ttm_kC*E4GVvdA@g8lg zIM`tT7{sI|bw7L)nXlSa$r4hmH^~Uus08dT1oiJGTdbLCduA>yB^-8XppM1M*P`qD zWw;7`pwp?|;Z{KE`cgtW9ku4whiB-O`Iwx)*L$vq@OLRdz8@G7EJD)G?MZt%DCz6| z9WuMPUM0A9Zn7O3nb>fjiNB@B`;`%-NKc|nx0{$t&8O0^7IMbLaID^`OQ?_DEvS3t z=Jt>MkAK$MPoxxq#>Hng|GrId^f&Iw?>UQqoJ8cOagNaYYXT(D1_e8(|8e|H}o zmO;wehKhZB*2`i0jy|TvrJ|4$Xy+(F$t#d_PRY9B(pRZ4lUAA=kMa>ie}9|TFF@+) z;N@%5UjM<4ZpKAnO7eJwL|}UCP&M8HsVK{NBX2F@by0*;RY@a#?kBv9(sK3Ok`{{k zJbM#(EqPeZAXReL;5IH5^;#k#NX;rE4sEe%pz9l=k3hIAb(-}5uMe(LL5fnA|0`t8 zj);P%Y|%)^f63(jPjFkb=g$Yuh=oYDbvtsL4>rV`_thLzUN5MRwA2xU;b|=}6g7s$ zKtNA#>38inDmpiHDOHG*^+8YrLZqNPY__dvE-uNOcaRkI962-}Quy=Z>-Tj^`%HMhBDCOh)o& zyKA43HQ_21Fy1r63?h0Yn-IJ?`3Y#n4JeLcP%Db%HLFU**i?q5=u}imlGi^;I`&HTc2Mir(rYc)MAAbd z&`p){4mB6x7Fbjm~yIrsN5Icr@J-=1$l-o9G@C{p&@jeFoOz(OE}t z4}}2Z$3khUK|%MB2M`rYNt?xymsJ+~G`ddF%*?DdxPSL_dggw}XxE3*XV7AU9*<7w z-aXQ6oclPhCz8=U1~5s|B}KSCYPGEFLd?sxw!Ir>NuG1Ti zd8>HbbSOs!1#-lH2u}CtO;?9TDZornq|TH~$^YkJu#&lMQ(E5m7;mikNBv*=Rc%Fy z4TFCWC%_}nW;2uaT3sWxj8DXU&T1~hLG3M;T6#@1D4m%GYVt%_j}u&XR9sKK;=QrNi^4GJj*lx9}T_5~j?LFpH*SxIg8n5My5V|nW5Yl!{JU#28zq&*r{dT1eBd(vtWAM7W zZuGhA4x@9J&h++E2i&X|eEPx{8q+TX#}!b}4|VKDmuS*>Zq<6e>!{Am+}ZhYaRL1G ztvBx`iamXOna#dkil_I)_j#-F>^JY6Rm~j=$(I>*VWgz2Sx&!+VlkP~j^!}>p4sNJ z`$MNsTUK1FhCR# z!FVZqOms9gV!P`1X$XhBsCk48u%Z<;3yIdxXK<#@P zlIW)qqA*#1xtPJ#ujU?v8t8d|Z`+AsF{xcuzJ# zdcyHpfVrmlbW-a+Q*Dbi6bG4_;>J~MF2?lhumrgskdQ)KB5pZ>=A~>+D4;f-i0v){ zR(pT%{(|E|OGeSS@UihJF}R`Jq=N1MywFn@2#lv3kI54|LfUGIWC6USp zb1C`kEG#CXODDAz3-v~3=fc4gmqkH`^Ur6Fiyx2{SMc{5zIH&NOOf#=a_{=khXO&@ zC@Y1Va_N0(N?{*w)SfkD@a?u^L9s-0HI9JJal8A@F~!k-`}OPXV5B@>hC%4E>GxPB z3*SjpGT+|!zux95x7B!eVF|};g1nI#LGAR=BcdQ-tpS8^WkReT<(4IsFgJI6QMRd3 z+~8jQE)jRaL>(_;_eYPxh9cfkg43mw)TDu-X{)AEc9}G11n@+p>PP{Pb=`;HA zrT+`Nf*N*P;o0Pm8YP5+qMeKj?NeE>V>S?i-8+w(CQU6(oSZ36)6xOm+a3h#pZJ$? z5py7R^DF*O9~-tGRWpZWQABCQ!quO%+AcnMf`<8buWVzKJZ|BAbN^WSW_{@FL>17m zt=nE*fvC^3L2V`o`QM`%*xwb)_<1Hed zpLUIn!Z620kpWNkFoQ*BOB)6YJwuu5{XIS#`Z>lkYhP)z^4OynTB~(-sse}Xzc_sc z>vj6GYGJl7a96RS`{rN)hLAZ_Q0-@(_!k5&L#Jx4!9+Yl=vqiDAt<5l_K;I;^m@T) z*O)!k+^&<6t(e?a)dlhl@fD$)`(Gh#O(xqCUwQ7VwGHOIqI#P<^S!^+XgD5D(!2HJ zW7++3*>MAn-bWOyC~3I%Q!Wf@z1#uYkVswJOEPKItF(E%Q(^=v6YD;X=&VXX+rq); zP$#U1%~}7MF`@kMSpjLgg%az;wrI;SM5kOTv?NV zGL_)2I&on&V3}s(LxJnIK~n%kQNK)Hu@yeuTl1d{LiR;E3b(7}4ol44dD3CBAqFXr z#Dgzcrii!tQOg1fR!Gx()SDSN)jO)mE(5aR>A?(9{n<-4j(5`?<0KP1o5jQOdKHs~ zv*nNBJaRD%kGwu%d7gWa)4t+&@SR*J5JKH;^9>1DT3ShZB)}NC26H8u=d=3Zf6@^j z5b7sD4E3Qg;~i?_h$1#;o5K5 zxwAE%?Ca&y5hGwe3UfDz#rW7`qmtZ1k1G{vyE=Qdn&nbqIB_x$GE}-t3K7AxqDq=J z13M2XWCmS_YNq3L0h3M|uLJah+S|M5zR@u?Xv^HV7W<`yw{-5uoar8#|BeG+&0()d zFf`JLKqs-_g%==iHyts}QpS@?lOCPv#AX)V{-G3o0E0yTbi=#@PP0bG2q<+VwiCb^ z45!O%|F(#ai#=2-bJqtkr~TZrxY(LtcgRrP?egXPf;NlZf1aNO8)Bu#K@Xp;6uLEO zxdo8D#e`SfTrVm*k~aH}b7$uHRZ3=MI1l#Rqt>=!s~$0b|5sy4kqrk5YMoQwvj~wN zPPrAOG3CwK=H9xycj6*(qAzeX?aC_Jk`@g^U^_ZHbT+EPPv|7jnG8BN%JH{hRNaiOO$V=wk%b zt28B@9yTzeLpPDxJk{qD$r2KS`V3#&9;-R({^E}=wz7&jE-<&K<|p`wu)d#@kFezB zJ5!btS&#X?REHQ_1yLClQ zUvB)ghxaMXR`oh1G^W$jV_arD4Gtgj1f%JoG|v5-RoO3xB9K_gP~^{e!TRf4 z&xXXnpo)7U2O;P6D_h%!aDAMfjRezr{J3}X$%olBpW*9ZG)VIg(iZDtf&^!u+vdt3 zvtYK)Z4mbs6mdRI;+&oAjlZcxo%Ri|ucf&1Kf(T>|P@kl% zVY@$P^01oiZq?X8bOhD_Mm5kS7upjt@E9P-u%9z+*ylgkiZ9CnQBwjbf~n7-(n#v zx4EDCGY5`(qpp;H#=Iu7_iWzd_S))eh&u@Z`+}9hUavvnmFYUgt9HY2|Mm;gW3H`d zF=Ju#MN};d0^u+BzPIfz-@{s*Sp^Y?#e(Td86k4HB8IPKyjPHmk-e>H1p9I?6OpxDhz$+z+w>KebX{VwA4kd#2g7!HiyhnR;26DG^0A`S+UGPAwYoU?mL7xp#?nSrk#MR`FabLMz3av zy-8+pi}u`1wbNkTyH_?(_gh9w5Un66Gn#rooo#)w^gM-SNSuz^gxYVXW`mwX|F-g$ zh$FM_Iz?iWe1X}GqZwOP08?Y{9wLm^tGBtyv`xlK4-K zlO&P$#9SRoNu&D#_41rUpu_$7dh&>QY!hk{dgP1%YQ5xuVt99rkCDi9T$gZDvLXRY ze?e^HLr8%a-lfPBqRCEUr@r|I#a-Z12TPy7Fp>5*Ho7B@bQ||MwXxt$e+pc|0iy7f zC!QKK&CCwRmu`H#%M6eNQFWR9s%l}`RsPfG$|=}>`tMy*wrSsUB*{gMfpY&*Zc)(F}#A>eU>{g zJ?T98Lmld#$R)mXb@o{`ICLqG!^c%?_kQ10mfTs{b5(XH>NpQZE$0 zX){P>ct$0 z%bjvIFb{@&m)cy#T2cfIta9}B^6`c={>CbU3WH-gCQb3ac5VAkw}~>NhL1hGsCy2z ze5}i_pa{BQ17}9fxnnW-i1z@8-%oWRp{_T+QrftQu;q{RRQ5v5eS5O~n zRDzdWrvE#$p4oav8v$sWnbL7^vGK&8qGVb)GmAbS)@jSfb<6QORzyBr_iQqQHv9O4 z<6*^QgK8D3QY$4BGBR`e>%r4E)cP;3DVH1i_uvV;n6k|>s)%}iy#cd-VQO#G%<*3e z-f;%c1=@dL_gRjMt_-Fq=?Fg~5gEyQ2G)Bf>G-j*uiy-34hk5J>G*`Emrh+TcLT4{ z+P(ex+E(Ybk#Sr9giwO|XT`=m-=sCIqW)HGP1+wwlqYm!UtMtxU%$i4!Zo1rbbE{P zxl{Q0E7T|zQ3FfZJX;sSiXz zs&*u#8n;TFhkCC1WJ#);;`*?bWYI&SS7kIc2DHB5JKU6_$b6Tx%(cORLDJi}YV-5x z%<@g;EuQ$8XD+7b5hoW&s&1UvbI8MvuHACYBEvf8Ukj+2?6ru_W<(HO08D?q=l$iR zP6enW_+5u{W-ch+JE8NA3l~z%L9XWHjwjyoho{jQzJA$l$1gQx)VCUOaqhkBd-ct# zt!GqLRkrikPs57&g1KHk?4ut-k3t1|%FEP80BZCv4;$~>tPjK>`|Tojuy})gB)R!Z{%TP6EIhMzi%`Anpc{RV6Z8pRn}IZc#EsKUx) zPj8ChmOuD`(}zQb-=x!oZ1I4>bLjf23%4sH41am%Q^J+wdOy*@0Si-*n<%8Mi)pv1 zH7|ZV{V}Jy^Uc>pl(JOh=yGkWD!ZF|!#A!WNpvY(KFz^G*VeGkSwjlvtqJ!XEFrMH2(*83tO#5c{*@Wvxgy%~IS6}}aclV#_Gch~dgfZjMon2~UKqO6?R`Qcd_}wb{`jvFif$iPY zd-wv}IyePN6_)oQqlxO^DbL#+VA9^xJQRo=7Ou#rKifs z^)RaBf|{0g8C*sf*}&`W_*313zEk*)`7qKAq}8|P9sc+CFA6R1kE%J|(&$rt+Vu6b)NmyF zNFcU*_m{H?)7yf{ZjZ;aICyJ|8yN3SjsuLM#X^XByB!0Rikz-b_il?^cqz_Y{H*^%G1W%w{iL%$((8 zRWO(Id%UgQwm&)-R3c}c#sUkJ>gYN}g}&u} zAA3$~aS4BCpiGPTDo(qt(HYR91H^2$`w~wM zUc{r}5S@c>J@OofwSI6_Z70Mcs*ddP>(^x2V!6>GOLSJ$B^Ib&YiZUC05(Mi!7Zq{ zc$j~ivwo=A{k6eR>N98z3M{F_4;Uq8H1hRf!TE)yzqmLs=9oa6A-od=jkCL+iATaT z;yXC;zRva7Zk+ayPM}iue1Z17#P;Z+F-0D`V49GpnCFOLUe&kf#7$e~ulDt2z@N%8 zH5V5!ud7JlMk|&E2%3gV?JfLa9#vEi3}yPmXULXpVfBfAM&I%ng*cu3xkz4D3_t!Q zCIt?=U?ReO`+EuTHZVi=Z-)!o7q5#uY0Tn0G{(7ZIT5$hf%~7MRR4C$fa+;Y%EgGK zri(~h|7-h1EgnNS0YkvWge_eN^C}1|t6Nr_^8ut*=QoDODSWT!ddu?eklYK^-#<5i zrv8hsoZml}Zl-DQo9D?%U}ZoyP)6@9C5Sg-sq8dv@|(A@P+!3auT~9@j2K*+3MTFe zC)|(Wu|^v1M+!3fowLy1+p_=;#iwX2A1um@!3JI?!5)ObRZMb4ApBay-ALYgNgYi2 zQfVybV#sd#n~gqjPT(kgOC0sP8Bo76x_5FO9;HyIKdleiG0P9&s-%zS!rUK2pA`(4 zi8+@z1W0EP#0gq*KJ+laNYlO+#nUJ8vp<)@)CHJx)=QfDkJ zS{BdKci6C-SSRBfUYSdN$GhA3?Aa+c;7Aa;>@|4A*OXapm#+n<_ub(~7-7%V7?J10 z6p*48`-qXlAK4Ew720y9mdG8C1ZYhLiwTn@soGwdyoS_CprU+ny8fX6Ero4MyqFtN zA5&pzCLZo2%r?1x3}cMM3fA|X`};C5Na#FuqP10AQ| z4M;a^`L|6GP6ty8ZaPf9^%1+Xwm%OoKH%ARt5wFwKH}iH?cKP2B)66QQxUz%e{k-> z(CQ<4w(#OTD+#S+N#O)Z2@rQ+yL4{dvDGY7^a~1pM8!#FsZIFrJIHjV$B-z(LvywK zF}n68yNTyL+3V|ikAIQp1O?=B(5{t< z8mUqRbVf6!*y%$TMni1Cvl;<{?o_*X&h$>1gJNj4S)Y}fZsj=)K(~4$UkD`V)holy zFb)H@CcL{IT)Y2yPu)Rpd0`pD9m7ZhdsSnZ7vZTa+%h(@Z?{SZxC2yp$uTm^Ey~k^}+436*NL8nPM-eX{83-8a$?12m7(&2CsmhKbIT=G%7W2uw zLt(q^X{n&p9=A1lcyb4q|TE*nEPm>E;z@zHwFWto@A$i$__u7(cBc73nQ)1rTXo}e{tmj|x zY?MH+JZV_h`HC&(CA)!fJXUZ)TySwbPO{TKpe>!v&UD;0M!F*xc2Jv#4>7NwiU7P< z2AK>j=z)(A9c;bB6$uRS30Q~tUR+JZyi(H{FMxoS)%-*HM^kPT3P+XEJ(G5OjV9Z2 z37st-4sHh1sBc5l5CR!A+X_wb`o*YfMJDg!bAkP~at`W)%O2Icz4KQ1A7f+y6e ztp$Iz;VbzAJ>w<*)v7p>yXJG$ZduZ}lzCbAkOkQMXPdtJh*|RV%O{`6JT;LJS31_f z^`9{7ABX_sv;u_ld`&M&;pDp;7pq%wMzi_%(yIdM7ABW+xMKbz`0^@xkKZaWv&-PQ zW$AbNXC)T@P*)nyzKAzF{rxTU{Rn{I_k%rk7e?}tiJxCOiBSqcA+hetB6c{yL^mO9 zAz*UgYHjQLMRbqv6^uTA$GIC|G-iq%rQw1^(r5Zw`phnm*|pl`9!`C9YX>4Wh0u&# zT3bb2l^umRi_&ECKd#!a_KJ9d7-HF#kraHt?H$1k{s9m{F-!uD{B z_q#@td&0u*dFFG&nXeCr$E&yha(BJ7y%j&j#`~z-YYY)&lg=5%rD}jD`3FLLtq2RrYGHP&P@v zIZ`4y$J&wku&lU2jjTu_Z<{(SS&qR>^)s2Wvy1Ng=Am=~l%)hKsH-DEuEZK3A(;h3 zx6fXc1oPUd<7J%pDkeQU%+QjD347XmJ1AIr@#WyqmX^ozmD59%dA)|65cm*{i|mO3 zc3?Q>xP^@#uO%YNwu#MP?ukGQpJ(c-s~RSGW0T_EG!b+cYa#p3mJy~OB;6XYix-zs z`dYk0mE&1)&KSHT^_CZW2D(MRx)vSouGs~hjCNnHbf|^&IFY++5zcm`CCKo?xRS-jp?bHtUex8?HTCAAWi|@vnW@wxZ?le3N?#SGD zRd9K^Bmd{Wu`?=80u(IxaBqi=#NsskyB8ZJj;gC#wJ9W(GP5IEI-i&G01?25luJY& z;r$|wE4KT65+M>N`#72uCKWkfpbi9bb$&ieMg1?M=>cUETmS`$U%fOUyiP_W8uaBv z##ig-!RfJvFuG@U=`xrD3qE0C^uPJ$KjN4SqyvGLS7_=*H>Sf0HTF|+(!CVz#hpezhkCX~oiLpBhdARk)&sR*Rwu||52vPkA&LlFd}W#k-O z1mfS)F~o5uPrYdLX*+XJ6-~CI89Q&!+OnNq*3#Jv+R3c9QSY2|nO;@w zU1*%bQ{dI0X&vIWL-zb{gFkhPmd;*g%L;qVX5MBDiT5gCBo>N3jAIADHUnD7>obH( zkgRh&M1jewjV3J(!-5F&zw5Bzz3!Ghk$OSgqrRDuoq!}*)^M_7^Hodx+Az1nFJ_Zb z^W>?6EDyQWmYMjY1s!5s(t@yw&gCA%dTl&oBmeaBy z%1NyQpZWmn6K?E(^-+eG4arZ#CpPaVj1josmM2pZY*N{Jg#{n-QGJkfhUZ#%HLYT` z8yb7iEZG-GESt@4+(hOqQ#LZ<-Uh}-=xC~Qd(~5_4(9T-_-X#DZXQ?uphvBI*(|QN zIuU~Q`rW}f-L_W6cxLeAW;&0nliJItCh5JZPk-ze)c8#6r}CIp+?P3HP_K(!RHG3Y z$?4jD2o?E2T1w&~qFNv_%`^)n8aFfIAZ&C_3Ra%Hc`lb2!(d4o=ijNhaLh}Cf3AVegJADo`($|vz%NKl!a?ZS0fMg(++fTo9 z(k6#VVO&u;MosjHa>p4`E_`zpd}6;k4t_je$nxPPM7U=FU`{Chpy20o2d9K*t;)+i zQV-eKyc(|aT27ZC);vGdU%OuQ^=rC}s;v0hhi8F6Nu}6z_0Tn^;MU`-yg>bIyDlxQ zDDnEU(@!j&*|_$OAOAVNa~q+nM1t()l9*mfG0h$AY)y_@NUoel3tX7eX}FbLm$NL2 zGq@^>Pk+7EpRYz=|MsO{T$huJxE={_>wZJ!IrFl_vgmn!LqN;19jYoM0rnXIg=*n*g!L|mCKYb-mWGLesSPpDPu)E^iN z^fh<7zCcGbAitANr-hro-gwGla(OqhK-FjU5P}uE-6IfGh|E{)0>0ve;_+~Z0W>ex ztQe=ZuR7Kozy)FWX){LX(3i*dA`k_ab(s+u+k;0yPA$$%as#(@daH<-D_ZESBqLx| zDo8-Hr*!GP^zKaYgSBI+)Ib1lruzEG%b(y5Uf{d=o2DNaover(Z%I^%+R65 zf`=GXJmsYc5lxm+JUzsG0CFeo>@eeg3R8G0U(3TqjPazz8VV)JX^R=lKVgA)14^FF zDB5eih0i#^v2$B*O=(vNV?YA1ZB`hd6X?%}GcH$h@BpO*FaPSO zJ_22hkg7nJtvBd75joDPfwH&r*xkc@KK@<`>tG%Gro*|ZoYe3pN~8IClGIYKN)_o$Ms6 zw&;#K~p_BK?}#7S)dWFuU;bq#Kpeg~xl^ z`6vvFtjkw$w#`@OYEf54T;}Z-IV*1zaLppaIZbufR|($IAt*j@D?<54gB+EcC)IQy64OB9Q>??7G? za6&9PZ8!kk_ftvrR}x*JN73oD-E{<5gw%-2^tP7Y_4|~Fhc9u@j1#fR2NCq#WHvXM z8%MTFqOOG(&xLc|{%)?V^ zBC41f_Vsj^kH_>c$^cm)GjK{Fc+{2(IS<^?ZqfTZ_TP0YCNqKfP7J?*9W-Ts)PtbM z9B18pMq&lURV8rK^LXuaChDv)C+)WyZ0ycB_;xqwqB}d)AT;EM0dB=$r0xKQ9z+a2 zBDfW}H$<=(gdQ8_EQ@(S-rI8@yS!tqrnk#Xmx(34ragN2wsYMa2A=@#|0rMNkDk{~ zF`Q!lTvsvu^wTt+$Dmf*o4I9OL;~I7t`m$cL;y%}n(S;9Di4T)F+0`@zyLbb>`aS= z{gYdrP;mx{u`I#RLaQk=(?Tu+Sdz>pAK~*9U|oXA*98*pM?!z{#ul}{hkW$m z5E_vqsc3tZ;LDsn0qTgKX|*XZmW~R*;pXxL=U+p+x7LPp zp1rkWbobhra*;sjufqgqLsv0Y#*G2LTS*dH3nRKt>UIz13>Y7=4ZV!A=G|MHB-?!j zq{<0UzBki;4w$X5nl#9gua7jWU^3hHTLF{G&$Rh*bUiIQHy(hzu;FkDP4!`VsJvj} z%;?e2o+H7ubZQy~3J#u7H~_s#Ji*rjx-(_bK5Bky0uEUd?tPq~5hMwuJQnopVUr7@ zG-z7M$>9-Ea3fhsSg+PT*cf}#ebe|p%8HL*y`hkQ7vKe`5uiRjA-W2%sB>wWNfTHO z;-JZeG;pQdZy=B%ySl0>!E>Dor7T)Tj=<(-R1m;(C&r4ndcgxMNTd#x3zDBC^7KP_cY%ur;rP9>C_8X__;~VgbO#IBTpbd3NRimf&>ROma@46#5f zHnhlG&34xcR-ImLyAAP&h69V{1Y_HHt-+)(AyF3soiudzkn~lxIzi{KL>97O=d4(2 z^B7>!`DdWD{TeRJA3yqv51Ma*%FTNf!HNTJaNHfoeyKEflV!m3^2}Rqlbv0*IP|+ig z?&pc}0m1ktfL$LMVHwRCmTw7&y!%?VoWW-J!|(@D9=pPkug=)TJEIwT|Jk( zKIpV(`fGWQD8+W_YxX9LFWDV=BWf>N-^AOi-<{3lYTv;|<0d5wu1`GNTmVH>S(V$C zAwN>2!$%fa9h^E|xXWCkzpb@CV)~?{6ZHn$0Xa#j8DUQgKKuMyjgR|V8 z<9_XCx6&ub`jVzjdrAY-eV$X(%86S(xkLEGH#O$`05aX)+1~orFDLS5yY>|Q*>k?< z53qiY5&;klh$_&BR1Qhee~xI*G93F^hY%JB8*`~~6Wf)JAY6KIHYq7seE=LbOVu8& zv7(dg5(6_WhseD|we+W@JKCEPj4W~3b<6JTrKRA_30#^b(LMJHI=>iy#dVf@0|eD~ z&`;K1?Bzj`B7O92=(Nv3_k8FN{nSj8Sy<>Hbks-5UiRtgFV z1rwj<`gT-mS+;PR&G%a?rMRZ6RPSv_j)xr2enfZwD3^d>^yd|P{jCNz8$a4q7d{+b zwbOd`O>OLx89v9Ol;9u@B`r{oL zlOuN7yn^83O~oY$@5g2%YX1Sm;V#?AU_q%1w z8aCI{u)U#So1evv&Bi6#HNX8@?~iSxdBk5!5Yi^4x4)dhZ0>fujgB=Z5RU|gO&Vf>6Q|g74MbD@0WyPaRvQsE|c1(B^QYtKY1`Wey&jYWO zBByaBK>j#HMv)-xBEFZ6C=EB_kGpG3Sit!np3YmH3r3E!+W35qn=UmgIX?|cud!t? z-MQV;Z+O#CQiBNBKehk%kN7$^YTP$}VgMPRc(CoV*IeaHKJq(|wso1606MRK^I?Q< zlWFBHN#jW*mNE^UOsDX>$c*bi5G$qD(%*65#)Eo5>_2&NqMPZ6G`aA3MZ~r;{Bb)R z8MiDBIq#{Fw7-rFA`$DqWzj);79zt?I9`~DBiD0S`q(x0%f`kNDCC1j5~gMJy=&k- zz9rj#rE0Z7A5&D(O{f6#nm?vN-x9Z~Qlwu(=18 zh|tX&Y1AI2*F~1FMC7r8$^EHOi7oKB9E{dzz6_Ax_XXS7-WnXIyeVoSvHb2MiJzuNI3X zI+lkv^qzLA}nf zpN!gIhbG-yw*8A7jm6K;6?WTGa~u}?-_SdmU9YO@fQ$PEa$>;w$ohx&&^gRDGz;&$ z{l3k4{_CEVwdeUs*5EjHo0~o|8h-vtPX*D7%v{rza3=mvQfsS@UW{TRYs`rI6AgP( zy*$=T#BEUi;@j@9AS5EPNWrt~1-Ja_m2Yb+sht4;ney6Dqv7`Nng@^&_%M z;~0_dRyz$X@op1|zP=mEc=RfYlmcjx4D^$I9(rh9%?1s{-g;+Bg{E3D$MlNjh3&n& z56H>)uwBm!WqY}u&CB(dvhnQX-PCVmPP>zn3Fd}lF6oS?E-%N*FD|xm5n$q_ zprH8vPLTH_=dAJ1anT5RwGj~=9n85Gfq}pOlDjR(z4M(s_-q3`U9v|+L|36089gkn zMUxutzK8XH&C#eUWl#CP{6aRttu zpPVQ-{H$;nY2|>20Y8M;Z7%c7cQ1>NZ6_wH33@Ht=pPWE|KwRO#qn#=<~wW5k+Au{ z9-*&s+E_$<;ny@4+vm1s;pF7(xZx+-KDPAFI>}+;*yy)T<1NRZ*yYQa6c`T)&_7vh z5mL=L9eOLov|E+1TIS{CE+?XpYMBJ{%wu^p{B>(3(eiG-frdjwhw+u`@|>}$@!V*L zfT)xdS^m(FZtwa0t;Hq%lB=10hyQV{#;C=n;ql>~L&dW6&@8={8=gqL{0oofNA{)y z-1SoS9-mm)+S!)yzs<|iHCAG2(0p&*$K_X92ClwFOki=z6N}oDttT@skPX-qD-f|D z==#X9YP@}H`Ja2~BxpL!%s2UYIeYmzr~blrU_h2@n~n;{PsCw$m@}q5ki|4)?Hlse z)h1ch>7)h~1B3T|oz;`iFTA4EUBmp0XKN2NPlsebiobr@%5Lq%oe{rdgdE3`haM$J z<0mJ1%=Rp=AX}@W_)Yt-f5k>GPfrE8QA<_s_8!bR`58+#+fX)J?b{t_Q)jnjM4;oa z8^IZ!nOkPs^^_~nL|lk;xa^MV{sM31p)>yHn9seVz5I7{(3$G?ka7Bt7`c<)#jsiv zS?f#caJ_sfAj!Quh`v&~&-hF%386({^U&Ohi;b53)HdVXJC`x)?vps9etLS^(fy*$ zc8*(voG56wbKRVkkloZ+8!qO&&FzzHD#`z^TZBe-w^twc%+ztZZvmgnRk1sQYZKZ{WB%BKhGkfEOJiOuD@!7-<+eq za=T*6Wu=zbzWeigJ!+G=7KNFox;2*nwOTIcp12-JL$)|L^sZd$bzVg~U>zsWA&@k) zqugfDc&%>6AtkU+zD=*xpsN?ypoe}%C&=V;{ct?2o=L4Ey;WaT)ca(E)t%A2wmN)y z#7@_A|8I#(&8{hpcHTb6PP!PvezZ|BN;+k`&ra;LQ`e4iih>-iE+b~HfqJ4qMRG+0 z6MRMLT=py3oI%(fM+J#__>+(xMz1 zhF;;oyztgZ-~Hn)&OV)DrTXNL@f#A^CTrud|NR%_B_s32kyjCkk^Xf2{6Su&L-MMG zt45%4z_P-R*c;ieBO5!@#B#Kk1+VT(JUhHl>7mSvgE|Mdp>tivU$n6hTdgM&#Z2tesvS=b8Kwm!?W!cb=X=(RCo{n z>+2JxRWVYG#{^4hE%Kuy3!NRki$6S%(&~~wcdYq7!~9vPgNKEF{KW&aNVyyU!G&D$MU|Zgz)Po0 z0ZS8HGevBCpJU&L3crY9)hMencS&PW*WgJ%fsO@X)79GAR>Sb{@M~cOd(8aad^6pR ze{_+|aQ}Eu!PXUt=LJaDi^`>s+o53*9VGUG-I}cgy@LC=0=A1#=6cM^ci2o$89FO) z$hK-k!*?`bV%o*P=@J#6E3s91riIMfYH=Bpv6DrCx%9a$3GpmgK3~jbtz>0QcF#uT zKbmtVb&ef|J`f|-uRa&`{Ud%N2<_kKp<1pG+V@=Fi3qg!wwjfIyGFUcm%HE<`r)hr zX+9|ZKzn|E*UH;yR+hD(GhCt{C53eVW&^7-TfOLVS!a6h)|J~O;YNKQpQ>D!f6R+{ z%Yd$h-A_M;raHl1>KA_OdCg{Orp+AE=Gmnj5XgYQsepXRvyIodG-vr~5@o-?<5Yj? z{G4i1R$;c?wcahT`tpoZ)pGS_kj8Bf19L^En4(fv1o7W^L8`V@Tan)CS-me3LI!IzG^Sj6Dmo!F3sC(swFms7cV z=v+BJfJTrF>mR*XWc>EcIr;?*C2#XB?OSd}f9x3jvXo@Uq zCQ7WG5(e9Ypwcq?dF5i$dz{m5mtI}MLN?^A4P~@Q?DYNpkm6AEK1|toQ58W37#X6#@%dZ%g8H>3{i7rSK@K_oF&u#TWDr>G$wzuebJo$>!su_` z%+#Ic=w(A)5lTY>86j9YPq~N}6pm`T3f!v$l~}Fbn;}j^iG$eFI^RPd$0_}uLf)#m zV2!t%;QRBd`*6w6(;Me#N!s!*;=R0jK zsVULDna`AD#m7o3UNyI@@k#nqQddQt8;64G6>Dn=jHmSq{4G!ZnL2p1Ywykav3vdH z^?t>`|MB^n?Hj)HGpb)(t1Y%oZ=ybZP85H1_wgL>=#&H>sX_7avbVur<9vOQ%PY=Gwg*s` z1=HLm;%LUC`JlHtu5t?{pz3M{XN(YqOg?aS`;%s%Rs;4D-&GVXNJ>uWknS20k)@~( zi`i9+r$b=F!HJGw%j=5S*Rb0B)7L>$M>}L(yJM+}b;cz58A+~wsrba5U?VegX1hgn z^Oh1G(~apI+I;kHTR=d51Wegq>0PY7XA+HiIBmk1k$6m~%3td`W!u+eV|%oxQK8zD zQ&8VTT9UCp$h-BJpCJO~J>PSez3ZBcb#?JG)zsLumEB;34FOib)q!n`EXCyH*Lbd4 z=hTZ2l96P+B6aDxt-3D<8x86bw$4|FRUI$-d__{?-AA&*@$j#G%t&pn2x0jJEcQOC7Pw>wiqj&qqSs{+ate731ZaLgh|8lVv-S^TL6XZ>#okSm=wr zGec7|y~5#jS{fz>@?<95`9vSmxs-j$wq2(f-)nAyKBT3X>p8|_vFz(P)C@6=A}nRO z(JJmx7hFHb5lOGnQ}mqa`_h2}VjEml?qYm%cB)!5G1wc!38>sdso3+ON?Kt};*0m% z=tj>!o!eABTj1q}2mL}4BNQv3#c_W+jkxqmy%OdN2@SXMfGXv_80sr3N>AKYcSakX zazH_#K5%3xD4M{=>C&Z3b!iW;;oTJ%9~f79-BXKRR#g-ghz>4j*f8X7A8(a$mH=Qv zm@dS#ow7OO2c_wuSDmX`n;4NZYQ2Up(1jLo)hc2+ST>vPCPzWKFxO)rwKq>^UdZ11 zbV~UyWA!g$*ntS$y!&H2QIpT{(7V1EP#M?S&a*M=Er~TEj<7Hoz-cvuxG$ifrXd++vul2Irq%ZMXCGO)3O~=o zS+uCxq-HQNu__avrV%z=myYqY6=~}1Dzeb-9o=At!S3+>k&Z#_u0B5r3AE`&wzz{X z;&{pQhS<`95N_Fz1=)lFHVwcQIEP}n6+_qC{4~1;{XZz9}3*Xr5|Hc;*Ie! zgIW3SG&8SG7Lrcp-bDQ<%=+v%@4xQe+8XOr-ZrH)=s4fU^vPn~eYy`mX{AW&YYS4) z=!8aCy`f7vhQ>yEhk>{6<}SHA2KwZI^ZQ7VzLfxZval%fcd5LUb#XMWGtZI0Z|eNW z5xe}_RY~DwH{Hem)7V#lMYVlztEhm?r39Q&1SO@UR2Zo%jijIw(y5|I2!fOd5;DYq zbX}DY2?>#q6d4+%MG0Ymkx;tpTSvIR|NqP9JC8ibnRE8pXYaM&^{#iVa|QwW{^!0l zyc54oz(IRsgWYF$m|>N+v^BM>j*P0Nj66G$rMGqUuNV(oI;je?Hc1O!qXImq$& z5yX8RqLzLW`~10@*W>2tr4_~4DjSwZyQAXAsC2+pGyvS~ag7y`Efp1n`@R40=ttRj z?xy&(Y6=8|5PUw^_QX(~sK8B+S5-a}_U8i#CK zekMuy?D!l+BnFqyS>Emef+-%%yNh4DI3(9T)|dXs)+ya4@J+H(EDor?>-c^`R?$B$ zf0d32T(ow?xS4HqPL9nhEVn>F+#UUL>rv4BqGzo>2O6k$8?eobgADY+lZ?SDKX$UK zAdaG~YK~{3t#SPO@a4mFaeBSF9~?-6x_S23*a_%rQ`|udAw>B@x1eipZ+z>kZ0g5F~ti=cq2>c^0eWMj^apXVV@4Y+ainw{p?|;OoZBEO;!~AJ1!8AyW z7nl8K=dwXhya?g11%b}87|iDV>7#)wvybF+kj z*!rlj*BpZGztB`@E^aCU`UByl5R;Ia1{26JAs0>gO=A=J(H7ySiHJEXO0IVsc+J)7 zij%S{zHUxG%Qoz35%gNWBqe25={2$gim=^Lxu-QC%2M?{$$w&IUT6v}6Y>O(lKBhp z>yZ%-`3JQ=g=W#U9}t-{5BV6)bqP5dL_{LOLkNN7tOiXdGzWcyD;>_{hOyqfs#uni zvb%7)^6^e3ELVAX7w+IX(>0r;s9R_*X=oYs{f`d(*#;4bH!=UwjvLw#rrqC)Sl!C^ z;&5;WG}7;JTIW82oaZr}KrU*bc!{$9v{+;qT++0zZgOc^0Zmb_O#PUau0IJUtY^q1 z1e&B`D8d`*=4M{~RleNIWw+ViG2q@#YG|Z(tm#Wh&(9C=b87pRfDtXg%oO$6@(8w{ z!Tyw!x8wku7J@0~-*dzHWxn23W0=sbF<*~gr>Oe%25Z_*q5~HqSy}5?oEBrT>u$|m>Oi=&T8gX)H>zI6lMvy?M9 zKvKf&BV{2%-j6`XE`)Bkpx{2P3ZXdwR#Msn`Vgg^$STQ7xM zI{N39ggp(Ie4}2Z78b_GyJPM;SqNePxMb?md+I@NS4W5Dr|Q37kao4}e8?H_v>}A`ogTr? zUZ&B|?=o@*@vL-s{FtgEEdT-%0NiqzyHdyOp(XrL77_|4Og2!Yi#6v%Qx^7tENtgT>=%2*wL}2Zd%sek`TE6ivwb)xdf#Yg7X)zMeNoWvTB13*=@=77S-;H_a;(_G74_` z!qW9;IXRV;Y`|Sft}}!F7}zj|sJ|kz8XH^8$d7+y#0)W~Dk&haL2se=W3%h@pjAn3 z)#z~P_6ZvEoVsyw>Jj_c?_Tg$CiMh^Pgg)BF?zHc?_(be0#&6)Q~rQQz2_Dv1P+WR z811)&sn&Y7tCZwFkMBtXE}3Jd;hK{6glC%ZwWop>J^R!0&?q|zVDu7UCvF#KQk`hi?G zM7`C}t0x5Rukhay(TRwNSnCyQ<9bXQxEma`WRXX#k&|!ILWj>)k8B_=0 zU`o({jq#kohZul;*gW3YJ}3*WdDU?a31R$?ht<1H^~WGT{qYJ;sW=;__tgikL{| zcQKI2@d)(|RP#XW2yWPV+|nu{E?Ln?j=eSKK3K_qYUFj({AEjJ!n$6W z<6o4-ei84e^bRu7w3vVCNP5QW6(@Oq=LcSW4?7n2mi?E_)m%YU3!If zWS%d*67ZC1Z{8%>oEQoK+Ylh2d7G{_v^7;*<)r0jMu7tN`F`Sv9@RzXzo26l z`D)ie!9|)S4-5(yPO(6p&N?OHeTPM|W4-k5Zf8<4sx8tqY9L#z@Qdlbr7sqPk-8F6P{C zOq^RUJSRB&!)Mbgto;O#7Z+m%eM&?`{xl*Kb!z(OuW5^0=JRNEf%|F?f1QRW&{B`1 z*7|!Oqz`G$Q9CSCu_N&!O3jSp{reA2>+sZUmG_f*%e!}k<1lyby(@@dGlVm7N5DKp zcnm#Tg*`~$gL!x+NBgP<1DemvBSIRC;8`ph$#a!r^^w#< z`O3sFMiAw{gu7q}66w9Wcez-?M6V0c{*!Haj1nFo)uRIY#39MSI0leL>L3tf%(Z>z zl`JD88vLxiW#k{ff;_s z0ujvH*IB`YiJOL2bvhJB521PnzA4^P;ugWec#V96&}w$r{?x~=M^$bto4=&OAkHHk zub&?kKB}vFOCrf=5Cw`8B}h$S^>S`^=z0wdzJ0@}{~5Fy>;q9sLiPLSm*Q_lVEE~m z9e18_7rNX(|E_AoNn869OuLfKO_3wI94D=|{ZGio#CUpI+XKwL6#pO#lseog}nu@KjA5C zdv%2&OAQHU5XK#fK0FdSiD30s?TmgazuTrvW9u)X?eN;6{8;F?i!r2Ya+NP!byAf8 z)FKDDyeRs2vN5P6lfNg^ii^WmF@W0tZC6GyO-#LOcn}Yy33aU%FW*{Ri5TZMDGCK) z+OQuY+#%zF#PO`&e8<+uH*@thm26Iw_Uq47tx8_H_+hShIOg9EZaAOLeN8PB+sMWo z6b=%hA_ue8uZ<808gasE0(@NDe32bL01Ovss6xN~eeIgq>d@l3l+CUF0oN|>wXiJX zzoGky?GDP2hVAM6rZGUkk6=)+I1z}sXbZe23FTL-b^*!fNvPhOYo%(3e9Q{x2uNiJ z3!pzQzIk2R?Q=aq)<1<=2`PiiyUMYU#z*q;*)1q8Hea*f*>@lT1xmv8n?0R9U2@*L z2YjBSfChGK(Fp^wAxHqP^3h=BI1X5E__Hhh?#E@yM)WHeD=dI|JO;LV5>{3{td+xz2gqO* zX6S;%IN}-YJv#3iCspxu*J_QtSsql!;sUi}gt0>6>JgEKg`z042 z6N%y_Y#rz1!281EIl-WB*#jFc$u7#)E26HF2^#T}-Bvryr26QA;6jI9^Cl=ZKgxjN z&K1KV6K~D=WoSR?Ed(p9&%{lb+iN8fQOrfk_WgSde6f0+Ki3}D#+LvY`li=s$BP3I~ zZCCcueZ6M2N(Jz(=I*qZFdbM#W8;+%b&w*9U`{QKTD@_juP`(Jnbw;W{C-`;M?((! zx~MU6)VLG#f@5L-N$)d3fcr-mAD4qbM;oHpKQQp=!`ik=2~xrGx!Br>(fyZj@LxSe zB~YDQ4II|YJ;%7%=UKaeHQRH7dG@fDx}#%wPQHUG_zOyhQwVFgfW$Ze?B~U3Lw_7Q zO6FQ&Y@UyngB!kdFPpPwdRgT4E0nLS^D%TF?E_E|}llX{kA_xHE;aFo1c$pr}MAW=`sCHA5g$CBnkEx$iukuTyYD7A_ zlz&n^L({;2Vx@~vVfA@Z&||*7>msz&qWzG%|>= zdQb+4Rd!Z37gLz9_P^FawY=IjEc5r7A0VnFt+btYc~S4$(AM_Z0!2wcLm`U2TLlX^ zHw-_w#Bp8C2dsb&^Jh5GpMiG?zUvEy0uumd+t+VSt&w zbRA*<4CTK0-Fol*Q2TnUriKew!pVe5R&+IE*e$qcRxUdttdY=E4p!r^08Cd?P#qB_ zIfRd4$|kS-g(aw#UCfxydZjYQ`Fu#?ur^Kmvx!8`E;+ zZS7E^axOkX(@uj4VKy32N>IKybc`SYL;=ipJUMGo=ucJq~@*dsqoWX9X;w`>fbw09J@CBMR~vgB0m*%m2_}qfR+$L zzsg6m*8nWT=8`bcdkKOOWmWZ;sU_iH31E3hG6=~y{9WB{3Xz9D6xo-DvE|Ev z2Wk(aZWOg7N&N-+r!KyxuX;L~z#b7^lFHq*LZQTlf`XQ&_nB!WAlU;_SczO!S0_M^ zq=0mN?f%-Lw*o8#(kKy0QCAe)DA`9cZA?<#Jq3wTb)ADS|ZqLq` z^Me6l6ax6yF5D`#>={J*aJWP7#qz1Uv7&{CfU$vVhb3I}SHk+atHH;38rtg^sRQ(H zmEvKhExU(06city%;ahGhz)H!PbdQ%ePHM%)s+!Fy1Lmv_;_;z>by+LvvP(%w@m6l zSol?+Y(yxNqidMeI&~k7>`qM0!ndznwN_7gKAa+d#p}F&90t;E>1jEqZ3Vv9nj*yH zjSy><45coRSg5`Ow?)JgmyyLOL~H8)aCNC5GanPo`O@3;Nze2pA-(EZo8L2&z%J=w zj=H)#39i>l^O^d+UhW)(hqp8V5P#7H6Mjz5+T=}rI|@KU`E27~w!0`8~l>%ddBd-<+7AeJsS2MCjZr+@8%jfgo!V3593)>MC4@&-uz z83>($d=-d@BCuQil;{LyNoZb%sSGf_up>LR)-g>9~!9LMCZ7AV*N&r)LYf zqkmxN@cAu!X0K?dt0`z?%cr8L%E>31l#P4HTz?h`rrzAXa22ri{%Iqy7@=zqAG#pb z5L-JOACyc?0`(-@0w#Y>S>GR5gw+76BoODk-`Ts6{rfrf`(@b&9|&3_Jg7s7KU^#H z&YTRP4^gC4VuLY?djk|4P}?Xc>mxaS{?lGp2!2DA(vRuzjZObwQVKC1fF2?WmlyCZ zd+*=;go4gd9uN*hy5BEock1Zi!>nt}$Rl7$xN2RGZQqiz#zP6u@0NJIC zB`e8dr+m#G?Tc0p0z8dMH?1$38o+i?N7Gwd?G;qQ|j}DU^XDY_X%WS3^hjR4!1>0+CJ!XXjNdkP`Zg-CNRgDtx=U8G0YQK}5%EX3_{A zLqI@4TerWdzGzz(QxtV-9tol}&Y~%Pd>iPNMe^W?Z(DTA|J&W2`!y$E3p8v95!KtN z%dj&3CsG_Q-Y^QhGiNdGmNG}kN{>JYD%5|N>HIbs-mh+aA1a5 z?9N@g*PxV`R)^U-=}Eg*&ULr{Ev{y8^_Z78{f&#uOV4Ccmuxq# zgI9{{YR@S$dtLY3v1`)mo5y(HooPI_y22f^7?4q{zb>#;S0+CR9-2?mvymXqyKXPm_ z|L%QGu!Hha(L48T!DDG@8#YW8A$AEAR_msAga6d!tIv=9WThy4Sw@{g6e=qBYx~WT zU4*-sk+OlkBvxck8l;KDClkclWRf_WbsaV`Q(i+A)uXMcv%qh*eom?O>***-+VwMm zj}Uk?s04T@Q@Z2GRI#SZ;yJ3n5-XL2h_R4?U+mXBeZ}?`dGYST6-W4gp!=W)0`}!Gf&T%@0mCrS7O$~eePCLw zH#`cj0c-olMQ79pI1#Dbg}MiOPI6z=|7Es381}SM%a7}k5%$i@O9-{)Vh;Ur6<`rP zCtC3e2kB&dN{nwiDf~;%U@+gwV7lNsD1?l|Oajb{x?3NCN@I3TY#^ALtga>vB}5&N zS>`#~v*ikH5yRgq}C-(8Kxc2!#^ElcTTN*ofh8Gw6aUfs{6ibn3tg zsACLL)YmPgvlX!DfLgl+5|rhDel}5Gyz^X-zYW>dVq#p=^ZmqVkyuP#U$WDh5WI-K zrl;$>(wlcFa7BLu@Za03gOP$>JB4E;>&4Ej2O`m+m7C?%MM060juT(*ZGJ^}_Y;$M z?f}&|2(Xf?%pv>6`+%-EeT_r;3=Ud>X#tKB3wH~!385ux)E=nclOgLh*ootOnV@D^GFOK4LprA=WGQinN*)z2{l8FCV>iq5F>cDLKscYTyK^K(D}5^ z9B1~X>FvqfvXM(G-<&ShpLXI<@*WlXz1Aa#(~qVxbI|AiC}*KV1%#$QYQL1%g^EUg z&b1g5W@2Z=Ikzi+zcDw*L}u}d$N$!&iDDc_E8Zrk=masH za7ryNKS25?ZCOZa{&2@_VL1_u(hqXfWN2O7^=ldps~&f=Z0Ogd^OMEX(ylE$eWG>4 z4g316pkOuj_8loTP01@43|hjW%^OkUehZ5W=Xm0(vpP;szs$aH)tHv{ZT7hgiC(!k zKyu$UIm-I53T0SdO2+j6%*-fuocKO0d+zef*i$kNSfAO&3}lzYPPEpwYiDh?mf0@Z z4QbrEMIQN<_Z&Ze^`hj^l5U1Q|HcOSxge(F8wp}bgr}uNTRSwG^S@@d{Wi+(5}t!N z_LjGq@fSk`lUn_2v8OJaP#xT&e>6RFL3@%Y-)N%Cy#{E50smm|};lT*3zKa}CPhb}IK zrp5x35B*))f@~De;ngA>Fib5u7R?`yu3S8A-n(=MIfpCqWl@gr^~>tKN$HcOR8DuZ zR5H5WL(F(`V`Fo2X2!av+@Nz3H7`%!j{CBcE_b~>zU}FT0caIJr=HK>U7|td?N*MC0vlhee+zP{F*ATTT*q6^+b0I z;UGv%GyQ#^r>%;Dho*q=bDl_iM9p4GN`4m^oaj{kf{dJ+ylo_rnEgoIygzC&6I;qm zhridGSD5ticYjMe>m5SNf!da8RBbid5@lnJOXtfi!Qnk-m8L1MB=PtN%Hx6D5vihg z4#}8$dBQhINXrqd8WgMv_HEcP1#5q12@>-Ho2Z5I6GZd~Ecc6ZxNs#|({u1%B3Xb% z`wkZ~wWx)pkf^*dpQxO%{{B(Cz_x*tva>l#x&;O*Gl?KEoTbM+mBYn|S_aNL`lE#X7mk4lah^xT>s?9q zrsatT+k}U(%xFQICr-FW)Z~O_%nxQ_QmxNCJ#OL>M#idz8}OUV;zzMUp-n6tAlK85uB(Nvo?EiAu^H>%H|1C z-x@B$Mm%DTZWm>@gn>j3bUq|6&+!o*E6PmWLJ{X=s@ULg7;^F^(ZV=0lm&sdwz`4x zkNpuEn8nGQp5VX#`E}N@{0L%B!qq-LSoSe{QvxllfU|6lhcIYCGjzL1vQ2~d59Zi| zFd*!!$bi^j$*lvK1)*Oeye%j&DI3XV=Ce)&+5nggsJM+s5s$!aU}O?~ge$Qqxv@1$ z#GNb35J z^~rNk@yFh(f6o(1i+KlqMGVp}R?k4z$_mRuQ0|OF1sE7i260oUpF3~fl*;STZoz?J zzJLGM!-vkIH+!zPkBRn~sPHCDgnpbQYMv@)6B&Xl!`474Y~{#Ur|@hGJNYm^+4ZT` z4J)iw+O-|#;l-a2Y-V2`7`kF&B7;|!ZhlwkC-J*7Oy>T;^9EPM@?hik11wK!3s>gA zcKT5&#A1?3!kxOS zOtj~Wrwnasc(k+QXU4TuPf46S>GpV<8XSx~q2R!u5y8HM$Hd@!3&fNz|9auJRv9+W zM#RRc`Tvr5;|rpjzO)QBUZ;;>J9!tg`h;^Fc{)$>~io*ki+C&cgd7SCT$VJ0@b zGo%T{+>4Zdnwki<{cTm7e*fM;c~VG1u~TYs;=H2tl22h~c5Fl3tSi;xw5@*8gt5Ul zYIc#Eu74&c&=S88l$PlI+Z_#rqEGNv%d2Njn8pXmt`TA08!m{4lI_xr|J2GSP4F9K zlP4VeQ^*IVevLM0uF+QTb@qF|FJXOBS09+L`pi0Q7u4|^h{xbjDAe3Gd0*iM-)dx| z<(qSY#BY3pvX&bch>cO5%nn7s;k!q-N?GLcEPr4TMi0=J($|Eh>5G^NzODfp~@csuwla= zH$M|Ks<)JJ6y2F})q(%0HV>gJQ|GPt;xBL2pOf$n8mn_ezYk(F6Sru#{IraXS@?4G zKk+P*>@S3EuO8nT2|Gn;H0M(oce(T!{@(s7;r_gnZhiS0H&eegD}|9&Wx_N<7VHqZVz=S%wG#*%qGzG!&kU~Nxd zo3%1@pij`K%A%4x0&xU(<{^CH{$R9*B{@e^chYt3P%7rc$1T?;-=42n(8nMA?%RGPuAh> zOUJ0#P^;bP_x*T>sm@bRf1?ZW#8=xBMAkk35mQjTd)G0W$0tUX}!g36zG9 zAOnD(`NdA*0nef+?yPUa`yq3Zw7k4n#Q1NuJS}xbt>zYQ{{Xkrux|I078{9pR`S7GDaY^TdHq4D2 z8{PwMDHd)CUEt8{0%VqDZNB}^*NPMPdnfjB4W1;iYz|Fgo_wz}0n32cPbTv?uuCIO z$*NBN{8~og*TQ!}K&dwe-h!ORRL1BHI6I zW-7efT5)e^1qH}fIyQBR4#T&H;T&1w#>gn}M82Jq3ez&XcJSqyIhZ@H7d0Kdy&9mS zTNp{=$395>9_c+5Lzkr0^4vBWmM-f=#xyH?WMiJwsg7t-psqy(7d$wQDI})mK^(t3* zN=&_S=j4T)0Eq@p@`PLK-K5HvPF0zfI}c*(yLoOHx{oVx(5rx9o6h?90s!;!af!GS zE~KmFnwKbZq@0QtupUnuhBpxf62f|=WF?cwh^!xDp?StW!=@&Qs+#Cu z5_F9&d(At_LXkTx4r~O;&5xPgNj#Z|7G&8Of9EzfKkWF8XHoBcUUFG{zgq6wGf9p1 zKTJi*X^INmzKd@CrqRSVT2c%;ta|cSZP0@;-86LPDfV8UzBiapg}=wNZ$a{0M2huF zC5ZNvMdDEGjh4(W9}|yPWc2`BKmXn|as>Q(aMv25mjA3Q zgwgCRZ9t&QdtL>Kr=9mbB~oVh2XheCv=3D9oa!FN1Iqu)U$AKf9&E5a^_JSIyhTYV z?o3Jxo?#z#J^jHJ5q2=vCO809oBKeHO@om83T?P5M0hEiqq>tb&(B6=?b97+?42pg zrmL#T8C?(SA2+l&zjhct=+SP|i_2tSVxYYYNd+**x4ca}=Ye#1E-V~+?7n$>;L#H; zGmcL!n65d`6g4h*wI6v9cf|D@FODZ^$zY|ZuBgz)XmfB<}#0iS-VKx}yV>FJ>l7iSKUFBkPN zFfv6bb7dw=`BrQu{0!oT5c5rDa^uAPc3N1e7WHk4iB50~ttQ9UibpcZZb)zq50$D1 z7z8q%C|s?+5>bKFDGA@7vUIsHV|^y-6^uQ%dXgHklFTwI(x&~#gy2Bk-@J7hF(z%9 z87<>sr#=W-YFlAhC0F^SoxUf*B&@|O4mF31RZU*EZoeGFm{Q>jBRqb>qv$hJ?iE^yYBaRlrquD50JVA*LZ^w1ct*tVY8d5q8NX^AdP?W&d91S ztI1j`@G-2eJMq;khg7Qi-6?`D%d)kN?7m`=iE=rZlbxVKadCPR>IsV^hx(`3f^ zv1m;JPKL&IlD#`Oo5hv=K=*tb)?d7yYGZS=zc(-8;pJBQ8;s-PBv&8eks$eq`|UHM zj96o#%z~$2#Zb=k@X;!*z=9b35f>*T6 z-pAuU{@&vm1?Fu2X6W(bs+i8x!J?V362oWLyKlC0%y}wyLLS$R<@obp3Pwe?L`mDr z?@}QP^`Z9kLlwf0L39ndu@VR`F-qr6 z9O;^K%U*o(mfjhj`Bnk>sYVuV-FEBsB3-(ZXyy5$qTNtS*4l&qPeu;Njk(UjN!&~( z9%q~1mC}=##RgodKDx|8Uap`ycQ9nYo+uzr((`JK;;&qjbu{9TNr5;g_SPd2Yr?6C z_OpT%Z{D6GrSe2JYJeUh9~yIj4)0((Uw5Lwd|f-yDSK@xN3z5Y9?p$OVlgaE{?hc_ z&b)3R&F}a2#G}R=$D6NC&V+V6eDmzfioYVT@Phb*Z9W)cTrGsLeeIapiRKPEZw1|=i{oBgPX0h5w^Ts(ZZhJEM>3I<~{O0?lk?jzr>Bac0IX0 z8>7R+f9}k~H)$at3*xwJOsRzk+~n5&4f7y`MARa8&lfGonWW(~m?7OPH_eJXTjU?m zD|7Xys>vEZ^4Y^#u`1Q`*ntiejtJ1|UZv$E!S@7SiUSiea$h03OD`7{Lgb6&j^Cp} zA|P+8W;PwWg#3!#lNQGEfE_^R#TXbFMT~C%+fiH5zG)!auqX-68z?;n(OUjU3n+Y* z9}Fb3OP_9^ww_jXM3`4>58ngMr7s&w;MZnWhYE-4rlN|?t+Ok3B?gvzig3Ey+uQeS zzp7tOQj~X$2?$gUy;Zbf*E_U(x&;TPjt@AsAx87Pl$7{iXJ#9CiV^i6@wcTuca*Ip z>D-cvtX`$V4?t8cLXCX*l9P2?i0{G2pfA4Dy--)(Pl9&L$M1`o{kig=^9L)_8HCO^Oa1OtkvEPOUazN(w=kWZ0?2VMFxR8VodA6&NaN%(8PLxbQc72f=UgOH@ z8<5Mjz1;oD{?w%h+}+^(5V(SIJE-SP6!%%>uq>vn#fg{y@7J-?#`s(<<0*J+Nd*cHSzxRXEIS5b2 z#>eqj)08TwJC~P=)o)-bhKY>a%ciV^45{;H3{~qSFBlB8X!O&1Y RYVrVVDhe9%xw58D{|5@Q+uQ&E literal 72354 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%0D{DerFK)!xqQxnyl9dt! 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 z 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 V-$+(mInIElqNt&echfZBe*wGUR|Nn7 diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Sticky notes should allow to drag sticky note #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Sticky notes should allow to drag sticky note #0.png index 77404342dfd951194295febe09a00fdf7e8de25b..605d94e27b461e5d87bf601664a241e2acb75c14 100644 GIT binary patch literal 71356 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 7GMGL3#YL4T; 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 P0UXK->heW*&HVls^qopq diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Sticky notes should disable sticky note when scenario is not saved #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Sticky notes should disable sticky note when scenario is not saved #0.png index e3022e7c644e3404b5e5668ce228b101fd764dd3..df07fd6e2189ce7e402044c398f1296402785a26 100644 GIT binary patch literal 72065 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 literal 71149 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 diff --git a/designer/client/src/components/toolbars/activities/ActivityPanelRowItem/ActivityItemHeader.tsx b/designer/client/src/components/toolbars/activities/ActivityPanelRowItem/ActivityItemHeader.tsx index 8de06c5afbc..8976e6bd133 100644 --- a/designer/client/src/components/toolbars/activities/ActivityPanelRowItem/ActivityItemHeader.tsx +++ b/designer/client/src/components/toolbars/activities/ActivityPanelRowItem/ActivityItemHeader.tsx @@ -1,5 +1,5 @@ import React, { PropsWithChildren, useCallback, useMemo } from "react"; -import { Button, styled, Typography } from "@mui/material"; +import { Button, styled, Tooltip, Typography } from "@mui/material"; import { SearchHighlighter } from "../../creator/SearchHighlighter"; import HttpService from "../../../../http/HttpService"; import { ActionMetadata, ActivityAttachment, ActivityComment, ActivityType } from "../types"; @@ -20,6 +20,7 @@ import { ActivityItemCommentModify } from "./ActivityItemCommentModify"; import { getLoggedUser } from "../../../../reducers/selectors/settings"; import { getCapabilities } from "../../../../reducers/selectors/other"; import { EventTrackingSelector, getEventTrackingProps } from "../../../../containers/event-tracking"; +import CircleIcon from "@mui/icons-material/Circle"; const StyledHeaderIcon = styled(UrlIcon)(({ theme }) => ({ width: "16px", @@ -37,15 +38,18 @@ const StyledHeaderActionRoot = styled("div")(({ theme }) => ({ gap: theme.spacing(0.5), })); -const StyledActivityItemHeader = styled("div")<{ isHighlighted: boolean; isDeploymentActive: boolean; isActiveFound: boolean }>( - ({ theme, isHighlighted, isDeploymentActive, isActiveFound }) => ({ - display: "flex", - alignItems: "center", - padding: theme.spacing(0.5, 0.5, 0.5, 0.75), - borderRadius: theme.spacing(0.5), - ...getHeaderColors(theme, isHighlighted, isDeploymentActive, isActiveFound), - }), -); +const StyledActivityItemHeader = styled("div")<{ + isHighlighted: boolean; + isDeploymentActive: boolean; + isActiveFound: boolean; + isVersionSelected: boolean; +}>(({ theme, isHighlighted, isDeploymentActive, isActiveFound, isVersionSelected }) => ({ + display: "flex", + alignItems: "center", + padding: theme.spacing(0.5, 0.5, 0.5, 0.75), + borderRadius: theme.spacing(0.5), + ...getHeaderColors(theme, isHighlighted, isDeploymentActive, isActiveFound, isVersionSelected), +})); const HeaderActivity = ({ activityAction, @@ -233,30 +237,45 @@ const WithOpenVersion = ({ const ActivityItemHeader = ({ activity, isDeploymentActive, isFound, isActiveFound, searchQuery }: Props) => { const scenario = useSelector(getScenario); const { processVersionId } = scenario || {}; + const { t } = useTranslation(); const isHighlighted = ["SCENARIO_DEPLOYED", "SCENARIO_CANCELED"].includes(activity.type); const openVersionEnable = ["SCENARIO_MODIFIED", "SCENARIO_DEPLOYED"].includes(activity.type) && activity.scenarioVersionId !== processVersionId; + const isVersionSelected = ["SCENARIO_MODIFIED"].includes(activity.type) && activity.scenarioVersionId === processVersionId; const getHeaderTitle = useMemo(() => { const text = activity.overrideDisplayableName || activity.activities.displayableName; + const activeItemIndicatorText = isDeploymentActive + ? t("activityItem.currentlyDeployedVersionText", "Currently deployed version") + : isVersionSelected + ? t("activityItem.currentlySelectedVersionText", "Currently selected version") + : undefined; + const headerTitle = ( - ({ - color: theme.palette.text.primary, - overflow: "hidden", - textOverflow: "ellipsis", - textWrap: "noWrap", - padding: !openVersionEnable && theme.spacing(0, 1), - })} - > - {text} - + <> + ({ + color: theme.palette.text.primary, + overflow: "hidden", + textOverflow: "ellipsis", + textWrap: "noWrap", + padding: !openVersionEnable && theme.spacing(0, 1), + })} + > + {text} + + {activeItemIndicatorText && ( + + + + )} + ); if (openVersionEnable) { @@ -273,13 +292,21 @@ const ActivityItemHeader = ({ activity, isDeploymentActive, isFound, isActiveFou activity.overrideDisplayableName, activity.scenarioVersionId, activity.type, + isDeploymentActive, isFound, + isVersionSelected, openVersionEnable, searchQuery, + t, ]); return ( - + {getHeaderTitle} diff --git a/designer/client/src/components/toolbars/activities/helpers/activityItemColors.ts b/designer/client/src/components/toolbars/activities/helpers/activityItemColors.ts index 0afdb0b454c..9e03f525c46 100644 --- a/designer/client/src/components/toolbars/activities/helpers/activityItemColors.ts +++ b/designer/client/src/components/toolbars/activities/helpers/activityItemColors.ts @@ -4,6 +4,8 @@ import { getBorderColor } from "../../../../containers/theme/helpers"; const defaultBorder = (theme: Theme) => `0.5px solid ${getBorderColor(theme)}`; const activeBorder = (theme: Theme) => `0.5px solid ${blend(theme.palette.background.paper, theme.palette.primary.main, 0.4)}`; +const deployedBorder = (theme: Theme) => `0.5px solid ${theme.palette.primary.main}`; +const selectedVersionBorder = (theme: Theme) => `0.5px solid ${theme.palette.primary.main}`; const runningActiveFoundHeaderBackground = (theme: Theme) => blend(theme.palette.background.paper, theme.palette.primary.main, 0.3); const highlightedHeaderBackground = (theme: Theme) => blend(theme.palette.background.paper, theme.palette.primary.main, 0.05); @@ -11,8 +13,15 @@ const highlightedActiveFoundHeaderBackground = (theme: Theme) => blend(theme.pal const runningHeaderBackground = (theme: Theme) => blend(theme.palette.background.paper, theme.palette.primary.main, 0.2); const activeFoundItemBackground = (theme: Theme) => blend(theme.palette.background.paper, theme.palette.primary.main, 0.2); const foundItemBackground = (theme: Theme) => blend(theme.palette.background.paper, theme.palette.primary.main, 0.08); +const selectedVersionHeaderBackground = (theme: Theme) => blend(theme.palette.background.paper, theme.palette.primary.main, 0.2); -export const getHeaderColors = (theme: Theme, isHighlighted: boolean, isDeploymentActive: boolean, isActiveFound: boolean) => { +export const getHeaderColors = ( + theme: Theme, + isHighlighted: boolean, + isDeploymentActive: boolean, + isActiveFound: boolean, + isVersionSelected: boolean, +) => { if (isDeploymentActive && isActiveFound) { return { backgroundColor: runningActiveFoundHeaderBackground(theme), @@ -30,7 +39,7 @@ export const getHeaderColors = (theme: Theme, isHighlighted: boolean, isDeployme if (isDeploymentActive) { return { backgroundColor: runningHeaderBackground(theme), - border: defaultBorder(theme), + border: deployedBorder(theme), }; } @@ -41,6 +50,13 @@ export const getHeaderColors = (theme: Theme, isHighlighted: boolean, isDeployme }; } + if (isVersionSelected) { + return { + backgroundColor: selectedVersionHeaderBackground(theme), + border: selectedVersionBorder(theme), + }; + } + return { backgroundColor: undefined, border: "none",