io.camunda.operate.webapp.opensearch.reader.OpensearchFlowNodeInstanceReader Maven / Gradle / Ivy
The newest version!
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
* one or more contributor license agreements. See the NOTICE file distributed
* with this work for additional information regarding copyright ownership.
* Licensed under the Camunda License 1.0. You may not use this file
* except in compliance with the Camunda License 1.0.
*/
package io.camunda.operate.webapp.opensearch.reader;
import static io.camunda.operate.entities.FlowNodeState.ACTIVE;
import static io.camunda.operate.entities.FlowNodeState.COMPLETED;
import static io.camunda.operate.entities.FlowNodeState.TERMINATED;
import static io.camunda.operate.schema.indices.DecisionIndex.DECISION_ID;
import static io.camunda.operate.schema.templates.DecisionInstanceTemplate.DECISION_NAME;
import static io.camunda.operate.schema.templates.DecisionInstanceTemplate.ELEMENT_INSTANCE_KEY;
import static io.camunda.operate.schema.templates.DecisionInstanceTemplate.EVALUATION_DATE;
import static io.camunda.operate.schema.templates.FlowNodeInstanceTemplate.END_DATE;
import static io.camunda.operate.schema.templates.FlowNodeInstanceTemplate.FLOW_NODE_ID;
import static io.camunda.operate.schema.templates.FlowNodeInstanceTemplate.ID;
import static io.camunda.operate.schema.templates.FlowNodeInstanceTemplate.INCIDENT;
import static io.camunda.operate.schema.templates.FlowNodeInstanceTemplate.LEVEL;
import static io.camunda.operate.schema.templates.FlowNodeInstanceTemplate.PROCESS_INSTANCE_KEY;
import static io.camunda.operate.schema.templates.FlowNodeInstanceTemplate.START_DATE;
import static io.camunda.operate.schema.templates.FlowNodeInstanceTemplate.STATE;
import static io.camunda.operate.schema.templates.FlowNodeInstanceTemplate.TREE_PATH;
import static io.camunda.operate.schema.templates.FlowNodeInstanceTemplate.TYPE;
import static io.camunda.operate.store.opensearch.OpensearchIncidentStore.ACTIVE_INCIDENT_QUERY;
import static io.camunda.operate.store.opensearch.dsl.AggregationDSL.filtersAggregation;
import static io.camunda.operate.store.opensearch.dsl.AggregationDSL.termAggregation;
import static io.camunda.operate.store.opensearch.dsl.AggregationDSL.topHitsAggregation;
import static io.camunda.operate.store.opensearch.dsl.AggregationDSL.withSubaggregations;
import static io.camunda.operate.store.opensearch.dsl.QueryDSL.*;
import static io.camunda.operate.store.opensearch.dsl.RequestDSL.QueryType.ONLY_RUNTIME;
import static io.camunda.operate.store.opensearch.dsl.RequestDSL.searchRequestBuilder;
import static io.camunda.operate.util.ElasticsearchUtil.TERMS_AGG_SIZE;
import static io.camunda.operate.webapp.rest.dto.incidents.IncidentDto.FALLBACK_PROCESS_DEFINITION_NAME;
import static org.opensearch.client.opensearch._types.SortOrder.Asc;
import static org.opensearch.client.opensearch._types.SortOrder.Desc;
import io.camunda.operate.cache.ProcessCache;
import io.camunda.operate.conditions.OpensearchCondition;
import io.camunda.operate.entities.FlowNodeInstanceEntity;
import io.camunda.operate.entities.FlowNodeState;
import io.camunda.operate.entities.FlowNodeType;
import io.camunda.operate.entities.IncidentEntity;
import io.camunda.operate.entities.dmn.DecisionInstanceState;
import io.camunda.operate.exceptions.OperateRuntimeException;
import io.camunda.operate.schema.templates.DecisionInstanceTemplate;
import io.camunda.operate.schema.templates.FlowNodeInstanceTemplate;
import io.camunda.operate.schema.templates.IncidentTemplate;
import io.camunda.operate.store.opensearch.client.sync.OpenSearchDocumentOperations;
import io.camunda.operate.util.TreePath;
import io.camunda.operate.webapp.data.IncidentDataHolder;
import io.camunda.operate.webapp.elasticsearch.reader.ProcessInstanceReader;
import io.camunda.operate.webapp.reader.FlowNodeInstanceReader;
import io.camunda.operate.webapp.rest.FlowNodeInstanceMetadataBuilder;
import io.camunda.operate.webapp.rest.dto.FlowNodeStatisticsDto;
import io.camunda.operate.webapp.rest.dto.activity.FlowNodeInstanceDto;
import io.camunda.operate.webapp.rest.dto.activity.FlowNodeInstanceQueryDto;
import io.camunda.operate.webapp.rest.dto.activity.FlowNodeInstanceRequestDto;
import io.camunda.operate.webapp.rest.dto.activity.FlowNodeInstanceResponseDto;
import io.camunda.operate.webapp.rest.dto.activity.FlowNodeStateDto;
import io.camunda.operate.webapp.rest.dto.incidents.IncidentDto;
import io.camunda.operate.webapp.rest.dto.metadata.DecisionInstanceReferenceDto;
import io.camunda.operate.webapp.rest.dto.metadata.FlowNodeInstanceBreadcrumbEntryDto;
import io.camunda.operate.webapp.rest.dto.metadata.FlowNodeInstanceMetadata;
import io.camunda.operate.webapp.rest.dto.metadata.FlowNodeMetadataDto;
import io.camunda.operate.webapp.rest.dto.metadata.FlowNodeMetadataRequestDto;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.opensearch.client.opensearch._types.aggregations.*;
import org.opensearch.client.opensearch._types.query_dsl.Query;
import org.opensearch.client.opensearch.core.SearchRequest;
import org.opensearch.client.opensearch.core.search.Hit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
@Conditional(OpensearchCondition.class)
@Component
public class OpensearchFlowNodeInstanceReader extends OpensearchAbstractReader
implements FlowNodeInstanceReader {
@Autowired private FlowNodeInstanceTemplate flowNodeInstanceTemplate;
@Autowired private IncidentTemplate incidentTemplate;
@Autowired private OpensearchIncidentReader incidentReader;
@Autowired private DecisionInstanceTemplate decisionInstanceTemplate;
@Autowired private ProcessInstanceReader processInstanceReader;
@Autowired private ProcessCache processCache;
@Autowired private FlowNodeInstanceMetadataBuilder flowNodeInstanceMetadataBuilder;
@Override
public Map getFlowNodeInstances(
final FlowNodeInstanceRequestDto request) {
final Map response = new HashMap<>();
for (final FlowNodeInstanceQueryDto query : request.getQueries()) {
response.put(query.getTreePath(), getFlowNodeInstances(query));
}
return response;
}
@Override
public FlowNodeMetadataDto getFlowNodeMetadata(
final String processInstanceId, final FlowNodeMetadataRequestDto request) {
if (request.getFlowNodeId() != null) {
return getMetadataByFlowNodeId(
processInstanceId, request.getFlowNodeId(), request.getFlowNodeType());
} else if (request.getFlowNodeInstanceId() != null) {
return getMetadataByFlowNodeInstanceId(request.getFlowNodeInstanceId());
}
return null;
}
@Override
public Map getFlowNodeStates(final String processInstanceId) {
final String latestFlowNodeAggName = "latestFlowNode";
final String activeFlowNodesAggName = "activeFlowNodes";
final String activeFlowNodesBucketsAggName = "activeFlowNodesBuckets";
final String finishedFlowNodesAggName = "finishedFlowNodes";
final Query query =
constantScore(withTenantCheck(term(PROCESS_INSTANCE_KEY, processInstanceId)));
final Aggregation notCompletedFlowNodesAggs =
withSubaggregations(
stringTerms(STATE, List.of(ACTIVE.name(), TERMINATED.name())),
Map.of(
activeFlowNodesBucketsAggName,
withSubaggregations(
termAggregation(FLOW_NODE_ID, TERMS_AGG_SIZE),
Map.of(
latestFlowNodeAggName,
topHitsAggregation(
List.of(STATE, TREE_PATH), 1, sortOptions(START_DATE, Desc))
._toAggregation()))));
final Aggregation finishedFlowNodesAggs =
withSubaggregations(
term(STATE, COMPLETED.name()),
Map.of(
FINISHED_FLOW_NODES_BUCKETS_AGG_NAME,
termAggregation(FLOW_NODE_ID, TERMS_AGG_SIZE)._toAggregation()));
final Aggregation incidentsAggs =
withSubaggregations(
term(INCIDENT, true),
Map.of(
AGG_INCIDENT_PATHS, termAggregation(TREE_PATH, TERMS_AGG_SIZE)._toAggregation()));
final var searchRequestBuilder =
searchRequestBuilder(flowNodeInstanceTemplate)
.query(query)
.aggregations(
Map.of(
activeFlowNodesAggName, notCompletedFlowNodesAggs,
AGG_INCIDENTS, incidentsAggs,
finishedFlowNodesAggName, finishedFlowNodesAggs))
.size(0);
final Map aggregates =
richOpenSearchClient.doc().searchAggregations(searchRequestBuilder);
final Set incidentPaths = getIncidentPaths(aggregates.get(AGG_INCIDENTS).filter());
final Set finishedFlowNodes =
collectFinishedFlowNodes(aggregates.get(finishedFlowNodesAggName).filter());
final StringTermsAggregate flowNodesAgg =
aggregates
.get(activeFlowNodesAggName)
.filter()
.aggregations()
.get(activeFlowNodesBucketsAggName)
.sterms();
final Map result = new HashMap<>();
if (flowNodesAgg != null) {
record FlowNodeResult(String state, String treePath) {}
for (final StringTermsBucket bucket : flowNodesAgg.buckets().array()) {
final var lastFlowNode =
bucket
.aggregations()
.get(latestFlowNodeAggName)
.topHits()
.hits()
.hits()
.get(0)
.source()
.to(FlowNodeResult.class);
var flowNodeState = FlowNodeStateDto.valueOf(lastFlowNode.state());
if (flowNodeState == FlowNodeStateDto.ACTIVE
&& incidentPaths.contains(lastFlowNode.treePath())) {
flowNodeState = FlowNodeStateDto.INCIDENT;
}
result.put(bucket.key(), flowNodeState);
}
}
// add finished when needed
for (final String finishedFlowNodeId : finishedFlowNodes) {
if (result.get(finishedFlowNodeId) == null) {
result.put(finishedFlowNodeId, FlowNodeStateDto.COMPLETED);
}
}
return result;
}
@Override
public List getFlowNodeInstanceKeysByIdAndStates(
final Long processInstanceId, final String flowNodeId, final List states) {
final var searchRequestBuilder =
searchRequestBuilder(flowNodeInstanceTemplate.getAlias())
.query(
withTenantCheck(
and(
term(FLOW_NODE_ID, flowNodeId),
term(PROCESS_INSTANCE_KEY, processInstanceId),
stringTerms(STATE, states.stream().map(Enum::name).toList()))))
.source(sourceInclude(ID));
record Result(String id) {}
return richOpenSearchClient.doc().searchValues(searchRequestBuilder, Result.class).stream()
.map(r -> Long.parseLong(r.id()))
.toList();
}
@Override
public Collection getFlowNodeStatisticsForProcessInstance(
final Long processInstanceId) {
final var searchRequestBuilder =
searchRequestBuilder(flowNodeInstanceTemplate)
.query(
constantScore(
withTenantCheck(
term(FlowNodeInstanceTemplate.PROCESS_INSTANCE_KEY, processInstanceId))))
.aggregations(
FLOW_NODE_ID_AGG,
withSubaggregations(
termAggregation(FLOW_NODE_ID, TERMS_AGG_SIZE),
Map.of(
COUNT_INCIDENT, term(INCIDENT, true)._toAggregation(),
COUNT_CANCELED,
and(
not(term(TYPE, FlowNodeType.MULTI_INSTANCE_BODY.name())),
term(STATE, TERMINATED.name()))
._toAggregation(),
COUNT_COMPLETED,
and(
not(term(TYPE, FlowNodeType.MULTI_INSTANCE_BODY.name())),
term(STATE, COMPLETED.name()))
._toAggregation(),
COUNT_ACTIVE,
and(
not(term(TYPE, FlowNodeType.MULTI_INSTANCE_BODY.name())),
term(STATE, ACTIVE.name()),
term(INCIDENT, false))
._toAggregation())))
.size(0);
return richOpenSearchClient
.doc()
.searchAggregations(searchRequestBuilder)
.get(FLOW_NODE_ID_AGG)
.sterms()
.buckets()
.array()
.stream()
.map(
entry ->
new FlowNodeStatisticsDto()
.setActivityId(entry.key())
.setCanceled(entry.aggregations().get(COUNT_CANCELED).filter().docCount())
.setIncidents(entry.aggregations().get(COUNT_INCIDENT).filter().docCount())
.setCompleted(entry.aggregations().get(COUNT_COMPLETED).filter().docCount())
.setActive(entry.aggregations().get(COUNT_ACTIVE).filter().docCount()))
.toList();
}
@Override
public List getAllFlowNodeInstances(final Long processInstanceKey) {
final var searchRequestBuilder =
searchRequestBuilder(flowNodeInstanceTemplate)
.query(
constantScore(
withTenantCheck(
term(FlowNodeInstanceTemplate.PROCESS_INSTANCE_KEY, processInstanceKey))))
.sort(sortOptions(FlowNodeInstanceTemplate.POSITION, Asc));
return richOpenSearchClient
.doc()
.scrollValues(searchRequestBuilder, FlowNodeInstanceEntity.class);
}
private FlowNodeInstanceResponseDto getFlowNodeInstances(final FlowNodeInstanceQueryDto request) {
final FlowNodeInstanceResponseDto response = queryFlowNodeInstances(request);
// query one additional instance
if (request.getSearchAfterOrEqual() != null || request.getSearchBeforeOrEqual() != null) {
adjustResponse(response, request);
}
return response;
}
private void adjustResponse(
final FlowNodeInstanceResponseDto response, final FlowNodeInstanceQueryDto request) {
String flowNodeInstanceId = null;
if (request.getSearchAfterOrEqual() != null) {
flowNodeInstanceId = (String) request.getSearchAfterOrEqual(objectMapper)[1];
} else if (request.getSearchBeforeOrEqual() != null) {
flowNodeInstanceId = (String) request.getSearchBeforeOrEqual(objectMapper)[1];
}
final FlowNodeInstanceQueryDto newRequest =
request
.createCopy()
.setSearchAfter(null)
.setSearchAfterOrEqual(null)
.setSearchBefore(null)
.setSearchBeforeOrEqual(null);
final List entities =
queryFlowNodeInstances(newRequest, flowNodeInstanceId).getChildren();
if (entities.size() > 0) {
final FlowNodeInstanceDto entity = entities.get(0);
final List children = response.getChildren();
if (request.getSearchAfterOrEqual() != null) {
// insert at the beginning of the list and remove the last element
if (request.getPageSize() != null && children.size() == request.getPageSize()) {
children.remove(children.size() - 1);
}
children.add(0, entity);
} else if (request.getSearchBeforeOrEqual() != null) {
// insert at the end of the list and remove the first element
if (request.getPageSize() != null && children.size() == request.getPageSize()) {
children.remove(0);
}
children.add(entity);
}
}
}
private FlowNodeInstanceResponseDto queryFlowNodeInstances(
final FlowNodeInstanceQueryDto flowNodeInstanceRequest) {
return queryFlowNodeInstances(flowNodeInstanceRequest, null);
}
private FlowNodeInstanceResponseDto queryFlowNodeInstances(
final FlowNodeInstanceQueryDto flowNodeInstanceRequest, final String flowNodeInstanceId) {
final String processInstanceId = flowNodeInstanceRequest.getProcessInstanceId();
final String parentTreePath = flowNodeInstanceRequest.getTreePath();
final int level = parentTreePath.split("/").length;
final Query idsQuery = flowNodeInstanceId != null ? ids(flowNodeInstanceId) : null;
final Query query =
withTenantCheck(constantScore(term(PROCESS_INSTANCE_KEY, processInstanceId)));
final Aggregation runningParentsAgg =
and(not(exists(END_DATE)), prefix(TREE_PATH, parentTreePath), term(LEVEL, level - 1))
._toAggregation();
final Query postFilter = and(term(LEVEL, level), prefix(TREE_PATH, parentTreePath), idsQuery);
final SearchRequest.Builder searchRequestBuilder =
searchRequestBuilder(flowNodeInstanceTemplate)
.query(query)
.aggregations(AGG_RUNNING_PARENT, runningParentsAgg)
.postFilter(postFilter);
if (flowNodeInstanceRequest.getPageSize() != null) {
searchRequestBuilder.size(flowNodeInstanceRequest.getPageSize());
}
applySorting(searchRequestBuilder, flowNodeInstanceRequest);
try {
final FlowNodeInstanceResponseDto response;
if (flowNodeInstanceRequest.getPageSize() != null) {
response = getOnePage(searchRequestBuilder, processInstanceId);
} else {
response = scrollAllSearchHits(searchRequestBuilder, processInstanceId);
}
// for process instance level, we don't return running flag
if (level == 1) {
response.setRunning(null);
}
if (flowNodeInstanceRequest.getSearchBefore() != null
|| flowNodeInstanceRequest.getSearchBeforeOrEqual() != null) {
Collections.reverse(response.getChildren());
}
return response;
} catch (final IOException e) {
final String message =
String.format(
"Exception occurred, while obtaining all flow node instances: %s", e.getMessage());
throw new OperateRuntimeException(message, e);
}
}
private FlowNodeInstanceResponseDto scrollAllSearchHits(
final SearchRequest.Builder searchRequestBuilder, final String processInstanceId)
throws IOException {
final OpenSearchDocumentOperations.AggregatedResult> response =
richOpenSearchClient.doc().scrollHits(searchRequestBuilder, FlowNodeInstanceEntity.class);
final List children =
response.values().stream()
.map(
hit -> {
final var entity = hit.source();
entity.setSortValues(hit.sort().toArray());
return entity;
})
.toList();
final boolean runningParent = isRunningParent(response.aggregates());
markHasIncident(processInstanceId, children);
return new FlowNodeInstanceResponseDto(
runningParent, FlowNodeInstanceDto.createFrom(children, objectMapper));
}
private void applySorting(
final SearchRequest.Builder searchRequestBuilder, final FlowNodeInstanceQueryDto request) {
final boolean directSorting =
request.getSearchAfter() != null
|| request.getSearchAfterOrEqual() != null
|| (request.getSearchBefore() == null && request.getSearchBeforeOrEqual() == null);
final Function