Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simulator run modes: [async/sync] #241

Merged
merged 5 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion simulator-docs/src/main/asciidoc/concepts-advanced.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,90 @@
[[concepts-advanced]]
= Advanced Concepts

[[concept-advanced-execution-modes]]
== Execution Modes in Citrus Simulator

The Citrus Simulator offers different modes of operation to accommodate various testing scenarios and requirements.
These modes dictate how the simulator executes the test scenarios.
It comes with two modes, a synchronous and an asynchronous one, providing flexibility in how interactions are simulated and tested.

[[concept-advanced-execution-sync-mode]]
=== Synchronous Execution Mode

The synchronous execution mode ensures that scenarios are executed one after the other, in a single thread.
This mode is beneficial for scenarios where operations need to be performed in a strict sequence, and data consistency is crucial.

==== Configuration

To configure the simulator in synchronous mode, set the `citrus.simulator.mode` property in your application's configuration file (`application.properties` or `application.yml`) to `sync`.
If this property is not set at all, the simulator defaults to synchronous mode.

.Example `application.properties`
----
citrus.simulator.mode=sync
----

.Example `application.yml`
----
citrus:
simulator:
mode: sync
----

[[concept-advanced-execution-async-mode]]
=== Asynchronous Execution Mode

In asynchronous execution mode, scenarios are executed concurrently in separate threads, allowing for parallel processing.
This mode is suitable for more complex simulations where scenarios do not depend on the execution order or when simulating high concurrency.

==== Configuration

To enable asynchronous mode, set the `citrus.simulator.mode` property to `async`.
Additionally, you can configure the number of executor threads that handle the parallel execution of scenarios through the `citrus.simulator.executor.threads` property.

.Example `application.properties`
----
citrus.simulator.mode=async
citrus.simulator.executor.threads=10
----

.Example `application.yml`
----
citrus:
simulator:
mode: async
executor:
threads: 10
----

[[concept-advanced-execution-custom-mode]]
=== Custom Executors

For advanced scenarios, you have the flexibility to provide custom executors by implementing the `ScenarioExecutorService` interface.
This allows for tailored execution strategies, such as custom thread management, prioritization of scenarios, or integration with external systems for scenario execution.

To use a custom executor, define your implementation of the `ScenarioExecutorService` and register it as a bean in your Spring application context.
Ensure that your custom executor is appropriately configured to be recognized by the simulator in place of the default synchronous or asynchronous executors.
To disable the default synchronous executor, set the following property: `citrus.simulator.mode=custom`.

.Example Custom Executor Bean Definition
[source,java]
----
@Bean
public ScenarioExecutorService customScenarioExecutorService() {
return new MyCustomScenarioExecutorService();
}
----

This custom executor will then be used by the simulator to execute scenarios according to the logic you've implemented.

== Best Practices

- Use the _synchronous mode_ as the standard, for linear simulations where data consistency matters or when debugging to ensure straightforward tracing of actions and outcomes.
- Opt for the _asynchronous mode_ only when explicitly needed, when simulating more complex scenarios that involve intermediate synchronous messages.

By understanding and appropriately configuring the execution modes of the Citrus Simulator, you can tailor the simulation environment to best suit your testing needs, whether you require precise control over scenario execution or need to simulate high-volume, concurrent interactions.

[[concept-advanced-database-schema]]
== Database Schema

Expand All @@ -10,7 +94,7 @@ This visual representation can help understand the relationships and structure o

image::database-schema.png[Database Schema, title="Database Schema of the Citrus Simulator"]

[[concept-runtime-scenario-registration]]
[[concept-advanced-runtime-scenario-registration]]
== Registering Simulator Scenarios at Runtime

Registering simulator scenarios at runtime is a perfectly valid approach.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ citrus.simulator.default-scenario=Default

# Should Citrus validate incoming messages on syntax and semantics
citrus.simulator.template-validation=true

