bpmn-elements
Version:
Executable workflow elements based on BPMN 2.0
931 lines (775 loc) • 27.5 kB
JavaScript
import { ProcessApi } from '../Api.js';
import { cloneContent, cloneMessage, pushParent } from '../messageHelper.js';
import { getUniqueId } from '../shared.js';
import { ActivityTracker } from '../Tracker.js';
export default ProcessExecution;
const kActivated = Symbol.for('activated');
const kActivityQ = Symbol.for('activityQ');
const kCompleted = Symbol.for('completed');
const kElements = Symbol.for('elements');
const kExecuteMessage = Symbol.for('executeMessage');
const kMessageHandlers = Symbol.for('messageHandlers');
const kParent = Symbol.for('parent');
const kStatus = Symbol.for('status');
const kStopped = Symbol.for('stopped');
const kTracker = Symbol.for('activity tracker');
function ProcessExecution(parentActivity, context) {
const { id, type, broker, isSubProcess, isTransaction } = parentActivity;
this[kParent] = parentActivity;
this.id = id;
this.type = type;
this.isSubProcess = isSubProcess;
this.isTransaction = isSubProcess && isTransaction;
this.broker = broker;
this.environment = context.environment;
this.context = context;
this[kElements] = {
postponed: new Set(),
children: context.getActivities(id),
associations: context.getAssociations(id),
flows: context.getSequenceFlows(id),
outboundMessageFlows: context.getMessageFlows(id),
startActivities: new Set(),
triggeredByEvent: new Set(),
detachedActivities: new Set(),
startSequences: {},
};
const exchangeName = (this._exchangeName = isSubProcess ? 'subprocess-execution' : 'execution');
broker.assertExchange(exchangeName, 'topic', { autoDelete: false, durable: true });
this[kCompleted] = false;
this[kStopped] = false;
this[kActivated] = false;
this[kStatus] = 'init';
this[kTracker] = new ActivityTracker(id);
this.executionId = undefined;
this[kMessageHandlers] = {
onActivityEvent: this._onActivityEvent.bind(this),
onApiMessage: this._onApiMessage.bind(this),
onChildMessage: this._onChildMessage.bind(this),
onMessageFlowEvent: this._onMessageFlowEvent.bind(this),
};
}
Object.defineProperties(ProcessExecution.prototype, {
stopped: {
get() {
return this[kStopped];
},
},
completed: {
get() {
return this[kCompleted];
},
},
status: {
get() {
return this[kStatus];
},
},
postponedCount: {
get() {
return this[kElements].postponed.size;
},
},
isRunning: {
get() {
return this[kActivated];
},
},
activityStatus: {
get() {
return this[kTracker].activityStatus;
},
},
});
ProcessExecution.prototype.execute = function execute(executeMessage) {
if (!executeMessage) throw new Error('Process execution requires message');
if (!executeMessage.content || !executeMessage.content.executionId) throw new Error('Process execution requires execution id');
const executionId = (this.executionId = executeMessage.content.executionId);
this[kExecuteMessage] = cloneMessage(executeMessage, {
executionId,
state: 'start',
});
this[kStopped] = false;
this.environment.assignVariables(executeMessage);
this[kActivityQ] = this.broker.assertQueue(`execute-${executionId}-q`, { durable: true, autoDelete: false });
if (executeMessage.fields.redelivered) {
return this.resume();
}
this._debug(`execute ${this.isSubProcess ? 'sub process' : 'process'}`);
this._activate();
this._start();
return true;
};
ProcessExecution.prototype.resume = function resume() {
this._debug(`resume process execution at ${this.status}`);
if (this[kCompleted]) return this._complete('completed');
this._activate();
const { startActivities, detachedActivities, postponed } = this[kElements];
if (startActivities.size > 1) {
for (const a of startActivities) a.shake();
}
postponed.clear();
detachedActivities.clear();
this[kActivityQ].consume(this[kMessageHandlers].onChildMessage, {
prefetch: 1000,
consumerTag: `_process-activity-${this.executionId}`,
});
if (this[kCompleted]) return;
const status = this.status;
if (status === 'init') return this._start();
const tracker = this[kTracker];
for (const msg of new Set(postponed)) {
const activity = this.getActivityById(msg.content.id);
if (!activity) continue;
if (msg.content.placeholder) continue;
if (!activity.status) {
this._popPostponed(msg.content);
msg.ack();
continue;
}
tracker.track(msg.fields.routingKey, msg);
activity.resume();
}
if (this[kCompleted]) return;
if (!postponed.size && status === 'executing') return this._complete('completed');
};
ProcessExecution.prototype.getState = function getState() {
const { children, flows, outboundMessageFlows, associations } = this[kElements];
const flowStates = flows.reduce((result, flow) => {
const elmState = flow.getState();
if (elmState) result.push(elmState);
return result;
}, []);
return {
executionId: this.executionId,
stopped: this[kStopped],
completed: this[kCompleted],
status: this.status,
children: children.reduce((result, activity) => {
if (activity.placeholder) return result;
const elmState = activity.getState();
if (elmState) result.push(elmState);
return result;
}, []),
...(flows.length && { flows: flowStates }),
...(outboundMessageFlows.length && {
messageFlows: outboundMessageFlows.map((f) => f.getState()).filter(Boolean),
}),
...(associations.length && { associations: associations.map((f) => f.getState()).filter(Boolean) }),
};
};
ProcessExecution.prototype.recover = function recover(state) {
if (!state) return this;
this.executionId = state.executionId;
this[kStopped] = state.stopped;
this[kCompleted] = state.completed;
this[kStatus] = state.status;
this._debug(`recover process execution at ${this.status}`);
if (state.messageFlows) {
for (const flowState of state.messageFlows) {
const flow = this._getMessageFlowById(flowState.id);
if (!flow) continue;
flow.recover(flowState);
}
}
if (state.associations) {
for (const associationState of state.associations) {
const association = this._getAssociationById(associationState.id);
if (!association) continue;
association.recover(associationState);
}
}
if (state.flows) {
for (const flowState of state.flows) {
const flow = this._getFlowById(flowState.id);
if (!flow) continue;
flow.recover(flowState);
}
}
if (state.children) {
for (const childState of state.children) {
const child = this.getActivityById(childState.id);
if (!child) continue;
child.recover(childState);
}
}
return this;
};
ProcessExecution.prototype.shake = function shake(fromId) {
let executing = true;
const id = this.id;
if (!this.isRunning) {
executing = false;
this.executionId = getUniqueId(id);
this._activate();
}
const toShake = fromId ? [this.getActivityById(fromId)].filter(Boolean) : this[kElements].startActivities;
const result = {};
this.broker.subscribeTmp(
'event',
'*.shake.*',
(routingKey, { content }) => {
let isLooped = false;
switch (routingKey) {
case 'flow.shake.loop':
isLooped = true;
case 'activity.shake.end': {
const { id: shakeId, parent: shakeParent } = content;
if (shakeParent.id !== id) return;
result[shakeId] = result[shakeId] || [];
result[shakeId].push({ ...content, isLooped });
break;
}
}
},
{ noAck: true, consumerTag: `_shaker-${this.executionId}` }
);
for (const a of toShake) a.shake();
if (!executing) this._deactivate();
this.broker.cancel(`_shaker-${this.executionId}`);
return result;
};
ProcessExecution.prototype.stop = function stop() {
this.getApi().stop();
};
ProcessExecution.prototype.getPostponed = function getPostponed(filterFn) {
const result = [];
for (const msg of this[kElements].postponed) {
const api = this._getChildApi(msg);
if (!api) continue;
if (filterFn && !filterFn(api)) continue;
result.push(api);
}
return result;
};
ProcessExecution.prototype.discard = function discard() {
this[kStatus] = 'discard';
return this[kActivityQ].queueMessage(
{ routingKey: 'execution.discard' },
{
id: this.id,
type: this.type,
executionId: this.executionId,
},
{ type: 'discard' }
);
};
ProcessExecution.prototype.cancel = function discard() {
return this[kActivityQ].queueMessage(
{ routingKey: 'execution.cancel' },
{
id: this.id,
type: this.type,
executionId: this.executionId,
},
{ type: 'cancel' }
);
};
ProcessExecution.prototype.getActivities = function getActivities() {
return this[kElements].children.slice();
};
ProcessExecution.prototype.getActivityById = function getActivityById(activityId) {
return this[kElements].children.find((child) => child.id === activityId);
};
ProcessExecution.prototype.getSequenceFlows = function getSequenceFlows() {
return this[kElements].flows.slice();
};
ProcessExecution.prototype.getAssociations = function getAssociations() {
return this[kElements].associations.slice();
};
ProcessExecution.prototype.getApi = function getApi(message) {
if (!message) return ProcessApi(this.broker, this[kExecuteMessage]);
const content = message.content;
if (content.executionId !== this.executionId) {
return this._getChildApi(message);
}
const api = ProcessApi(this.broker, message);
const postponed = this[kElements].postponed;
const self = this;
api.getExecuting = function getExecuting() {
const result = [];
for (const msg of postponed) {
const childApi = self._getChildApi(msg);
if (childApi) result.push(childApi);
}
return result;
};
return api;
};
ProcessExecution.prototype._start = function start() {
if (this[kElements].children.length === 0) {
return this._complete('completed');
}
this[kStatus] = 'start';
const executeContent = { ...this[kExecuteMessage].content, state: this.status };
this.broker.publish(this._exchangeName, 'execute.start', cloneContent(executeContent));
const { startActivities, postponed, detachedActivities } = this[kElements];
if (startActivities.size > 1) {
for (const a of startActivities) a.shake();
}
for (const a of startActivities) a.init();
this[kStatus] = 'executing';
for (const a of startActivities) a.run();
postponed.clear();
detachedActivities.clear();
this[kActivityQ].assertConsumer(this[kMessageHandlers].onChildMessage, {
prefetch: 1000,
consumerTag: `_process-activity-${this.executionId}`,
});
};
ProcessExecution.prototype._activate = function activate() {
const { onApiMessage, onMessageFlowEvent, onActivityEvent } = this[kMessageHandlers];
if (!this.isSubProcess) {
this.broker.consume('api-q', onApiMessage, {
noAck: true,
consumerTag: `_process-api-consumer-${this.executionId}`,
priority: 200,
});
} else {
this.broker.subscribeTmp('api', '#', onApiMessage, {
noAck: true,
consumerTag: `_process-api-consumer-${this.executionId}`,
priority: 200,
});
}
const { outboundMessageFlows, flows, associations, startActivities, triggeredByEvent, children } = this[kElements];
for (const flow of outboundMessageFlows) {
flow.activate();
flow.broker.subscribeTmp('event', '#', onMessageFlowEvent, {
consumerTag: '_process-message-consumer',
noAck: true,
priority: 200,
});
}
for (const flow of flows) {
flow.broker.subscribeTmp('event', '#', onActivityEvent, {
consumerTag: '_process-flow-controller',
noAck: true,
priority: 200,
});
}
for (const association of associations) {
association.broker.subscribeTmp('event', '#', onActivityEvent, {
consumerTag: '_process-association-controller',
noAck: true,
priority: 200,
});
}
startActivities.clear();
triggeredByEvent.clear();
for (const activity of children) {
if (activity.placeholder) continue;
activity.activate(this);
activity.broker.subscribeTmp('event', '#', onActivityEvent, {
noAck: true,
consumerTag: '_process-activity-consumer',
priority: 200,
});
if (activity.isStart) startActivities.add(activity);
if (activity.triggeredByEvent) triggeredByEvent.add(activity);
}
this[kActivated] = true;
};
ProcessExecution.prototype._deactivate = function deactivate() {
const broker = this.broker;
const executionId = this.executionId;
broker.cancel(`_process-api-consumer-${executionId}`);
broker.cancel(`_process-activity-${executionId}`);
const { children, flows, associations, outboundMessageFlows } = this[kElements];
for (const activity of children) {
if (activity.placeholder) continue;
activity.broker.cancel('_process-activity-consumer');
activity.deactivate();
}
for (const flow of flows) {
flow.broker.cancel('_process-flow-controller');
}
for (const association of associations) {
association.broker.cancel('_process-association-controller');
}
for (const flow of outboundMessageFlows) {
flow.deactivate();
flow.broker.cancel('_process-message-consumer');
}
this[kActivated] = false;
};
ProcessExecution.prototype._onDelegateEvent = function onDelegateEvent(message) {
const eventType = message.properties.type;
let delegate = true;
const content = message.content;
if (content.message?.id) {
this._debug(`delegate ${eventType} event with id <${content.message.id}>`);
} else {
this._debug(`delegate ${eventType} anonymous event`);
}
for (const activity of this[kElements].triggeredByEvent) {
if (activity.getStartActivities({ referenceId: content.message?.id, referenceType: eventType }).length) {
delegate = false;
activity.run(content.message);
}
}
this.getApi().sendApiMessage(eventType, content, { delegate: true });
return delegate;
};
ProcessExecution.prototype._onMessageFlowEvent = function onMessageFlowEvent(routingKey, message) {
this.broker.publish('message', routingKey, cloneContent(message.content), message.properties);
};
ProcessExecution.prototype._onActivityEvent = function onActivityEvent(routingKey, message) {
if (message.fields.redelivered && message.properties.persistent === false) return;
const content = message.content;
const parent = (content.parent = content.parent || {});
let delegate = message.properties.delegate;
const shaking = message.properties.type === 'shake';
const isDirectChild = content.parent.id === this.id;
if (isDirectChild) {
parent.executionId = this.executionId;
} else {
content.parent = pushParent(parent, { id: this.id, type: this.type, executionId: this.executionId });
}
if (delegate) delegate = this._onDelegateEvent(message);
this[kTracker].track(routingKey, message);
this.broker.publish('event', routingKey, content, { ...message.properties, delegate, mandatory: false });
if (shaking) return this._onShookEnd(message);
if (!isDirectChild) return;
switch (routingKey) {
case 'process.terminate':
return this[kActivityQ].queueMessage({ routingKey: 'execution.terminate' }, cloneContent(content), {
type: 'terminate',
persistent: true,
});
case 'activity.stop':
return;
}
this[kActivityQ].queueMessage(message.fields, cloneContent(content), { persistent: true, ...message.properties });
};
ProcessExecution.prototype._onChildMessage = function onChildMessage(routingKey, message) {
if (message.fields.redelivered && message.properties.persistent === false) return message.ack();
const content = message.content;
switch (routingKey) {
case 'execution.stop':
message.ack();
return this._stopExecution(message);
case 'execution.terminate':
message.ack();
return this._terminate(message);
case 'execution.discard':
message.ack();
return this._onDiscard(message);
case 'execution.discard.detached': {
message.ack();
for (const detached of this[kElements].detachedActivities) {
this._getChildApi(detached).discard();
}
return;
}
case 'execution.cancel':
message.ack();
return this._onCancel(message);
case 'activity.error.caught': {
let prevMsg;
for (const msg of this[kElements].postponed) {
if (msg.content.executionId === content.executionId) {
prevMsg = msg;
break;
}
}
if (!prevMsg) return message.ack();
break;
}
case 'flow.looped':
case 'activity.leave':
return this._onChildCompleted(message);
}
this._stateChangeMessage(message, true);
switch (routingKey) {
case 'activity.detach': {
this[kElements].detachedActivities.add(cloneMessage(message));
break;
}
case 'activity.cancel': {
if (this.isTransaction) this._onCancel(message);
break;
}
case 'activity.discard':
case 'activity.enter': {
if (!content.inbound) break;
for (const inbound of content.inbound) {
if (!inbound.isSequenceFlow && !inbound.isAssociation) continue;
const inboundMessage = this._popPostponed(inbound);
if (inboundMessage) inboundMessage.ack();
}
break;
}
case 'activity.error': {
let eventCaughtBy;
for (const msg of this[kElements].postponed) {
if (msg.fields.routingKey === 'activity.catch' && msg.content.source?.executionId === content.executionId) {
eventCaughtBy = msg;
break;
}
}
if (eventCaughtBy) {
this[kActivityQ].queueMessage({ routingKey: 'activity.error.caught' }, cloneContent(content), {
persistent: true,
...message.properties,
});
return this._debug('error was caught');
}
return this._complete('error', { error: content.error });
}
}
};
ProcessExecution.prototype._stateChangeMessage = function stateChangeMessage(message, postponeMessage) {
const previousMsg = this._popPostponed(message.content);
if (previousMsg) previousMsg.ack();
if (postponeMessage) this[kElements].postponed.add(message);
};
ProcessExecution.prototype._popPostponed = function popPostponed(byContent) {
const { postponed, detachedActivities } = this[kElements];
let postponedMsg;
if (byContent.sequenceId) {
for (const msg of postponed) {
if (!msg.content.isSequenceFlow && !msg.content.isAssociation) continue;
if (msg.content.sequenceId === byContent.sequenceId) {
postponedMsg = msg;
break;
}
}
} else {
for (const msg of postponed) {
if (msg.content.executionId === byContent.executionId) {
postponedMsg = msg;
break;
}
}
}
if (postponedMsg) postponed.delete(postponedMsg);
for (const msg of detachedActivities) {
if (msg.content.executionId === byContent.executionId) {
detachedActivities.delete(msg);
break;
}
}
return postponedMsg;
};
ProcessExecution.prototype._onChildCompleted = function onChildCompleted(message) {
this._stateChangeMessage(message, false);
if (message.fields.redelivered) return message.ack();
const { id, type, isEnd } = message.content;
const { postponed, detachedActivities, startActivities } = this[kElements];
const postponedCount = postponed.size;
if (!postponedCount) {
this._debug(`left <${id}> (${type}), pending runs ${postponedCount}`);
message.ack();
return this._complete('completed');
}
message.ack();
this._debug(`left <${id}> (${type}), pending activities ${postponedCount}`);
if (postponedCount && postponedCount === detachedActivities.size) {
return this[kActivityQ].queueMessage(
{ routingKey: 'execution.discard.detached' },
{
id: this.id,
type: this.type,
executionId: this.executionId,
},
{ type: 'cancel' }
);
}
if (isEnd && startActivities.size) {
const startSequences = this[kElements].startSequences;
for (const msg of postponed) {
const postponedId = msg.content.id;
const startSequence = startSequences[postponedId];
if (startSequence) {
if (startSequence.content.sequence.some(({ id: sid }) => sid === id)) {
this._getChildApi(msg).discard();
}
}
}
}
};
ProcessExecution.prototype._stopExecution = function stopExecution(message) {
const postponedCount = this.postponedCount;
this._debug(`stop process execution (stop child executions ${postponedCount})`);
if (postponedCount) {
for (const api of this.getPostponed()) api.stop();
}
this._deactivate();
this[kStopped] = true;
return this.broker.publish(
this._exchangeName,
`execution.stopped.${this.executionId}`,
{
...this[kExecuteMessage].content,
...(message && message.content),
},
{ type: 'stopped', persistent: false }
);
};
ProcessExecution.prototype._onDiscard = function onDiscard() {
this._deactivate();
const postponed = this[kElements].postponed;
const running = new Set(postponed);
postponed.clear();
this._debug(`discard process execution (discard child executions ${running.size})`);
if (this.isSubProcess) {
this.stop();
} else {
for (const flow of this.getSequenceFlows()) flow.stop();
for (const flow of this.getAssociations()) flow.stop();
for (const msg of running) this._getChildApi(msg).discard();
}
this[kActivityQ].purge();
return this._complete('discard');
};
ProcessExecution.prototype._onCancel = function onCancel() {
const postponed = this[kElements].postponed;
const running = new Set(postponed);
const isTransaction = this.isTransaction;
if (isTransaction) {
this._debug(`cancel transaction execution (cancel child executions ${running.size})`);
this[kStatus] = 'cancel';
this.broker.publish(
'event',
'transaction.cancel',
cloneMessage(this[kExecuteMessage], {
state: 'cancel',
})
);
for (const msg of running) {
if (msg.content.expect === 'compensate') {
this._getChildApi(msg).sendApiMessage('compensate');
} else if (!msg.content.isForCompensation) {
this._getChildApi(msg).discard();
}
}
} else {
this._debug(`cancel process execution (cancel child executions ${running.size})`);
for (const msg of running) {
this._getChildApi(msg).discard();
}
}
};
ProcessExecution.prototype._onApiMessage = function onApiMessage(routingKey, message) {
if (message.properties.delegate) {
return this._delegateApiMessage(routingKey, message);
}
if (this.id !== message.content.id) {
const child = this.getActivityById(message.content.id);
if (!child) return null;
return child.broker.publish('api', routingKey, message.content, message.properties);
}
if (this.executionId !== message.content.executionId) return;
switch (message.properties.type) {
case 'cancel':
return this.cancel(message);
case 'discard':
return this.discard(message);
case 'stop':
this[kActivityQ].queueMessage({ routingKey: 'execution.stop' }, cloneContent(message.content), { persistent: false });
break;
}
};
ProcessExecution.prototype._delegateApiMessage = function delegateApiMessage(routingKey, message, continueOnConsumed) {
const correlationId = message.properties.correlationId || getUniqueId(this.executionId);
this._debug(`delegate api ${routingKey} message to children, with correlationId <${correlationId}>`);
const broker = this.broker;
let consumed = false;
broker.subscribeTmp(
'event',
'activity.consumed',
(_, msg) => {
if (msg.properties.correlationId === correlationId) {
consumed = true;
this._debug(`delegated api message was consumed by ${msg.content ? msg.content.executionId : 'unknown'}`);
}
},
{ consumerTag: `_ct-delegate-${correlationId}`, noAck: true }
);
for (const child of this[kElements].children) {
if (child.placeholder) continue;
child.broker.publish('api', routingKey, cloneContent(message.content), message.properties);
if (consumed && !continueOnConsumed) break;
}
return broker.cancel(`_ct-delegate-${correlationId}`);
};
ProcessExecution.prototype._complete = function complete(completionType, content) {
this._deactivate();
this[kCompleted] = true;
const status = this.status;
switch (this.status) {
case 'cancel':
this._debug('process execution cancelled');
case 'discard':
completionType = status;
break;
case 'terminated':
break;
default:
this._debug(`process execution ${completionType}`);
this[kStatus] = completionType;
}
const broker = this.broker;
this[kActivityQ].delete();
return broker.publish(
this._exchangeName,
`execution.${completionType}.${this.executionId}`,
cloneContent(this[kExecuteMessage].content, {
output: { ...this.environment.output },
...content,
state: completionType,
}),
{ type: completionType, mandatory: completionType === 'error' }
);
};
ProcessExecution.prototype._terminate = function terminate(message) {
this[kStatus] = 'terminated';
this._debug('terminating process execution');
const postponed = this[kElements].postponed;
const running = new Set(postponed);
postponed.clear();
for (const flow of this.getSequenceFlows()) flow.stop();
for (const flow of this.getAssociations()) flow.stop();
for (const msg of running) {
const { id: postponedId, isSequenceFlow, isAssociation } = msg.content;
if (postponedId === message.content.id) continue;
if (isSequenceFlow || isAssociation) continue;
this._getChildApi(msg).stop();
msg.ack();
}
this[kActivityQ].purge();
};
ProcessExecution.prototype._getFlowById = function getFlowById(flowId) {
return this[kElements].flows.find((f) => f.id === flowId);
};
ProcessExecution.prototype._getAssociationById = function getAssociationById(associationId) {
return this[kElements].associations.find((a) => a.id === associationId);
};
ProcessExecution.prototype._getMessageFlowById = function getMessageFlowById(flowId) {
return this[kElements].outboundMessageFlows.find((f) => f.id === flowId);
};
ProcessExecution.prototype._getChildById = function getChildById(childId) {
return this.getActivityById(childId) || this._getFlowById(childId);
};
ProcessExecution.prototype._getChildApi = function getChildApi(message) {
const content = message.content;
let child = this._getChildById(content.id);
if (child) return child.getApi(message);
if (!content.parent) return;
child = this._getChildById(content.parent.id);
if (child) return child.getApi(message);
if (!content.parent.path) return;
for (const pp of content.parent.path) {
child = this._getChildById(pp.id, message);
if (child) return child.getApi(message);
}
};
ProcessExecution.prototype._onShookEnd = function onShookEnd(message) {
const routingKey = message.fields.routingKey;
if (routingKey !== 'activity.shake.end') return;
this[kElements].startSequences[message.content.id] = cloneMessage(message);
};
ProcessExecution.prototype._debug = function debugMessage(logMessage) {
this[kParent].logger.debug(`<${this.executionId} (${this.id})> ${logMessage}`);
};