Skip to content

Commit

Permalink
fix(simulator): scenario execution performance
Browse files Browse the repository at this point in the history
improved performance of `/api/scenario-executions` endpoint.
instead of fetching all relationships and then trimming the result,
relationships are now only conditionally being fetched.
  • Loading branch information
bbortt committed Jan 20, 2025
1 parent ce8911e commit bcb593c
Show file tree
Hide file tree
Showing 8 changed files with 342 additions and 201 deletions.
16 changes: 15 additions & 1 deletion simulator-docs/src/main/asciidoc/rest-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,21 @@ The endpoint `/api/test-results` additionally supports the `DELETE` request that
=== Receive SINGLE Test-Parameter

A `TestParameter` is uniquely identified by a composite key, consisting of the `TestResult` ID and the `TestParameter` key.
To retrieve a single `TestParameter`, use the `GET /{testResultId}/{key}` endpoint.
To retrieve a single `TestParameter`, use the `GET /api/test-parameters/{testResultId}/{key}` endpoint. all recorded Test Results and Executions.

[[receive-scenario-execution-details]]
=== Receive Scenario Execution with Details

The `ScenarioExecution` is also unique in regard to the amount of details that _could_ be extracted from it.
However, more information (almost) always comes at the cost of performance.
Thus, the `/api/scenario-executions` endpoint offers four unique boolean query parameters:

* `includeActions`: When `true`, additionally fetches related `ScenarioAction`
* `includeMessages`: When `true`, additionally fetches related `Message` (without `MessageHeader`)
* `includeMessageHeaders`: When `true`, additionally fetches related `Message` and `MessageHeaders`
* `includeParameters`: When `true`, additionally fetches related `ScenarioParameter`

They are all being set to `false` by default.

[[scenario-resource]]
=== Scenario Resource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,4 @@ public interface ScenarioExecutionRepository extends JpaRepository<ScenarioExecu

@EntityGraph(attributePaths = {"testResult", "scenarioParameters", "scenarioActions", "scenarioMessages", "scenarioMessages.headers"})
Optional<ScenarioExecution> findOneByExecutionId(@Param("executionId") Long executionId);

@Query("FROM ScenarioExecution WHERE executionId IN :scenarioExecutionIds")
@EntityGraph(attributePaths = {"testResult", "scenarioParameters", "scenarioActions", "scenarioMessages", "scenarioMessages.headers"})
Page<ScenarioExecution> findAllWhereExecutionIdIn(@Param("scenarioExecutionIds") List<Long> scenarioExecutionIds, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.citrusframework.simulator.service;

import com.google.common.annotations.VisibleForTesting;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.Path;
Expand Down Expand Up @@ -59,8 +60,8 @@
import static org.citrusframework.simulator.service.CriteriaQueryUtils.newSelectIdBySpecificationQuery;
import static org.citrusframework.simulator.service.ScenarioExecutionQueryService.MessageHeaderFilter.fromFilterPattern;
import static org.citrusframework.simulator.service.ScenarioExecutionQueryService.Operator.parseOperator;
import static org.citrusframework.simulator.service.ScenarioExecutionQueryService.ResultDetailsConfiguration.withAllDetails;
import static org.citrusframework.util.StringUtils.isEmpty;
import static org.springframework.data.domain.Pageable.unpaged;

/**
* Service for executing complex queries for {@link ScenarioExecution} entities in the database.
Expand All @@ -84,10 +85,51 @@ public ScenarioExecutionQueryService(EntityManager entityManager, ScenarioExecut
this.scenarioExecutionRepository = scenarioExecutionRepository;
}

@VisibleForTesting
static boolean isValidFilterPattern(String filterPattern) {
return HEADER_FILTER_PATTERN.matcher(filterPattern).matches();
}

private static Specification<ScenarioExecution> withResultDetailsConfiguration(ResultDetailsConfiguration config) {
return (root, query, cb) -> {
assert query != null;

// Prevent duplicate joins in count queries
if (query.getResultType() == Long.class || query.getResultType() == long.class) {
return null;
}

root.fetch(ScenarioExecution_.testResult, JoinType.LEFT);

if (config.includeParameters()) {
root.fetch(ScenarioExecution_.scenarioParameters, JoinType.LEFT);
}

if (config.includeActions()) {
root.fetch(ScenarioExecution_.scenarioActions, JoinType.LEFT);
}

if (config.includeMessages() || config.includeMessageHeaders()) {
var messagesJoin = root.fetch(ScenarioExecution_.scenarioMessages, JoinType.LEFT);

if (config.includeMessageHeaders()) {
messagesJoin.fetch(Message_.headers, JoinType.LEFT);
}
}

// Return no additional where clause
return null;
};
}

private static Specification<ScenarioExecution> withIds(List<Long> executionIds) {
return (root, query, builder) -> {
var in = builder.in(root.get(ScenarioExecution_.executionId));
executionIds.forEach(in::value);
return in;
};
}

/**
* Return a {@link List} of {@link ScenarioExecution} which matches the criteria from the database.
*
Expand All @@ -110,6 +152,19 @@ public List<ScenarioExecution> findByCriteria(ScenarioExecutionCriteria criteria
*/
@Transactional(readOnly = true)
public Page<ScenarioExecution> findByCriteria(ScenarioExecutionCriteria criteria, Pageable page) {
return findByCriteria(criteria, page, withAllDetails());
}

/**
* Return a {@link Page} of {@link ScenarioExecution} which matches the criteria from the database.
*
* @param criteria The object which holds all the filters, which the entities should match.
* @param page The page, which should be returned.
* @param resultDetailsConfiguration Fetch-configuration of relationships
* @return the matching entities.
*/
@Transactional(readOnly = true)
public Page<ScenarioExecution> findByCriteria(ScenarioExecutionCriteria criteria, Pageable page, ResultDetailsConfiguration resultDetailsConfiguration) {
logger.debug("find by criteria : {}, page: {}", criteria, page);

var specification = createSpecification(criteria);
Expand All @@ -122,11 +177,20 @@ public Page<ScenarioExecution> findByCriteria(ScenarioExecutionCriteria criteria
)
.getResultList();

var scenarioExecutions = scenarioExecutionRepository.findAllWhereExecutionIdIn(scenarioExecutionIds, unpaged(page.getSort()));
if (scenarioExecutionIds.isEmpty()) {
return Page.empty(page);
}

var fetchSpec = withIds(scenarioExecutionIds)
.and(withResultDetailsConfiguration(resultDetailsConfiguration));

var scenarioExecutions = scenarioExecutionRepository.findAll(fetchSpec, page.getSort());

return new PageImpl<>(
scenarioExecutions.getContent(),
scenarioExecutions,
page,
scenarioExecutionRepository.count(specification));
scenarioExecutionRepository.count(specification)
);
}