# Use async mode for intermediate messaging
citrus.simulator.mode=async
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ citrus.simulator.template-validation=true
# JMS destinations
citrus.simulator.jms.inbound-destination=Fax.Inbound
citrus.simulator.jms.status-destination=Fax.Status

# Use async mode for intermediate messaging
citrus.simulator.mode=async
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ citrus.simulator.default-scenario=Default

# Should Citrus validate incoming messages on syntax and semantics
citrus.simulator.template-validation=true

# Use async mode for intermediate messaging
citrus.simulator.mode=async
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ citrus.simulator.default-scenario=Default

# Should Citrus validate incoming messages on syntax and semantics
citrus.simulator.template-validation=true

# Use async mode for intermediate messaging
citrus.simulator.mode=async
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ citrus.simulator.default-scenario=Default

# Should Citrus validate incoming messages on syntax and semantics
citrus.simulator.template-validation=true

# Use async mode for intermediate messaging
citrus.simulator.mode=async
2 changes: 1 addition & 1 deletion simulator-spring-boot/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<version>2.2.0</version>
<version>2.3.0</version>
</dependency>

<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2006-2017 the original author or authors.
* Copyright 2006-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -30,13 +30,17 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static org.citrusframework.TestResult.failed;
import static org.citrusframework.TestResult.success;
import static org.citrusframework.util.StringUtils.hasText;
import static org.springframework.util.StringUtils.arrayToCommaDelimitedString;

/**
* @author Christoph Deppisch
*/
Expand All @@ -50,7 +54,7 @@ public class SimulatorStatusListener extends AbstractTestListener implements Tes

