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

improved scenario execution endpoint fetch performance #319

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
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) {
bbortt marked this conversation as resolved.
Show resolved Hide resolved
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));
bbortt marked this conversation as resolved.
Show resolved Hide resolved
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
Loading