
io.nflow.engine.internal.dao.WorkflowInstanceDao Maven / Gradle / Ivy
package io.nflow.engine.internal.dao;
import static io.nflow.engine.internal.dao.DaoUtil.firstColumnLengthExtractor;
import static io.nflow.engine.internal.dao.DaoUtil.getInt;
import static io.nflow.engine.internal.dao.DaoUtil.getLong;
import static io.nflow.engine.internal.dao.DaoUtil.toTimestamp;
import static io.nflow.engine.workflow.instance.WorkflowInstance.WorkflowInstanceStatus.created;
import static io.nflow.engine.workflow.instance.WorkflowInstance.WorkflowInstanceStatus.executing;
import static io.nflow.engine.workflow.instance.WorkflowInstance.WorkflowInstanceStatus.inProgress;
import static io.nflow.engine.workflow.instance.WorkflowInstanceAction.WorkflowActionType.recovery;
import static java.lang.Math.min;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.sort;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.abbreviate;
import static org.apache.commons.lang3.StringUtils.join;
import static org.joda.time.DateTime.now;
import static org.slf4j.LoggerFactory.getLogger;
import static org.springframework.transaction.annotation.Propagation.MANDATORY;
import static org.springframework.util.CollectionUtils.isEmpty;
import static org.springframework.util.StringUtils.collectionToDelimitedString;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.springframework.core.env.Environment;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.support.AbstractInterruptibleBatchPreparedStatementSetter;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.nflow.engine.config.NFlow;
import io.nflow.engine.internal.executor.InstanceInfo;
import io.nflow.engine.internal.executor.WorkflowInstanceExecutor;
import io.nflow.engine.internal.storage.db.SQLVariants;
import io.nflow.engine.model.ModelObject;
import io.nflow.engine.service.WorkflowInstanceInclude;
import io.nflow.engine.workflow.instance.QueryWorkflowInstances;
import io.nflow.engine.workflow.instance.WorkflowInstance;
import io.nflow.engine.workflow.instance.WorkflowInstance.WorkflowInstanceStatus;
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
import io.nflow.engine.workflow.instance.WorkflowInstanceAction.WorkflowActionType;
import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
/**
* Use setter injection because constructor injection may not work when nFlow is used in some legacy systems.
*/
@Component
@SuppressFBWarnings(value = "SIC_INNER_SHOULD_BE_STATIC_ANON", justification = "common jdbctemplate practice")
public class WorkflowInstanceDao {
private static final Logger logger = getLogger(WorkflowInstanceDao.class);
static final Map> EMPTY_ACTION_STATE_MAP = emptyMap();
final JdbcTemplate jdbc;
private final NamedParameterJdbcTemplate namedJdbc;
private final TransactionTemplate transaction;
final ExecutorDao executorInfo;
final SQLVariants sqlVariants;
private final WorkflowInstanceExecutor workflowInstanceExecutor;
final WorkflowInstanceFactory workflowInstanceFactory;
private final long workflowInstanceQueryMaxResults;
private final long workflowInstanceQueryMaxResultsDefault;
private final long workflowInstanceQueryMaxActions;
private final long workflowInstanceQueryMaxActionsDefault;
private final boolean disableBatchUpdates;
int instanceStateTextLength;
int actionStateTextLength;
@Inject
public WorkflowInstanceDao(SQLVariants sqlVariants,
@NFlow JdbcTemplate nflowJdbcTemplate,
@NFlow TransactionTemplate transactionTemplate,
@NFlow NamedParameterJdbcTemplate nflowNamedParameterJdbcTemplate,
ExecutorDao executorDao,
WorkflowInstanceExecutor workflowInstanceExecutor,
WorkflowInstanceFactory workflowInstanceFactory,
Environment env) {
this.sqlVariants = sqlVariants;
this.jdbc = nflowJdbcTemplate;
this.transaction = transactionTemplate;
this.namedJdbc = nflowNamedParameterJdbcTemplate;
this.executorInfo = executorDao;
this.workflowInstanceExecutor = workflowInstanceExecutor;
this.workflowInstanceFactory = workflowInstanceFactory;
workflowInstanceQueryMaxResults = env.getRequiredProperty("nflow.workflow.instance.query.max.results", Long.class);
workflowInstanceQueryMaxResultsDefault = env.getRequiredProperty("nflow.workflow.instance.query.max.results.default",
Long.class);
workflowInstanceQueryMaxActions = env.getRequiredProperty("nflow.workflow.instance.query.max.actions", Long.class);
workflowInstanceQueryMaxActionsDefault = env.getRequiredProperty("nflow.workflow.instance.query.max.actions.default",
Long.class);
disableBatchUpdates = env.getRequiredProperty("nflow.db.disable_batch_updates", Boolean.class);
if (disableBatchUpdates) {
logger.info("nFlow DB batch updates are disabled (system property nflow.db.disable_batch_updates=true)");
}
// In one deployment, FirstColumnLengthExtractor returned 0 column length (H2), so allow explicit length setting.
instanceStateTextLength = env.getProperty("nflow.workflow.instance.state.text.length", Integer.class, -1);
actionStateTextLength = env.getProperty("nflow.workflow.action.state.text.length", Integer.class, -1);
}
private int getInstanceStateTextLength() {
if (instanceStateTextLength == -1) {
instanceStateTextLength = jdbc.query("select state_text from nflow_workflow where 1 = 0", firstColumnLengthExtractor);
}
return instanceStateTextLength;
}
int getActionStateTextLength() {
if (actionStateTextLength == -1) {
actionStateTextLength = jdbc.query("select state_text from nflow_workflow_action where 1 = 0", firstColumnLengthExtractor);
}
return actionStateTextLength;
}
public long insertWorkflowInstance(WorkflowInstance instance) {
long id;
if (sqlVariants.hasUpdateableCTE()) {
id = insertWorkflowInstanceWithCte(instance);
} else {
id = insertWorkflowInstanceWithTransaction(instance);
}
if (instance.nextActivation != null && instance.nextActivation.isBeforeNow()) {
workflowInstanceExecutor.wakeUpDispatcherIfNeeded();
}
return id;
}
private long insertWorkflowInstanceWithCte(WorkflowInstance instance) {
try {
StringBuilder sqlb = new StringBuilder(256);
sqlb.append("with wf as (").append(insertWorkflowInstanceSql()).append(" returning id)");
Object[] instanceValues = new Object[] { instance.type, instance.priority, instance.rootWorkflowId,
instance.parentWorkflowId, instance.parentActionId, instance.businessKey, instance.externalId,
executorInfo.getExecutorGroup(), instance.status.name(), instance.state,
abbreviate(instance.stateText, getInstanceStateTextLength()), toTimestamp(instance.nextActivation),
instance.signal.orElse(null) };
int pos = instanceValues.length;
Object[] args = Arrays.copyOf(instanceValues, pos + instance.stateVariables.size() * 2);
for (Entry variable : instance.stateVariables.entrySet()) {
sqlb.append(", ins").append(pos).append(" as (").append(insertWorkflowInstanceStateSql())
.append(" select wf.id,0,?,? from wf)");
args[pos++] = variable.getKey();
args[pos++] = variable.getValue();
}
sqlb.append(" select wf.id from wf");
return jdbc.queryForObject(sqlb.toString(), Long.class, args);
} catch (DuplicateKeyException e) {
logger.warn("Failed to insert workflow instance", e);
return -1;
}
}
boolean useBatchUpdate() {
return !disableBatchUpdates && sqlVariants.useBatchUpdate();
}
String insertWorkflowInstanceSql() {
return "insert into nflow_workflow(type, priority, root_workflow_id, parent_workflow_id, parent_action_id, business_key, external_id, "
+ "executor_group, status, state, state_text, next_activation, workflow_signal) values (?, ?, ?, ?, ?, ?, ?, ?, "
+ sqlVariants.workflowStatus() + ", ?, ?, ?, ?)";
}
String insertWorkflowInstanceStateSql() {
return "insert into nflow_workflow_state(workflow_id, action_id, state_key, state_value)";
}
@SuppressFBWarnings(value = { "OBL_UNSATISFIED_OBLIGATION_EXCEPTION_EDGE",
"SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING" }, justification = "findbugs does not trust jdbctemplate, sql string is practically constant")
private long insertWorkflowInstanceWithTransaction(final WorkflowInstance instance) {
return transaction.execute(status -> {
KeyHolder keyHolder = new GeneratedKeyHolder();
try {
jdbc.update((PreparedStatementCreator) connection -> {
int p = 1;
PreparedStatement ps = connection.prepareStatement(insertWorkflowInstanceSql(), new String[] { "id" });
ps.setString(p++, instance.type);
ps.setShort(p++, instance.priority);
ps.setObject(p++, instance.rootWorkflowId);
ps.setObject(p++, instance.parentWorkflowId);
ps.setObject(p++, instance.parentActionId);
ps.setString(p++, instance.businessKey);
ps.setString(p++, instance.externalId);
ps.setString(p++, executorInfo.getExecutorGroup());
ps.setString(p++, instance.status.name());
ps.setString(p++, instance.state);
ps.setString(p++, abbreviate(instance.stateText, getInstanceStateTextLength()));
sqlVariants.setDateTime(ps, p++, instance.nextActivation);
if (instance.signal.isPresent()) {
ps.setInt(p++, instance.signal.get());
} else {
ps.setNull(p++, Types.INTEGER);
}
return ps;
}, keyHolder);
} catch (DuplicateKeyException e) {
logger.warn("Failed to insert workflow instance", e);
return -1L;
}
long id = keyHolder.getKey().longValue();
insertVariables(id, 0, instance.stateVariables);
return id;
});
}
void insertVariables(final long id, final long actionId, Map changedStateVariables) {
if (changedStateVariables.isEmpty()) {
return;
}
if (useBatchUpdate()) {
insertVariablesWithBatchUpdate(id, actionId, changedStateVariables);
} else {
insertVariablesWithMultipleUpdates(id, actionId, changedStateVariables);
}
}
private void insertVariablesWithMultipleUpdates(final long id, final long actionId, Map changedStateVariables) {
for (Entry entry : changedStateVariables.entrySet()) {
int updated = jdbc.update(insertWorkflowInstanceStateSql() + " values (?,?,?,?)", id, actionId, entry.getKey(),
entry.getValue());
if (updated != 1) {
throw new IllegalStateException("Failed to insert state variable " + entry.getKey());
}
}
}
private void insertVariablesWithBatchUpdate(final long id, final long actionId, Map changedStateVariables) {
final Iterator> variables = changedStateVariables.entrySet().iterator();
int[] updateStatus = jdbc.batchUpdate(insertWorkflowInstanceStateSql() + " values (?,?,?,?)",
new AbstractInterruptibleBatchPreparedStatementSetter() {
@Override
protected boolean setValuesIfAvailable(PreparedStatement ps, int i) throws SQLException {
if (!variables.hasNext()) {
return false;
}
Entry variable = variables.next();
ps.setLong(1, id);
ps.setLong(2, actionId);
ps.setString(3, variable.getKey());
ps.setString(4, variable.getValue());
return true;
}
});
int updatedRows = 0;
boolean unknownResults = false;
for (int i = 0; i < updateStatus.length; ++i) {
if (updateStatus[i] == Statement.SUCCESS_NO_INFO) {
unknownResults = true;
continue;
}
if (updateStatus[i] == Statement.EXECUTE_FAILED) {
throw new IllegalStateException("Failed to insert/update state variable at index " + i + " (" + updateStatus[i] + ")");
}
updatedRows += updateStatus[i];
}
int changedVariables = changedStateVariables.size();
if (!unknownResults && updatedRows != changedVariables) {
throw new IllegalStateException(
"Failed to insert/update state variables, expected update count " + changedVariables + ", actual " + updatedRows);
}
}
@SuppressWarnings("null")
public void updateWorkflowInstanceAfterExecution(WorkflowInstance instance, WorkflowInstanceAction action,
List childWorkflows, List workflows, boolean createAction) {
Assert.isTrue(action != null, "action can not be null");
Assert.isTrue(childWorkflows != null, "childWorkflows can not be null");
Assert.isTrue(workflows != null, "workflows can not be null");
Map changedStateVariables = instance.getChangedStateVariables();
if (!createAction && (!childWorkflows.isEmpty() || !workflows.isEmpty() || !changedStateVariables.isEmpty())) {
logger.info("Forcing action creation because new workflow instances are created or state variables are changed.");
createAction = true;
}
if (createAction) {
if (sqlVariants.hasUpdateableCTE() && childWorkflows.isEmpty() && workflows.isEmpty()) {
updateWorkflowInstanceWithCTE(instance, action, changedStateVariables);
} else {
updateWorkflowInstanceWithTransaction(instance, action, childWorkflows, workflows, changedStateVariables);
}
} else {
updateWorkflowInstance(instance);
}
}
public int updateWorkflowInstance(WorkflowInstance instance) {
// using sqlVariants.nextActivationUpdate() requires that nextActivation is used 3 times
Object nextActivation = sqlVariants.toTimestampObject(instance.nextActivation);
return jdbc.update(updateWorkflowInstanceSql(), instance.status.name(), instance.state,
abbreviate(instance.stateText, getInstanceStateTextLength()), nextActivation, nextActivation, nextActivation,
instance.status == executing ? executorInfo.getExecutorId() : null, instance.retries, toTimestamp(instance.started),
instance.id);
}
private void updateWorkflowInstanceWithTransaction(final WorkflowInstance instance, final WorkflowInstanceAction action,
final List childWorkflows, final List workflows,
final Map changedStateVariables) {
transaction.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
updateWorkflowInstance(instance);
long parentActionId = insertWorkflowInstanceAction(action);
insertVariables(action.workflowInstanceId, parentActionId, changedStateVariables);
for (WorkflowInstance childTemplate : childWorkflows) {
Long rootWorkflowId = instance.rootWorkflowId == null ? instance.id : instance.rootWorkflowId;
WorkflowInstance childWorkflow = new WorkflowInstance.Builder(childTemplate).setRootWorkflowId(rootWorkflowId)
.setParentWorkflowId(instance.id).setParentActionId(parentActionId).build();
insertWorkflowInstance(childWorkflow);
}
for (WorkflowInstance workflow : workflows) {
insertWorkflowInstance(workflow);
}
}
});
}
public void recoverWorkflowInstancesFromDeadNodes() {
WorkflowInstanceAction.Builder builder = new WorkflowInstanceAction.Builder().setExecutionStart(now()).setExecutionEnd(now())
.setType(recovery).setStateText("Recovered");
for (InstanceInfo instance : getRecoverableInstances()) {
WorkflowInstanceAction action = builder.setState(instance.state).setWorkflowInstanceId(instance.id).build();
recoverWorkflowInstance(instance.id, action);
}
}
private List getRecoverableInstances() {
String sql = "select id, state from nflow_workflow where executor_id in (select id from nflow_executor where "
+ executorInfo.getExecutorGroupCondition() + " and id <> " + executorInfo.getExecutorId() + " and "
+ sqlVariants.dateLtEqDiff("expires", "current_timestamp") + ")";
return jdbc.query(sql, new RowMapper() {
@Override
public InstanceInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
InstanceInfo instance = new InstanceInfo();
instance.id = rs.getLong("id");
instance.state = rs.getString("state");
return instance;
}
});
}
private void recoverWorkflowInstance(final long instanceId, final WorkflowInstanceAction action) {
transaction.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
int executorId = executorInfo.getExecutorId();
int updated = jdbc.update("update nflow_workflow set executor_id = null, status = "
+ sqlVariants.workflowStatus(inProgress) + " where id = ? and executor_id in (select id from nflow_executor where "
+ executorInfo.getExecutorGroupCondition() + " and id <> " + executorId + " and "
+ sqlVariants.dateLtEqDiff("expires", "current_timestamp") + ")", instanceId);
if (updated > 0) {
insertWorkflowInstanceAction(action);
}
}
});
}
private void updateWorkflowInstanceWithCTE(WorkflowInstance instance, final WorkflowInstanceAction action,
Map changedStateVariables) {
int executorId = executorInfo.getExecutorId();
StringBuilder sqlb = new StringBuilder(256);
sqlb.append("with wf as (").append(updateWorkflowInstanceSql()).append(" returning id), ");
sqlb.append("act as (").append(insertWorkflowActionSql()).append(" select wf.id, ?, ").append(sqlVariants.actionType())
.append(", ?, ?, ?, ?, ? from wf returning id)");
// using sqlVariants.nextActivationUpdate() requires that nextActivation is added 3 times
Timestamp nextActivation = toTimestamp(instance.nextActivation);
Object[] fixedValues = new Object[] { instance.status.name(), instance.state,
abbreviate(instance.stateText, getInstanceStateTextLength()), nextActivation, nextActivation, nextActivation,
instance.status == executing ? executorId : null, instance.retries, toTimestamp(action.executionStart), instance.id,
executorId, action.type.name(), action.state, abbreviate(action.stateText, getActionStateTextLength()), action.retryNo,
toTimestamp(action.executionStart), toTimestamp(action.executionEnd) };
int pos = fixedValues.length;
Object[] args = Arrays.copyOf(fixedValues, pos + changedStateVariables.size() * 2);
for (Entry variable : changedStateVariables.entrySet()) {
sqlb.append(", ins").append(pos).append(" as (").append(insertWorkflowInstanceStateSql())
.append(" select wf.id,act.id,?,? from wf,act)");
args[pos++] = variable.getKey();
args[pos++] = variable.getValue();
}
sqlb.append(" select act.id from act");
jdbc.queryForObject(sqlb.toString(), Long.class, args);
}
String insertWorkflowActionSql() {
return "insert into nflow_workflow_action(workflow_id, executor_id, type, state, state_text, retry_no, execution_start, execution_end)";
}
private String updateWorkflowInstanceSql() {
return "update nflow_workflow set status = " + sqlVariants.workflowStatus() + ", state = ?, state_text = ?, "
+ "next_activation = " + sqlVariants.nextActivationUpdate()
+ ", external_next_activation = null, executor_id = ?, retries = ?, "
+ "started = (case when started is null then ? else started end) where id = ? and executor_id = "
+ executorInfo.getExecutorId();
}
public boolean updateNotRunningWorkflowInstance(WorkflowInstance instance) {
List vars = new ArrayList<>();
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy