Skip to content

Commit 150d58c

Browse files
authored
[NU-2149] End scenario without sink (#8004)
1 parent fa98628 commit 150d58c

File tree

51 files changed

+813
-354
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+813
-354
lines changed

benchmarks/src/test/scala/pl/touk/nussknacker/engine/benchmarks/interpreter/InterpreterSetup.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ class InterpreterSetup[T: ClassTag] {
8383
ComponentDefinitionExtractionMode.FinalDefinition
8484
),
8585
ModelDefinitionBuilder.emptyExpressionConfig,
86-
ClassExtractionSettings.Default
86+
ClassExtractionSettings.Default,
87+
allowEndingScenarioWithoutSink = false,
8788
)
8889
val definitionsWithTypes = ModelDefinitionWithClasses(definitions)
8990

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package pl.touk.nussknacker.engine
2+
3+
import com.typesafe.config.Config
4+
import net.ceedubs.ficus.Ficus.toFicusConfig
5+
import net.ceedubs.ficus.readers.AnyValReaders._
6+
import net.ceedubs.ficus.readers.OptionReader._
7+
8+
final case class ModelConfig(
9+
allowEndingScenarioWithoutSink: Boolean,
10+
// TODO: we should parse this underlying config as ModelConfig class fields instead of passing raw config
11+
underlyingConfig: Config,
12+
)
13+
14+
object ModelConfig {
15+
16+
def parse(modelConfig: Config): ModelConfig = {
17+
ModelConfig(
18+
allowEndingScenarioWithoutSink = modelConfig.getOrElse[Boolean]("allowEndingScenarioWithoutSink", false),
19+
underlyingConfig = modelConfig,
20+
)
21+
}
22+
23+
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
package pl.touk.nussknacker.engine.api.process
22

33
import com.typesafe.config.Config
4+
import pl.touk.nussknacker.engine.ModelConfig
45
import pl.touk.nussknacker.engine.api.namespaces.NamingStrategy
56

67
// TODO: Rename to ModelDependencies + rename config to modelConfig
7-
final case class ProcessObjectDependencies private (config: Config, namingStrategy: NamingStrategy) extends Serializable
8+
final case class ProcessObjectDependencies private (modelConfig: ModelConfig, namingStrategy: NamingStrategy)
9+
extends Serializable {
10+
def config: Config = modelConfig.underlyingConfig
11+
}
812

913
object ProcessObjectDependencies {
1014

11-
def withConfig(modelConfig: Config): ProcessObjectDependencies = {
12-
ProcessObjectDependencies(modelConfig, NamingStrategy.fromConfig(modelConfig))
15+
def apply(underlyingConfig: Config, namingStrategy: NamingStrategy): ProcessObjectDependencies = {
16+
new ProcessObjectDependencies(ModelConfig.parse(underlyingConfig), namingStrategy)
17+
}
18+
19+
def withConfig(config: Config): ProcessObjectDependencies = {
20+
ProcessObjectDependencies(ModelConfig.parse(config), NamingStrategy.fromConfig(config))
1321
}
1422

1523
}

designer/server/src/main/scala/pl/touk/nussknacker/ui/additionalInfo/AdditionalInfoProviders.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ class AdditionalInfoProviders(typeToConfig: ProcessingTypeDataProvider[ModelData
2323
ScalaServiceLoader
2424
.load[AdditionalInfoProvider](pt.modelClassLoader)
2525
.headOption
26-
.map(_.nodeAdditionalInfo(pt.modelConfig))
26+
.map(_.nodeAdditionalInfo(pt.modelConfig.underlyingConfig))
2727
)
2828

2929
private val propertiesProviders: ProcessingTypeDataProvider[Option[MetaData => Future[Option[AdditionalInfo]]], _] =
3030
typeToConfig.mapValues(pt =>
3131
ScalaServiceLoader
3232
.load[AdditionalInfoProvider](pt.modelClassLoader)
3333
.headOption
34-
.map(_.propertiesAdditionalInfo(pt.modelConfig))
34+
.map(_.propertiesAdditionalInfo(pt.modelConfig.underlyingConfig))
3535
)
3636

3737
def prepareAdditionalInfoForNode(nodeData: NodeData, processingType: ProcessingType)(

designer/server/src/main/scala/pl/touk/nussknacker/ui/definition/DefinitionsService.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class DefinitionsService(
8484
}
8585

8686
import net.ceedubs.ficus.Ficus._
87-
val scenarioPropertiesDocsUrl = modelData.modelConfig.getAs[String]("scenarioPropertiesDocsUrl")
87+
val scenarioPropertiesDocsUrl = modelData.modelConfig.underlyingConfig.getAs[String]("scenarioPropertiesDocsUrl")
8888

8989
prepareUIDefinitions(
9090
withStaticDefinition,

designer/server/src/main/scala/pl/touk/nussknacker/ui/process/processingtype/ProcessingTypeData.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ object ProcessingTypeData {
7676
componentDefinitionExtractionMode: ComponentDefinitionExtractionMode
7777
) = {
7878
// TODO: consider using ParameterName for property names instead of String (for scenario and fragment properties)
79-
val scenarioProperties = deploymentData.deploymentScenarioPropertiesConfig ++ modelData.modelConfig
79+
val scenarioProperties = deploymentData.deploymentScenarioPropertiesConfig ++ modelData.modelConfig.underlyingConfig
8080
.getOrElse[Map[String, ScenarioPropertyConfig]](
8181
"scenarioPropertiesConfig",
8282
Map.empty
8383
)
84-
val fragmentProperties = modelData.modelConfig
84+
val fragmentProperties = modelData.modelConfig.underlyingConfig
8585
.getOrElse[Map[String, ScenarioPropertyConfig]]("fragmentPropertiesConfig", Map.empty)
8686

8787
val staticDefinitionForDynamicComponents =

designer/server/src/test/scala/pl/touk/nussknacker/test/utils/domain/ProcessTestData.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ object ProcessTestData {
207207

208208
// TODO: merge with this below
209209
val sampleScenario: CanonicalProcess = {
210-
def endWithMessage(idSuffix: String, message: String): SubsequentNode = {
210+
def endWithMessage(idSuffix: String, message: String): Option[SubsequentNode] = {
211211
GraphBuilder
212212
.buildVariable("message" + idSuffix, "output", "message" -> s"'$message'".spel)
213213
.emptySink(
@@ -496,7 +496,7 @@ object ProcessTestData {
496496
.to(endWithMessage)
497497
}
498498

499-
private def endWithMessage: SubsequentNode = {
499+
private def endWithMessage: Option[SubsequentNode] = {
500500
val idSuffix = "suffix"
501501
val endMessage = "#test #{#input} #test \n#{\"abc\".toString + {1,2,3}.toString + \"abc\"}\n#test\n#{\"ab{}c\"}"
502502

designer/server/src/test/scala/pl/touk/nussknacker/ui/config/ConfigurationTest.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class ConfigurationTest extends AnyFunSuite with WithTestDeploymentManagerClassL
2828
)
2929
}
3030

31-
private lazy val modelDataConfig = modelData.modelConfig
31+
private lazy val modelDataConfig = modelData.modelConfig.underlyingConfig
3232

3333
private def classLoader = {
3434
getClass.getClassLoader

designer/server/src/test/scala/pl/touk/nussknacker/ui/definition/DefinitionsServiceSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ class DefinitionsServiceSpec extends AnyFunSuite with Matchers with PatientScala
340340
val processingType = Streaming
341341

342342
val alignedComponentsDefinitionProvider = new AlignedComponentsDefinitionProvider(
343-
new BuiltInComponentsDefinitionsPreparer(ComponentsUiConfigParser.parse(model.modelConfig)),
343+
new BuiltInComponentsDefinitionsPreparer(ComponentsUiConfigParser.parse(model.modelConfig.underlyingConfig)),
344344
new FragmentComponentDefinitionExtractor(
345345
getClass.getClassLoader,
346346
model.modelDefinitionWithClasses.classDefinitions,

designer/server/src/test/scala/pl/touk/nussknacker/ui/definition/component/DefaultComponentServiceSpec.scala

+14-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import pl.touk.nussknacker.engine.api.component._
1414
import pl.touk.nussknacker.engine.api.component.Component.AllowedProcessingModes
1515
import pl.touk.nussknacker.engine.api.component.ComponentType._
1616
import pl.touk.nussknacker.engine.api.graph.ScenarioGraph
17-
import pl.touk.nussknacker.engine.api.process.{ProcessObjectDependencies, ProcessingType}
17+
import pl.touk.nussknacker.engine.api.process.{ProcessingType, ProcessObjectDependencies}
1818
import pl.touk.nussknacker.engine.definition.component.Components.ComponentDefinitionExtractionMode
1919
import pl.touk.nussknacker.engine.definition.component.defaultconfig.DefaultsComponentGroupName._
2020
import pl.touk.nussknacker.engine.definition.component.defaultconfig.DefaultsComponentIcon
@@ -26,23 +26,33 @@ import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap
2626
import pl.touk.nussknacker.restmodel.component.{ComponentLink, ComponentListElement, NodeUsageData}
2727
import pl.touk.nussknacker.restmodel.component.NodeUsageData.{FragmentUsageData, ScenarioUsageData}
2828
import pl.touk.nussknacker.security.Permission
29-
import pl.touk.nussknacker.test.config.ConfigWithScalaVersion
3029
import pl.touk.nussknacker.test.{EitherValuesDetailedMessage, PatientScalaFutures, ValidatedValuesDetailedMessage}
30+
import pl.touk.nussknacker.test.config.ConfigWithScalaVersion
3131
import pl.touk.nussknacker.test.mock.{MockDeploymentManager, MockFetchingProcessRepository}
3232
import pl.touk.nussknacker.test.utils.domain.{TestFactory, TestProcessingTypeDataProviderFactory}
3333
import pl.touk.nussknacker.test.utils.domain.TestProcessUtil.createFragmentEntity
3434
import pl.touk.nussknacker.ui.api.ScenarioStatusPresenter
3535
import pl.touk.nussknacker.ui.config.{ComponentLinkConfig, DesignerConfig}
3636
import pl.touk.nussknacker.ui.config.ComponentLinkConfig._
3737
import pl.touk.nussknacker.ui.definition.AlignedComponentsDefinitionProvider
38-
import pl.touk.nussknacker.ui.definition.component.ComponentListQueryOptions.{FetchAllWithUsages, FetchAllWithoutUsages, FetchNonFragmentsWithUsages, FetchNonFragmentsWithoutUsages}
38+
import pl.touk.nussknacker.ui.definition.component.ComponentListQueryOptions.{
39+
FetchAllWithoutUsages,
40+
FetchAllWithUsages,
41+
FetchNonFragmentsWithoutUsages,
42+
FetchNonFragmentsWithUsages
43+
}
3944
import pl.touk.nussknacker.ui.definition.component.ComponentModelData._
4045
import pl.touk.nussknacker.ui.definition.component.ComponentTestProcessData._
4146
import pl.touk.nussknacker.ui.definition.component.DynamicComponentProvider._
4247
import pl.touk.nussknacker.ui.process.DBProcessService
4348
import pl.touk.nussknacker.ui.process.deployment.scenariostatus.ScenarioStatusProvider
4449
import pl.touk.nussknacker.ui.process.fragment.DefaultFragmentRepository
45-
import pl.touk.nussknacker.ui.process.processingtype.{DeploymentData, ProcessingTypeData, ScenarioParametersService, ValueWithRestriction}
50+
import pl.touk.nussknacker.ui.process.processingtype.{
51+
DeploymentData,
52+
ProcessingTypeData,
53+
ScenarioParametersService,
54+
ValueWithRestriction
55+
}
4656
import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider
4757
import pl.touk.nussknacker.ui.process.repository.ScenarioWithDetailsEntity
4858
import pl.touk.nussknacker.ui.security.api._

docs/Changelog.md

+3
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@
165165
* [#7964](https://github.com/TouK/nussknacker/pull/7964) Add JsonTemplate language and editor.
166166
* [#8006](https://github.com/TouK/nussknacker/pull/8006) Add JsonTemplate editor to Event Generator source and Kafka sink value.
167167
* [#7970](https://github.com/TouK/nussknacker/pull/7970) Added "limits.maxActiveScenariosCount" setting defined per processing type and "globalLimits.maxActiveScenariosCount" to limit active scenarios globally
168+
* [#8004](https://github.com/TouK/nussknacker/pull/8004) Scenarios no longer have to end with final `Sink` node
169+
* set `modelConfig.allowEndingScenarioWithoutSink` of the scenarioType in the `scenarioTypes` config section to `true` in order to allow ending scenarios with nodes other than sinks
170+
* the flag is optional, the default value of the flag is `false` (no changes in behavior)
168171

169172
## 1.18
170173

engine/flink/components-api/src/main/scala/pl/touk/nussknacker/engine/flink/api/NkGlobalParameters.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package pl.touk.nussknacker.engine.flink.api
22

33
import _root_.java.util
4-
import com.typesafe.config.Config
54
import com.typesafe.scalalogging.LazyLogging
65
import io.circe.Encoder
76
import net.ceedubs.ficus.Ficus._
87
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
98
import org.apache.flink.api.common.ExecutionConfig.GlobalJobParameters
9+
import pl.touk.nussknacker.engine.ModelConfig
1010
import pl.touk.nussknacker.engine.api.ProcessVersion
1111
import pl.touk.nussknacker.engine.api.modelinfo.ModelInfo
1212
import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId}
@@ -50,10 +50,10 @@ object NkGlobalParameters extends LazyLogging {
5050
modelInfo: ModelInfo,
5151
deploymentId: String, // TODO: Pass here DeploymentId?
5252
processVersion: ProcessVersion,
53-
modelConfig: Config,
53+
modelConfig: ModelConfig,
5454
additionalInformation: Map[String, String]
5555
): NkGlobalParameters = {
56-
val configGlobalParameters = modelConfig.getAs[ConfigGlobalParameters]("globalParameters")
56+
val configGlobalParameters = modelConfig.underlyingConfig.getAs[ConfigGlobalParameters]("globalParameters")
5757
NkGlobalParameters(
5858
modelInfo,
5959
deploymentId,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package pl.touk.nussknacker.engine.flink
2+
3+
import com.typesafe.config.ConfigFactory
4+
import pl.touk.nussknacker.engine.api.component.ComponentDefinition
5+
import pl.touk.nussknacker.engine.api.process.SourceFactory
6+
import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess
7+
import pl.touk.nussknacker.engine.flink.test.FlinkSpec
8+
import pl.touk.nussknacker.engine.flink.test.ScalatestMiniClusterJobStatusCheckingOps.miniClusterWithServicesToOps
9+
import pl.touk.nussknacker.engine.flink.util.source.EmitWatermarkAfterEachElementCollectionSource
10+
import pl.touk.nussknacker.engine.flink.util.transformer.FlinkBaseComponentProvider
11+
import pl.touk.nussknacker.engine.flink.util.transformer.aggregate.AggregateWindowsConfig
12+
import pl.touk.nussknacker.engine.process.helpers.ConfigCreatorWithCollectingListener
13+
import pl.touk.nussknacker.engine.process.runner.FlinkScenarioUnitTestJob
14+
import pl.touk.nussknacker.engine.testing.LocalModelData
15+
import pl.touk.nussknacker.engine.testmode.{ResultsCollectingListener, ResultsCollectingListenerHolder}
16+
import pl.touk.nussknacker.engine.testmode.TestProcess.{NodeTransition, TestResults}
17+
import pl.touk.nussknacker.engine.util.config.DocsConfig
18+
import pl.touk.nussknacker.test.ProcessUtils.convertToAnyShouldWrapper
19+
20+
import java.time.{Duration, Instant}
21+
import scala.jdk.CollectionConverters._
22+
import scala.util.Try
23+
24+
trait FlinkMiniClusterTestRunner { _: FlinkSpec =>
25+
26+
protected def sourcesWithMockedData: Map[String, List[Int]]
27+
28+
protected def withCollectingTestResults(
29+
canonicalProcess: CanonicalProcess,
30+
assertions: TestResults[Any] => Unit,
31+
allowEndingScenarioWithoutSink: Boolean = false,
32+
): Unit = {
33+
ResultsCollectingListenerHolder.withListener { collectingListener =>
34+
val model = modelData(collectingListener, AggregateWindowsConfig.Default, allowEndingScenarioWithoutSink)
35+
flinkMiniCluster.withDetachedStreamExecutionEnvironment { env =>
36+
val executionResult = new FlinkScenarioUnitTestJob(model).run(canonicalProcess, env)
37+
flinkMiniCluster.waitForJobIsFinished(executionResult.getJobID)
38+
assertions(collectingListener.results)
39+
}
40+
}
41+
}
42+
43+
private def modelData(
44+
collectingListener: => ResultsCollectingListener[Any],
45+
aggregateWindowsConfig: AggregateWindowsConfig,
46+
allowEndingScenarioWithoutSink: Boolean,
47+
): LocalModelData = {
48+
def sourceComponent(data: List[Int]) = SourceFactory.noParamUnboundedStreamFactory[Int](
49+
EmitWatermarkAfterEachElementCollectionSource
50+
.create[Int](data, _ => Instant.now.toEpochMilli, Duration.ofHours(1))
51+
)
52+
val config =
53+
if (allowEndingScenarioWithoutSink) {
54+
ConfigFactory.parseString("""allowEndingScenarioWithoutSink: true""")
55+
} else {
56+
ConfigFactory.empty()
57+
}
58+
LocalModelData(
59+
config,
60+
sourcesWithMockedData.toList.map { case (name, data) =>
61+
ComponentDefinition(name, sourceComponent(data))
62+
} :::
63+
FlinkBaseUnboundedComponentProvider.create(
64+
DocsConfig.Default,
65+
aggregateWindowsConfig
66+
) ::: FlinkBaseComponentProvider.Components,
67+
configCreator = new ConfigCreatorWithCollectingListener(collectingListener),
68+
)
69+
}
70+
71+
protected def transitionVariables(
72+
testResults: TestResults[Any],
73+
fromNodeId: String,
74+
toNodeId: Option[String]
75+
): Set[Map[String, Any]] =
76+
testResults
77+
.nodeTransitionResults(NodeTransition(fromNodeId, toNodeId))
78+
.map(_.variables)
79+
.toSet[Map[String, Any]]
80+
.map(_.map { case (key, value) => (key, scalaMap(value)) })
81+
82+
private def scalaMap(value: Any): Any = {
83+
value match {
84+
case hashMap: java.util.HashMap[_, _] => hashMap.asScala.toMap
85+
case other => other
86+
}
87+
}
88+
89+
protected def assertNumberOfSamplesThatFinishedInNode(testResults: TestResults[Any], sinkId: String, expected: Int) =
90+
testResults.nodeTransitionResults.get(NodeTransition(sinkId, None)).map(_.length) shouldBe Some(expected)
91+
92+
protected def catchExceptionMessage(f: => Any): String = Try(f).failed.get.getMessage
93+
94+
}

0 commit comments

Comments
 (0)