/**
* Currently running test.
*
* <p>
* TODO: Replace with metric.
*/
private Map<String, TestResult> runningTests = new ConcurrentHashMap<>();
Expand All @@ -68,25 +72,25 @@ public SimulatorStatusListener(ScenarioActionService scenarioActionService, Scen

@Override
public void onTestStart(TestCase test) {
if (test instanceof DefaultTestCase) {
runningTests.put(StringUtils.arrayToCommaDelimitedString(getParameters(test)), TestResult.success(test.getName(), test.getTestClass().getSimpleName(), ((DefaultTestCase)test).getParameters()));
if (test instanceof DefaultTestCase defaultTestCase) {
runningTests.put(arrayToCommaDelimitedString(getParameters(test)), success(test.getName(), test.getTestClass().getSimpleName(), defaultTestCase.getParameters()));
} else {
runningTests.put(StringUtils.arrayToCommaDelimitedString(getParameters(test)), TestResult.success(test.getName(), test.getTestClass().getSimpleName()));
runningTests.put(arrayToCommaDelimitedString(getParameters(test)), success(test.getName(), test.getTestClass().getSimpleName()));
}
}

@Override
public void onTestFinish(TestCase test) {
runningTests.remove(StringUtils.arrayToCommaDelimitedString(getParameters(test)));
runningTests.remove(arrayToCommaDelimitedString(getParameters(test)));
}

@Override
public void onTestSuccess(TestCase test) {
TestResult result;
if (test instanceof DefaultTestCase) {
result = TestResult.success(test.getName(), test.getTestClass().getSimpleName(), ((DefaultTestCase)test).getParameters());
if (test instanceof DefaultTestCase defaultTestCase) {
result = success(test.getName(), test.getTestClass().getSimpleName(), defaultTestCase.getParameters());
} else {
result = TestResult.success(test.getName(), test.getTestClass().getSimpleName());
result = success(test.getName(), test.getTestClass().getSimpleName());
}

testResultService.transformAndSave(result);
Expand All @@ -98,10 +102,10 @@ public void onTestSuccess(TestCase test) {
@Override
public void onTestFailure(TestCase test, Throwable cause) {
TestResult result;
if (test instanceof DefaultTestCase) {
result = TestResult.failed(test.getName(), test.getTestClass().getSimpleName(), cause, ((DefaultTestCase)test).getParameters());
if (test instanceof DefaultTestCase defaultTestCase) {
result = failed(test.getName(), test.getTestClass().getSimpleName(), cause, defaultTestCase.getParameters());
} else {
result = TestResult.failed(test.getName(), test.getTestClass().getSimpleName(), cause);
result = failed(test.getName(), test.getTestClass().getSimpleName(), cause);
}

testResultService.transformAndSave(result);
Expand All @@ -116,9 +120,9 @@ public void onTestActionStart(TestCase testCase, TestAction testAction) {
if (!ignoreTestAction(testAction)) {
if (logger.isDebugEnabled()) {
logger.debug(testCase.getName() + "(" +
StringUtils.arrayToCommaDelimitedString(getParameters(testCase)) + ") - " +
testAction.getName() + ": " +
(testAction instanceof Described && StringUtils.hasText(((Described) testAction).getDescription()) ? ((Described) testAction).getDescription() : ""));
arrayToCommaDelimitedString(getParameters(testCase)) + ") - " +
testAction.getName() +
(testAction instanceof Described described && hasText(described.getDescription()) ? ": " + described.getDescription() : ""));
}

scenarioActionService.createForScenarioExecutionAndSave(testCase, testAction);
Expand All @@ -140,8 +144,8 @@ public void onTestActionSkipped(TestCase testCase, TestAction testAction) {
private String[] getParameters(TestCase test) {
List<String> parameterStrings = new ArrayList<>();

if (test instanceof DefaultTestCase) {
for (Map.Entry<String, Object> param : ((DefaultTestCase) test).getParameters().entrySet()) {
if (test instanceof DefaultTestCase defaultTestCase) {
for (Map.Entry<String, Object> param : defaultTestCase.getParameters().entrySet()) {
parameterStrings.add(param.getKey() + "=" + param.getValue());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public class Message extends AbstractAuditingEntity<Message, Long> implements Se
@OneToMany(fetch = FetchType.LAZY, mappedBy = "message", cascade = CascadeType.ALL, orphanRemoval = true)
private final Set<MessageHeader> headers = new HashSet<>();

@ToString.Exclude
@ManyToOne(fetch = FetchType.LAZY)
@JsonIgnoreProperties(value = {"scenarioParameters", "scenarioActions", "scenarioMessages"}, allowSetters = true)
private ScenarioExecution scenarioExecution;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class MessageHeader extends AbstractAuditingEntity<MessageHeader, Long> i
private String value;

@NotNull
@ToString.Exclude
@ManyToOne(optional = false)
@JoinColumn(nullable = false)
@JsonIgnoreProperties(value = {"headers", "scenarioExecution"}, allowSetters = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class ScenarioAction implements Serializable {
private Instant endDate;

@ManyToOne
@ToString.Exclude
@JsonIgnoreProperties(value = { "scenarioParameters", "scenarioActions", "scenarioMessages" }, allowSetters = true)
private ScenarioExecution scenarioExecution;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public class ScenarioParameter extends AbstractAuditingEntity<ScenarioParameter,
private String value;

@ManyToOne
@ToString.Exclude
@JsonIgnoreProperties(value = {"scenarioParameters", "scenarioActions", "scenarioMessages"}, allowSetters = true)
private ScenarioExecution scenarioExecution;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public class TestParameter extends AbstractAuditingEntity<TestParameter, TestPar

@NotNull
@ManyToOne
@ToString.Exclude
@MapsId("testResultId")
@JoinColumn(name = "test_result_id", nullable = false)
@JsonIgnoreProperties(value = { "testParameters" }, allowSetters = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.citrusframework.simulator.repository;

import org.citrusframework.simulator.model.MessageHeader;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.citrusframework.simulator.repository;

import org.citrusframework.simulator.model.Message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.citrusframework.simulator.repository;

import org.citrusframework.simulator.model.ScenarioAction;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.citrusframework.simulator.repository;

import org.citrusframework.simulator.model.ScenarioExecution;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.citrusframework.simulator.repository;

import org.citrusframework.simulator.model.ScenarioParameter;
Expand Down
Loading
Loading