Skip to content

Commit

Permalink
feat(simulator): different running modes
Browse files Browse the repository at this point in the history
the property `citrus.simulator.mode=[async/sync]` steers the mode
the simulator will use to execute simulations. note that while the
synchronous modus is simpler, it disallows the simulation of any
intermediate messaging with synchronous protocols.
  • Loading branch information
bbortt committed Feb 13, 2024
1 parent cadb91b commit cff7926
Show file tree
Hide file tree
Showing 16 changed files with 547 additions and 175 deletions.
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 the original author or authors.
* Copyright 2023-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 All @@ -19,31 +19,28 @@
import jakarta.annotation.Nullable;
import org.citrusframework.simulator.model.ScenarioParameter;
import org.citrusframework.simulator.scenario.SimulatorScenario;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;

import java.util.List;

/**
* Service capable of executing test executables. The service takes care on setting up the
* executable before execution. Service gets a list of normalized parameters which has to be
* translated to setters on the test executable instance before execution.
* Service capable of executing test executables. It takes care on setting up the executable before execution. The given
* list of normalized parameters has to be translated to setters on the test executable instance before execution.
* <p>
* Careful, this service is not to be confused with the {@link ScenarioExecutionService}. That is
* the "CRUD Service" for {@link org.citrusframework.simulator.model.ScenarioExecution} and has
* nothing to do with actual {@link SimulatorScenario} execution.
* Careful, this service is not to be confused with the {@link ScenarioExecutionService}. That is the "CRUD Service"
* for {@link org.citrusframework.simulator.model.ScenarioExecution} and has nothing to do with the
* actual {@link SimulatorScenario} execution.
*/
public interface ScenarioExecutorService extends DisposableBean, ApplicationListener<ContextClosedEvent> {
public interface ScenarioExecutorService {

/**
* Starts a new scenario instance using the collection of supplied parameters.
* Starts a new scenario instance using the collection of supplied parameters. The {@link SimulatorScenario} will
* be constructed based on the given {@code name}.
*
* @param name the name of the scenario to start
* @param scenarioParameters the list of parameters to pass to the scenario when starting
* @return the scenario execution id
*/
public Long run(String name, @Nullable List<ScenarioParameter> scenarioParameters);
Long run(String name, @Nullable List<ScenarioParameter> scenarioParameters);

/**
* Starts a new scenario instance using the collection of supplied parameters.
Expand All @@ -53,5 +50,5 @@ public interface ScenarioExecutorService extends DisposableBean, ApplicationList
* @param scenarioParameters the list of parameters to pass to the scenario when starting
* @return the scenario execution id
*/
public Long run(SimulatorScenario scenario, String name, @Nullable List<ScenarioParameter> scenarioParameters);
Long run(SimulatorScenario scenario, String name, @Nullable List<ScenarioParameter> scenarioParameters);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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.service.runner;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.citrusframework.Citrus;
import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
import org.citrusframework.simulator.model.ScenarioParameter;
import org.citrusframework.simulator.scenario.SimulatorScenario;
import org.citrusframework.simulator.service.ScenarioExecutionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.ExecutorService;

import static java.util.concurrent.Executors.newFixedThreadPool;

/**
* Asynchronous Legay
* {@inheritDoc}
*/
@Service
@ConditionalOnProperty(name = "citrus.simulator.mode", havingValue = "async")
public class AsyncScenarioExecutorService extends DefaultScenarioExecutorService implements ApplicationListener<ContextClosedEvent>, DisposableBean {

private static final Logger logger = LoggerFactory.getLogger(AsyncScenarioExecutorService.class);

private final ExecutorService executorService;

public AsyncScenarioExecutorService(ApplicationContext applicationContext, Citrus citrus, ScenarioExecutionService scenarioExecutionService, SimulatorConfigurationProperties properties) {
super(applicationContext, citrus, scenarioExecutionService);

this.executorService = newFixedThreadPool(
properties.getExecutorThreads(),
new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat("execution-svc-thread-%d")
.build()
);
}

@Override
public void destroy() throws Exception {
shutdownExecutor();
}

@Override
public void onApplicationEvent(ContextClosedEvent event) {
shutdownExecutor();
}

@Override
public void startScenario(Long executionId, String name, SimulatorScenario scenario, List<ScenarioParameter> scenarioParameters) {
startScenarioAsync(executionId, name, scenario, scenarioParameters);
}

private void startScenarioAsync(Long executionId, String name, SimulatorScenario scenario, List<ScenarioParameter> scenarioParameters) {
executorService.submit(() -> super.startScenario(executionId, name, scenario, scenarioParameters));
}

private void shutdownExecutor() {
logger.debug("Request to shutdown executor");

if (!executorService.isShutdown()) {
logger.trace("Shutting down executor");
executorService.shutdownNow();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 the original author or authors.
* 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.
Expand All @@ -14,18 +14,11 @@
* limitations under the License.
*/

package org.citrusframework.simulator.service.impl;
package org.citrusframework.simulator.service.runner;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import jakarta.annotation.Nullable;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.citrusframework.Citrus;
import org.citrusframework.annotations.CitrusAnnotations;
import org.citrusframework.context.TestContext;
import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
import org.citrusframework.simulator.model.ScenarioExecution;
import org.citrusframework.simulator.model.ScenarioParameter;
import org.citrusframework.simulator.scenario.ScenarioRunner;
Expand All @@ -34,90 +27,92 @@
import org.citrusframework.simulator.service.ScenarioExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Service;

import java.util.List;

import static org.citrusframework.annotations.CitrusAnnotations.injectAll;
import static org.citrusframework.simulator.model.ScenarioExecution.EXECUTION_ID;

/**
* Default implementation of the {@link ScenarioExecutorService} that executed all scenarios in a synchronous way. This
* has the advantage that all data is promised to be persisted and all messages to be handled at the end of the
* simulation. However, it limits the possibility to handle intermediate messages inside a single scenario, because it
* blocks the executor thread as long as it is running.
* <p>
* {@inheritDoc}
*/
@Service
public class DefaultScenarioExecutorServiceImpl implements ScenarioExecutorService {
@ConditionalOnProperty(name = "citrus.simulator.mode", havingValue = "sync", matchIfMissing = true)
public class DefaultScenarioExecutorService implements ScenarioExecutorService {

private static final Logger logger = LoggerFactory.getLogger( DefaultScenarioExecutorServiceImpl.class);
private static final Logger logger = LoggerFactory.getLogger(DefaultScenarioExecutorService.class);

private final ApplicationContext applicationContext;
private final Citrus citrus;
private final ScenarioExecutionService scenarioExecutionService;

private final ExecutorService executorService;

public DefaultScenarioExecutorServiceImpl(ApplicationContext applicationContext, Citrus citrus, ScenarioExecutionService scenarioExecutionService, SimulatorConfigurationProperties properties) {
public DefaultScenarioExecutorService(ApplicationContext applicationContext, Citrus citrus, ScenarioExecutionService scenarioExecutionService) {
this.applicationContext = applicationContext;
this.citrus = citrus;
this.scenarioExecutionService = scenarioExecutionService;

this.executorService = Executors.newFixedThreadPool(
properties.getExecutorThreads(),
new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat("execution-svc-thread-%d")
.build()
);
}

@Override
public void destroy() throws Exception {
shutdownExecutor();
}

@Override
public void onApplicationEvent(ContextClosedEvent event) {
shutdownExecutor();
}

/**
* Starts a new scenario instance using the collection of supplied parameters. The {@link SimulatorScenario} is
* expected to be an existing bean given the name {@code name}.
*/
@Override
public final Long run(String name, @Nullable List<ScenarioParameter> scenarioParameters) {
return run(applicationContext.getBean(name, SimulatorScenario.class), name, scenarioParameters);
}

@Override
public final Long run(SimulatorScenario scenario, String name, @Nullable List<ScenarioParameter> scenarioParameters) {
logger.info("Starting scenario : {}", name);

ScenarioExecution scenarioExecution = scenarioExecutionService.createAndSaveExecutionScenario(name, scenarioParameters);

prepareBeforeExecution(scenario);

startScenarioAsync(scenarioExecution.getExecutionId(), name, scenario, scenarioParameters);
startScenario(scenarioExecution.getExecutionId(), name, scenario, scenarioParameters);

return scenarioExecution.getExecutionId();
}

private Future<?> startScenarioAsync(Long executionId, String name, SimulatorScenario scenario, List<ScenarioParameter> scenarioParameters) {
return executorService.submit(() -> startScenarioSync(executionId, name, scenario, scenarioParameters));
}

private void startScenarioSync(Long executionId, String name, SimulatorScenario scenario, List<ScenarioParameter> scenarioParameters) {
protected void startScenario(Long executionId, String name, SimulatorScenario scenario, List<ScenarioParameter> scenarioParameters) {
logger.info("Starting scenario : {}", name);
try {
TestContext context = citrus.getCitrusContext().createTestContext();
var context = createTestContext();
createAndRunScenarioRunner(context, executionId, name, scenario, scenarioParameters);
logger.debug("Scenario completed: {}", name);
} catch (Exception e) {
logger.error("Scenario completed with error: {}!", name, e);
}
}

/**
* Prepare {@link SimulatorScenario} before execution. Subclasses can add custom preparation steps in here.
*
* @param scenario the scenario soon to be executed.
*/
protected void prepareBeforeExecution(SimulatorScenario scenario) {
}

private TestContext createTestContext() {
return citrus.getCitrusContext().createTestContext();
}

private void createAndRunScenarioRunner(TestContext context, Long executionId, String name, SimulatorScenario scenario, List<ScenarioParameter> scenarioParameters) {
ScenarioRunner runner = new ScenarioRunner(scenario.getScenarioEndpoint(), applicationContext, context);
var runner = new ScenarioRunner(scenario.getScenarioEndpoint(), applicationContext, context);
if (scenarioParameters != null) {
scenarioParameters.forEach(p -> runner.variable(p.getName(), p.getValue()));
}

runner.variable(ScenarioExecution.EXECUTION_ID, executionId);
runner.variable(EXECUTION_ID, executionId);
runner.name(String.format("Scenario(%s)", name));

CitrusAnnotations.injectAll(scenario, citrus, context);
injectAll(scenario, citrus, context);

try {
runner.start();
Expand All @@ -126,22 +121,4 @@ private void createAndRunScenarioRunner(TestContext context, Long executionId, S
runner.stop();
}
}

/**
* Prepare scenario instance before execution. Subclasses can add custom preparation steps in
* here.
*
* @param scenario
*/
protected void prepareBeforeExecution(SimulatorScenario scenario) {
}

private void shutdownExecutor() {
logger.debug("Request to shutdown executor");

if (!executorService.isShutdown()) {
logger.trace("Shutting down executor");
executorService.shutdownNow();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.service.runner;

public enum SimulatorMode {

ASYNC,
SYNC
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
"description": "Full qualified class name of additional Spring Java configuration to automatically load beans from.",
"defaultValue": "org.citrusframework.simulator.SimulatorConfig"
},
{
"name": "citrus.simulator.mode",
"type": "org.citrusframework.simulator.service.runner.SimulatorMode",
"description": "Execution mode of simulator.",
"defaultValue": "sync"
},
{
"name": "citrus.simulator.inbound.xml.dictionary.enabled",
"type": "java.lang.Boolean",
Expand Down
Loading

0 comments on commit cff7926

Please sign in to comment.