/**
Expand Down Expand Up @@ -343,4 +407,16 @@ public InvalidPatternException(String filterPattern) {
super(format("The header filter pattern '%s' does not comply with the regex '%s'!", filterPattern, HEADER_FILTER_PATTERN.pattern()));
}
}

public record ResultDetailsConfiguration(
boolean includeActions,
boolean includeMessages,
boolean includeMessageHeaders,
boolean includeParameters
) {

static ResultDetailsConfiguration withAllDetails() {
return new ResultDetailsConfiguration(true, true, true, true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import org.citrusframework.simulator.model.ScenarioExecution;
import org.citrusframework.simulator.service.ScenarioExecutionQueryService;
import org.citrusframework.simulator.service.ScenarioExecutionQueryService.ResultDetailsConfiguration;
import org.citrusframework.simulator.service.ScenarioExecutionService;
import org.citrusframework.simulator.service.criteria.ScenarioExecutionCriteria;
import org.citrusframework.simulator.web.rest.dto.ScenarioExecutionDTO;
Expand All @@ -40,7 +41,6 @@
import java.util.List;
import java.util.Optional;

import static java.lang.Boolean.FALSE;
import static org.citrusframework.simulator.web.util.PaginationUtil.generatePaginationHttpHeaders;

/**
Expand Down Expand Up @@ -77,21 +77,27 @@ public ScenarioExecutionResource(
@GetMapping("/scenario-executions")
public ResponseEntity<List<ScenarioExecutionDTO>> getAllScenarioExecutions(
ScenarioExecutionCriteria criteria,
@RequestParam(name = "includeActions", required = false, defaultValue = "false") Boolean includeActions,
@RequestParam(name = "includeMessages", required = false, defaultValue = "false") Boolean includeMessages,
@RequestParam(name = "includeParameters", required = false, defaultValue = "false") Boolean includeParameters,
@RequestParam(name = "includeActions", required = false, defaultValue = "false") boolean includeActions,
@RequestParam(name = "includeMessages", required = false, defaultValue = "false") boolean includeMessages,
@RequestParam(name = "includeMessageHeaders", required = false, defaultValue = "false") boolean includeMessageHeaders,
@RequestParam(name = "includeParameters", required = false, defaultValue = "false") boolean includeParameters,
@ParameterObject Pageable pageable
) {
logger.debug("REST request to get ScenarioExecutions by criteria: {}", criteria);

Page<ScenarioExecution> page = scenarioExecutionQueryService.findByCriteria(criteria, pageable);
Page<ScenarioExecution> page = scenarioExecutionQueryService.findByCriteria(
criteria,
pageable,
new ResultDetailsConfiguration(includeActions, includeMessages, includeMessageHeaders, includeParameters)
);
HttpHeaders headers = generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page);
return ResponseEntity.ok()
.headers(headers)
.body(page.getContent().stream()
.map(scenarioExecution -> stripPageContents(scenarioExecution, includeActions, includeMessages, includeParameters))
.map(scenarioExecutionMapper::toDto)
.toList());
.body(
page.getContent().stream()
.map(scenarioExecutionMapper::toDto)
.toList()
);
}

/**
Expand All @@ -118,17 +124,4 @@ public ResponseEntity<ScenarioExecutionDTO> getScenarioExecution(@PathVariable("
Optional<ScenarioExecution> scenarioExecution = scenarioExecutionService.findOne(id);
return ResponseUtil.wrapOrNotFound(scenarioExecution.map(scenarioExecutionMapper::toDto));
}

private ScenarioExecution stripPageContents(ScenarioExecution scenarioExecution, Boolean includeActions, Boolean includeMessages, Boolean includeParameters) {
if (FALSE.equals(includeActions)) {
scenarioExecution.getScenarioActions().clear();
}
if (FALSE.equals(includeMessages)) {
scenarioExecution.getScenarioMessages().clear();
}
if (FALSE.equals(includeParameters)) {
scenarioExecution.getScenarioParameters().clear();
}
return scenarioExecution;
}
}
Loading

0 comments on commit bcb593c

Please sign in to comment.