/* * Copyright 2014 Effektif GmbH. * * 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 com.effektif.workflow.impl.workflowinstance; import com.effektif.workflow.api.Configuration; import com.effektif.workflow.api.WorkflowEngine; import com.effektif.workflow.api.model.TriggerInstance; import com.effektif.workflow.api.model.WorkflowInstanceId; import com.effektif.workflow.api.query.WorkflowInstanceQuery; import com.effektif.workflow.api.workflowinstance.TimerInstance; import com.effektif.workflow.api.workflowinstance.WorkflowInstance; import com.effektif.workflow.impl.WorkflowEngineImpl; import com.effektif.workflow.impl.WorkflowInstanceStore; import com.effektif.workflow.impl.activity.ActivityType; import com.effektif.workflow.impl.activity.types.SubProcessImpl; import com.effektif.workflow.impl.job.Job; import com.effektif.workflow.impl.util.Lists; import com.effektif.workflow.impl.util.Time; import com.effektif.workflow.impl.workflow.ActivityImpl; import com.effektif.workflow.impl.workflow.MultiInstanceImpl; import com.effektif.workflow.impl.workflow.WorkflowImpl; import org.joda.time.LocalDateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; import static com.effektif.workflow.impl.workflowinstance.ActivityInstanceImpl.*; /** * @author Tom Baeyens */ public class WorkflowInstanceImpl extends ScopeInstanceImpl { public static final Logger log = LoggerFactory.getLogger(WorkflowEngine.class); public WorkflowInstanceId id; public String businessKey; public LockImpl lock; public Queue<ActivityInstanceImpl> work; public Queue<ActivityInstanceImpl> workAsync; public WorkflowInstanceId callingWorkflowInstanceId; public String callingActivityInstanceId; public List<String> startActivityIds; public Boolean isAsync; public Long nextActivityInstanceId; public Long nextVariableInstanceId; public Long nextTimerInstanceId; public List<Job> jobs; public List<UnlockListener> unlockListeners; /** * local cache of the locked workflow instance for the purpose of the call * activity. in case the subprocess is fully synchronous and it finishes and * wants to continue the parent, that parent is already locked in the db. the * call activity will first check this cache to see if the workflow instance * is already locked and use this one instead of going to the db. */ public Map<WorkflowInstanceId, WorkflowInstanceImpl> lockedWorkflowInstances; public WorkflowInstanceImpl() { } public WorkflowInstanceImpl(Configuration configuration, WorkflowImpl workflow, WorkflowInstanceId workflowInstanceId, TriggerInstance triggerInstance, LockImpl lock, Map<String, Object> transientProperties) { this.id = workflowInstanceId; this.configuration = configuration; this.workflow = workflow; this.scope = workflow; this.workflowInstance = this; this.start = Time.now(); this.nextActivityInstanceId = 1l; this.nextVariableInstanceId = 1l; this.nextTimerInstanceId = 1l; this.businessKey = triggerInstance.getBusinessKey(); this.callingWorkflowInstanceId = triggerInstance.getCallingWorkflowInstanceId(); this.callingActivityInstanceId = triggerInstance.getCallingActivityInstanceId(); this.startActivityIds = triggerInstance.getStartActivityIds(); this.lock = lock; this.transientProperties = transientProperties; this.initializeVariableInstances(); } public WorkflowInstance toWorkflowInstance() { return toWorkflowInstance(false); } public WorkflowInstance toWorkflowInstance(boolean includeWorkState) { WorkflowInstance workflowInstance = new WorkflowInstance(); workflowInstance.setId(id); workflowInstance.setBusinessKey(businessKey); workflowInstance.setWorkflowId(workflow.id); workflowInstance.setCallingWorkflowInstanceId(callingWorkflowInstanceId); workflowInstance.setCallingActivityInstanceId(callingActivityInstanceId); if (jobs != null) { List<TimerInstance> timerInstances = new ArrayList<>(); for (Job job : jobs) { TimerInstance timerInstance = new TimerInstance(); timerInstance.setDueDate(job.getDueDate()); timerInstances.add(timerInstance); } workflowInstance.setJobs(timerInstances); } toScopeInstance(workflowInstance, includeWorkState); return workflowInstance; } public static List<WorkflowInstance> toWorkflowInstances(List<WorkflowInstanceImpl> workflowInstanceImpls) { if (workflowInstanceImpls == null) { return null; } List<WorkflowInstance> workflowInstances = new ArrayList<>(); for (WorkflowInstanceImpl workflowInstance : workflowInstanceImpls) { workflowInstances.add(workflowInstance.toWorkflowInstance()); } return workflowInstances; } public WorkflowInstance executeWork() { boolean isFirst = true; while (hasWork()) { ActivityInstanceImpl activityInstance = getNextWork(); ActivityImpl activity = activityInstance.getActivity(); ActivityType activityType = activity.activityType; // in the first iteration, the updates will be empty and hence no updates // will be flushed if (isFirst || activityType.isFlushSkippable()) { isFirst = false; } else { flushDbUpdates(); } if (STATE_STARTING.equals(activityInstance.workState)) { if (log.isDebugEnabled()) log.debug("Starting " + activityInstance); activityInstance.execute(); } else if (STATE_STARTING_MULTI_INSTANCE.equals(activityInstance.workState)) { if (log.isDebugEnabled()) log.debug("Starting multi instance " + activityInstance); activityInstance.execute(); } else if (STATE_STARTING_MULTI_CONTAINER.equals(activityInstance.workState)) { Collection<Object> values = null; MultiInstanceImpl multiInstance = activityType.getMultiInstance(); if (multiInstance != null && multiInstance.valuesBindings != null) { Object value = activityInstance.getValues(multiInstance.valuesBindings); if (value != null) { if (value instanceof Collection) { values = (Collection<Object>) value; } else { values = Lists.of(value); } } } if (values != null) { if (log.isDebugEnabled()) { log.debug("Starting multi instance container " + activityInstance); } for (Object element : values) { if (element!=null) { ActivityInstanceImpl elementActivityInstance = activityInstance.createActivityInstance(activity); elementActivityInstance.setWorkState(STATE_STARTING_MULTI_INSTANCE); elementActivityInstance.initializeForEachElement(multiInstance.elementVariable, element); } } } else { if (log.isDebugEnabled()) { log.debug("Skipping empty multi instance container " + activityInstance); } activityInstance.onwards(); } } else if (STATE_PROPAGATE_TO_PARENT.equals(activityInstance.workState)) { if (log.isDebugEnabled()) { log.debug("Propagating end of " + activityInstance + " to parent " + activityInstance.parent); } activityInstance.parent.activityInstanceEnded(activityInstance); activityInstance.workState = null; } else if (activityInstance.workState == null) { if (log.isDebugEnabled()) { log.debug("Activity instance " + activityInstance + " is completely done"); } } } WorkflowInstance workflowInstanceSnapshot = workflowInstance.toWorkflowInstance(); if (hasAsyncWork()) { if (log.isDebugEnabled()) log.debug("Going asynchronous " + this); flushDbUpdates(); Runnable asyncContinuation = new Runnable() { public void run() { try { work = workAsync; workAsync = null; isAsync = true; if (updates != null) { getUpdates().isWorkChanged = true; getUpdates().isAsyncWorkChanged = true; } executeWork(); } catch (Throwable e) { log.error("in workflow execution", e); } } }; WorkflowEngineImpl workflowEngine = configuration.get(WorkflowEngineImpl.class); workflowEngine.executeAsync(asyncContinuation); } else { WorkflowInstanceStore workflowInstanceStore = configuration.get(WorkflowInstanceStore.class); workflowInstanceStore.flushAndUnlock(this); workflow.workflowEngine.notifyUnlocked(this); } return workflowInstanceSnapshot; } public void cancel() { super.cancel(); if (updates!=null) { getUpdates().isActivityInstancesChanged = true; getUpdates().isEndStateChanged = true; getUpdates().isEndChanged = true; WorkflowInstanceStore workflowInstanceStore = configuration.get(WorkflowInstanceStore.class); workflowInstanceStore.flushAndUnlock(this); } } protected void flushDbUpdates() { workflow.workflowEngine.notifyFlush(this); WorkflowInstanceStore workflowInstanceStore = configuration.get(WorkflowInstanceStore.class); workflowInstanceStore.flush(this); } public void addLockedWorkflowInstance(WorkflowInstanceImpl lockedWorkflowInstance) { if (lockedWorkflowInstances == null) { lockedWorkflowInstances = new HashMap<>(); } lockedWorkflowInstances.put(lockedWorkflowInstance.getId(), lockedWorkflowInstance); } /** * Notifies event listeners tha the workflow instance has finished execution. */ public void workflowInstanceEnded() { workflow.workflowEngine.notifyWorkflowInstanceEnded(workflowInstance); destroyScopeInstance(); if (callingWorkflowInstanceId != null) { WorkflowInstanceImpl callingWorkflowInstance = getLockedWorkflowInstance(callingWorkflowInstanceId); final ActivityInstanceImpl callingActivityInstance = callingWorkflowInstance.findActivityInstance(callingActivityInstanceId); if (log.isDebugEnabled()) log.debug("Notifying calling activity instance " + callingActivityInstance); ActivityImpl activityDefinition = callingActivityInstance.getActivity(); final SubProcessImpl callActivity = (SubProcessImpl) activityDefinition.activityType; callActivity.calledWorkflowInstanceEnded(callingActivityInstance, this); } } public WorkflowInstanceImpl getLockedWorkflowInstance(WorkflowInstanceId workflowInstanceId) { WorkflowInstanceImpl callingWorkflowInstance = null; if (lockedWorkflowInstances != null) { // the lockedWorkflowInstances is a local cache of the locked workflow // instances which is passed down to the sub workflow instance in the // call activity. In case the subprocess is fully synchronous and it // finishes and wants to continue the parent, that parent is already // locked in the db. the call activity will first check this cache to // see if the workflow instance is already locked and use this one // instead of going to the db. callingWorkflowInstance = lockedWorkflowInstances.get(workflowInstanceId); } if (callingWorkflowInstance == null) { WorkflowEngineImpl workflowEngine = configuration.get(WorkflowEngineImpl.class); callingWorkflowInstance = workflowEngine.lockWorkflowInstanceWithRetry(workflowInstance.callingWorkflowInstanceId); if (callingWorkflowInstance == null) { log.error("Couldn't continue calling activity instance after workflow instance completion"); } } return callingWorkflowInstance; } public void addWork(ActivityInstanceImpl activityInstance) { if (isWorkAsync(activityInstance)) { addAsyncWork(activityInstance); } else { addSyncWork(activityInstance); } } protected boolean isWorkAsync(ActivityInstanceImpl activityInstance) { // if this workflow instance is already running in an async thread, // the new work should be done sync in this thread. if (Boolean.TRUE.equals(isAsync)) { return false; } if (!ActivityInstanceImpl.START_WORKSTATES.contains(activityInstance.workState)) { return false; } return activityInstance.getActivity().activityType.isAsync(activityInstance); } protected void addSyncWork(ActivityInstanceImpl activityInstance) { if (work == null) { work = new LinkedList<>(); } work.add(activityInstance); if (updates != null) { getUpdates().isWorkChanged = true; } } protected void addAsyncWork(ActivityInstanceImpl activityInstance) { if (workAsync == null) { workAsync = new LinkedList<>(); } workAsync.add(activityInstance); if (updates != null) { getUpdates().isAsyncWorkChanged = true; } } public ActivityInstanceImpl getNextWork() { ActivityInstanceImpl nextWork = work != null ? work.poll() : null; if (nextWork != null && updates != null) { getUpdates().isWorkChanged = true; } return nextWork; } public boolean hasAsyncWork() { return workAsync != null && !workAsync.isEmpty(); } public boolean hasWork() { return work != null && !work.isEmpty(); } /** * Instructs the engine to propagate execution forwards after ending the current activity instance. */ public void endAndPropagateToParent() { if (this.end == null) { if (hasOpenActivityInstances()) { throw new RuntimeException("Can't end this process instance. There are open activity instances: " + this); } setEnd(Time.now()); if (log.isDebugEnabled()) { log.debug("Ends " + this); } workflowInstanceEnded(); } } public String toString() { return "(" + ((workflow.name != null ? workflow.name + "|" : workflow.sourceWorkflowId != null ? workflow.sourceWorkflowId + "|" : "")) + (id != null ? id.toString() : Integer.toString(System.identityHashCode(this))) + ")"; } public void removeLock() { setLock(null); if (updates != null) { getUpdates().isLockChanged = true; } } public void setLock(LockImpl lock) { this.lock = lock; if (updates != null) { getUpdates().isLockChanged = true; } } public void setEnd(LocalDateTime end) { this.end = end; if (start != null && end != null) { this.duration = end.toDate().getTime() - start.toDate().getTime(); } if (updates != null) { getUpdates().isEndChanged = true; } } @Override public void setProperty(String key, Object value) { super.setProperty(key, value); if (updates != null) { getUpdates().isPropertiesChanged = true; } } @Override public void setPropertyOpt(String key, Object value) { getUpdates().isPropertiesChanged = true; super.setPropertyOpt(key, value); } @Override public void setProperties(Map<String, Object> properties) { getUpdates().isPropertiesChanged = true; super.setProperties(properties); } @Override public Object removeProperty(String key) { getUpdates().isPropertiesChanged = true; return super.removeProperty(key); } /** getter for casting convenience */ @Override public WorkflowInstanceUpdates getUpdates() { return (WorkflowInstanceUpdates) updates; } @Override public boolean isWorkflowInstance() { return true; } public void trackUpdates(boolean isNew) { if (updates == null) { updates = new WorkflowInstanceUpdates(isNew); } else { updates.reset(isNew); } super.trackUpdates(isNew); } /*** * isIncluded * * @param query * , with any combination of ActivityId and WorkflowInstanceId set or * not set. When set, the value is taken into account, otherwise it * is ignored. If both ActivityId and WorkflowInstanceId are null * (empty query), true is returned */ public boolean isIncluded(WorkflowInstanceQuery query) { if (query.getActivityId() == null && query.getWorkflowInstanceId() == null) return true; if (query.getWorkflowInstanceId() != null && query.getWorkflowInstanceId().equals(id)) { return true; } if (query.getActivityId() != null && hasActivityInstances()) { for (ActivityInstanceImpl activityInstance : activityInstances) { if (activityInstance.activity.getId().equals(query.getActivityId()) && !activityInstance.isEnded()) { return true; } } } return false; } public String generateNextActivityInstanceId() { if (updates != null) { getUpdates().isNextActivityInstanceIdChanged = true; } return Long.toString(nextActivityInstanceId++); } public String generateNextVariableInstanceId() { if (updates != null) { getUpdates().isNextVariableInstanceIdChanged = true; } return Long.toString(nextVariableInstanceId++); } public void addJob(Job job) { if (jobs == null) { jobs = new ArrayList<>(); } jobs.add(job); if (updates != null) { getUpdates().isJobsChanged = true; } } public void removeJob(Job job) { if (jobs != null) { jobs.remove(job); } if (updates != null) { getUpdates().isJobsChanged = true; } } public WorkflowInstanceId getId() { return this.id; } public String getEndState() { return endState; } public void addUnlockListener(UnlockListener unlockListener) { if (unlockListeners==null) { unlockListeners = new ArrayList<>(); } unlockListeners.add(unlockListener); } public void notifyUnlockListeners() { if (unlockListeners!=null) { WorkflowEngineImpl workflowEngine = configuration.get(WorkflowEngineImpl.class); for (final UnlockListener unlockListener: unlockListeners) { workflowEngine.executeAsync(new Runnable() { @Override public void run() { unlockListener.unlocked(WorkflowInstanceImpl.this); } }); } } } }