Packages
mozart
0.9.0
1.0.1
1.0.0
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Mozart is an Elixir based BPM platform. It provides a powerful and highly readable DSL for implementing executable business process models.
Current section
Files
Jump to
Current section
Files
lib/mozart/process_engine.ex
defmodule Mozart.ProcessEngine do
@moduledoc """
A ProcessEngine is dynamically spawned for the purpose of executing a process model defined by **defprocess** function call.
"""
@doc false
use GenServer
require Logger
alias Mozart.ProcessService, as: PS
alias Mozart.Data.ProcessState
alias Mozart.Data.ExecutionFrame
alias Phoenix.PubSub
alias Ecto.UUID
alias Mozart.Task
## Client API
@doc false
def start_link(uid, process, data, business_key) do
{:ok, pid} = GenServer.start_link(__MODULE__, {uid, process, data, business_key})
{:ok, pid, {uid, business_key}}
end
@doc """
Used to complete any "complete-able" open tasks. Task execution frequently spawns new
open tasks. Execute will continue to called recursively as long as there are "complete-able" open tasks.
"""
def execute(ppid) do
GenServer.cast(ppid, :execute)
end
@doc false
def execute_and_wait(ppid) do
GenServer.call(ppid, :execute)
end
@doc """
Use this function to create a ProcessEngine instance initialized with the
name of the process model to be executed and any initialization data. The
engine will start executing tasks when the execute/1 function is called.
Arguments are a process model name, initial process data, an optional business_key, and a parent
process pid if there is a parent. If a process key is not specified, one will be assigned.
Returns a tuple of the form:
```
{:ok, ppid, uid, business_key}
```
where:
* **ppid** is the Elixir pid for the spawned GenServer.
* **uid** is a uniquie identifier for a process execution.
* **business_key** is a unique identifier for a hierarchial process execution.
Sample invocation:
```
{:ok, ppid, uid, business_key} = ProcessEngine.start_process("a process model name", )
```
"""
def start_process(process, data, business_key \\ nil) do
uid = UUID.generate()
business_key = business_key || UUID.generate()
child_spec = %{
id: MyProcessEngine,
start: {Mozart.ProcessEngine, :start_link, [uid, process, data, business_key]},
restart: :transient
}
{:ok, pid, {uid, business_key}} =
DynamicSupervisor.start_child(ProcessEngineSupervisor, child_spec)
{:ok, pid, uid, business_key}
end
@doc false
def restart_process(state) do
uid = state.uid
process = state.top_level_process
data = state.data
business_key = state.business_key
child_spec = %{
id: MyProcessEngine,
start: {Mozart.ProcessEngine, :start_link, [uid, process, data, business_key]},
restart: :transient
}
{:ok, pid, {uid, business_key}} =
DynamicSupervisor.start_child(ProcessEngineSupervisor, child_spec)
{:ok, pid, uid, business_key}
end
@doc false
def complete_on_task_exit_event(ppid) do
GenServer.cast(ppid, :complete_on_task_exit_event)
end
def get_completed_tasks(ppid) do
GenServer.call(ppid, :get_completed_tasks)
end
@doc """
Returns the state of the process. Useful for debugging.
"""
def get_state(ppid) do
GenServer.call(ppid, :get_state)
end
@doc false
def restore_previous_state(ppid, previous_state) do
GenServer.call(ppid, {:restore_previous_state, previous_state})
end
@doc false
def get_uid(ppid) do
GenServer.call(ppid, :get_uid)
end
@doc false
def get_model(ppid) do
GenServer.call(ppid, :get_model)
end
@doc false
def get_data(ppid) do
GenServer.call(ppid, :get_data)
end
@doc """
Gets the open tasks of the given process engine. Useful for debugging.
"""
def get_open_tasks(ppid) do
GenServer.call(ppid, :get_open_tasks)
end
@doc false
def get_all_open_tasks(ppid) do
GenServer.call(ppid, :get_all_open_tasks)
end
@doc false
def complete_user_task(ppid, task_uid, data) do
GenServer.call(ppid, {:complete_user_task, task_uid, data})
end
@doc false
def assign_user_task(ppid, task_uid, user_id) do
GenServer.cast(ppid, {:assign_user_task, task_uid, user_id})
end
@doc false
def complete_user_task_and_go(ppid, task_uid, data) do
GenServer.cast(ppid, {:complete_user_task, task_uid, data})
end
@doc false
def set_data(ppid, data) do
GenServer.cast(ppid, {:set_data, data})
end
@doc false
def is_complete(ppid) do
GenServer.call(ppid, :is_complete)
end
@doc false
def notify_child_complete(parent_pid, sub_process_name, data) do
GenServer.cast(parent_pid, {:notify_child_complete, sub_process_name, data})
end
## GenServer callbacks
@doc false
def init({uid, process, data, business_key}) do
pe_recovered_state = PS.get_cached_state(uid)
state =
pe_recovered_state ||
%ProcessState{
uid: uid,
data: data,
business_key: business_key,
top_level_process: process,
start_time: DateTime.utc_now(),
execution_frames: [%ExecutionFrame{process: process, data: data}]
}
# if pe_recovered_state do
# Logger.warning("Restart process instance [#{process}][#{uid}]")
# else
# Logger.info("Start process instance [#{process}][#{uid}]")
# end
Logger.info("Start process instance [#{process}][#{uid}]")
{:ok, state, {:continue, {:register_and_subscribe, uid}}}
end
@doc false
def handle_continue({:register_and_subscribe, uid}, state) do
PS.register_process_instance(uid, self(), state.business_key)
Phoenix.PubSub.subscribe(:pubsub, "pe_topic")
{:noreply, state}
end
def handle_call({:restore_previous_state, previous_state}, _from, state) do
state =
state
|> Map.put(:execution_frames, previous_state.execution_frames)
|> Map.put(:completed_tasks, previous_state.completed_tasks)
{:reply, state, state}
end
def handle_call(:is_complete, _from, state) do
{:reply, state.complete, state}
end
def handle_call(:get_all_open_tasks, _from, state) do
open_tasks = get_all_open_tasks_impl(state)
{:reply, open_tasks, state}
end
def handle_call(:get_completed_tasks, _from, state) do
{:reply, state.completed_tasks, state}
end
def handle_call(:get_state, _from, state) do
{:reply, state, state}
end
def handle_call(:get_uid, _from, state) do
{:reply, state.uid, state}
end
def handle_call(:get_data, _from, state) do
{:reply, state.data, state}
end
def handle_call(:get_open_tasks, _from, state) do
{:reply, get_open_tasks_impl(state), state}
end
def handle_call({:complete_user_task, task_uid, return_data}, _from, state) do
complete_user_task_impl(state, task_uid, return_data)
|> test_for_process_completion()
end
def handle_call(:execute, _from, state) do
model = PS.get_process_model(get_process_from_state(state))
create_next_tasks(state, model.initial_task)
|> execute_process()
|> test_for_process_completion()
end
def handle_cast(:execute, state) do
current_state = get_current_execution_frame(state)
model = PS.get_process_model(current_state.process)
create_next_tasks(state, model.initial_task)
|> execute_process()
|> test_for_process_completion()
end
def handle_cast({:complete_user_task, task_uid, return_data}, state) do
complete_user_task_impl(state, task_uid, return_data)
|> test_for_process_completion()
end
def handle_cast({:assign_user_task, task_uid, user_id}, state) do
task_instance = get_task_instance(task_uid, state)
task_instance = Map.put(task_instance, :assigned_user, user_id)
state = insert_open_task(state, task_instance)
{:noreply, state}
end
def handle_cast({:set_data, data}, state) do
{:noreply, Map.put(state, :data, data)}
end
def handle_info({:timer_expired, timer_task_uid}, state) do
open_tasks = get_open_tasks_impl(state)
timer_task = Map.get(open_tasks, timer_task_uid)
timer_task = Map.put(timer_task, :expired, true)
insert_open_task(state, timer_task)
|> execute_process()
|> test_for_process_completion()
end
def handle_info({:message, payload}, state) do
open_tasks =
Enum.into(get_open_tasks_impl(state), %{}, fn {uid, task} ->
if task.type == :receive,
do: {uid, update_receive_event_task(task, payload)},
else: {uid, task}
end)
set_open_tasks(state, open_tasks)
|> execute_process()
|> test_for_process_completion()
end
def handle_info({:exit_task_event, payload}, state) do
model = PS.get_process_model(get_process_from_state(state))
event = Enum.find(model.events, fn e -> apply(e.module, e.selector, [payload]) end)
state = if event, do: exit_task(event, state), else: state
{:noreply, state}
end
defp test_for_process_completion(state) do
if tl(state.execution_frames) == [] && !work_remaining(state) do
Logger.info(
"Exit process: process complete [#{get_process_from_state(state)}][#{state.uid}]"
)
{:stop, :shutdown, state}
else
{:noreply, state}
end
end
defp set_open_tasks(state, tasks) do
execution_frame = hd(state.execution_frames)
execution_frame = Map.put(execution_frame, :open_tasks, tasks)
Map.put(state, :execution_frames, [execution_frame | tl(state.execution_frames)])
end
defp exit_task(event, state) do
open_tasks = Map.values(get_open_tasks_impl(state))
task =
Enum.find(open_tasks, fn t -> t.name == event.exit_task end)
|> Map.put(:complete, :exit_on_task_event)
if task.type == :subprocess, do: complete_on_task_exit_event(task.subprocess_pid)
# TODO: Code exit repeat task
update_completed_task_state(state, task, event.next) |> execute_process()
end
def terminate(reason, state) do
if reason != :shutdown do
IO.puts("**************************************")
IO.puts("Process engine terminated with reason:")
IO.inspect(reason, label: "terminate reason")
IO.inspect(state, label: "terminate state")
IO.puts("**************************************")
PS.cache_pe_state(state.uid, state)
end
end
## callback utilities
defp update_receive_event_task(s_task, payload) do
select_result = apply(s_task.module, s_task.selector, [payload])
if select_result,
do: Map.put(s_task, :data, select_result) |> Map.put(:complete, true),
else: s_task
end
defp set_timer_for(timer_task, timer_duration) do
apply(timer_task.module, timer_task.function, [
timer_task.uid,
timer_task.process_uid,
timer_duration
])
end
defp insert_open_task(state, task) do
execution_frame = get_current_execution_frame(state)
open_tasks = execution_frame.open_tasks |> Map.put(task.uid, task)
execution_frame = Map.put(execution_frame, :open_tasks, open_tasks)
Map.put(state, :execution_frames, [execution_frame | tl(state.execution_frames)])
end
defp update_user_task(new_task, state) do
input_data =
if new_task.inputs,
do: Map.take(state.data, new_task.inputs),
else: state.data
new_task =
if new_task.listener,
do: apply(new_task.module, new_task.listener, [new_task, input_data]),
else: new_task
new_task =
Map.put(new_task, :data, input_data)
|> Map.put(:business_key, state.business_key)
|> Map.put(:top_level_process, state.top_level_process)
PS.insert_user_task(new_task)
new_task
end
defp trigger_repeat_execution(state, new_task) do
if apply(new_task.module, new_task.condition, [state.data]) do
first_task = get_new_task_instance(new_task.first, state)
process_new_task(state, first_task)
else
new_task = Map.put(new_task, :complete, true)
insert_open_task(state, new_task)
end
end
def create_next_tasks(state, next_task_name) do
new_task = get_new_task_instance(next_task_name, state)
process_new_task(state, new_task)
end
defp process_new_task(state, new_task) do
Logger.info("New #{new_task.type} task instance [#{new_task.name}][#{new_task.uid}]")
if new_task.type == :timer, do: set_timer_for(new_task, new_task.timer_duration)
if new_task.type == :send,
do: PubSub.broadcast(:pubsub, "pe_topic", {:message, new_task.message})
new_task =
if new_task.type == :user, do: update_user_task(new_task, state), else: new_task
state = insert_open_task(state, new_task)
state =
if new_task.type == :repeat,
do: trigger_repeat_execution(state, new_task),
else: state
state =
if new_task.type == :conditional,
do: trigger_conditional_execution(state, new_task),
else: state
if new_task.type == :subprocess, do: spawn_subprocess_task(new_task, state), else: state
end
defp get_task_def(task_name, process_name) do
model = PS.get_process_model(process_name)
get_task_def_from_model(task_name, model)
end
defp get_task_def_from_model(task_name, model) do
Enum.find(model.tasks, fn task -> task.name == task_name end)
end
defp get_new_task_instance(task_name, state) do
process_name = get_process_from_state(state)
get_task_def(task_name, process_name) |> initialize_new_task(state.uid)
end
defp initialize_new_task(task, process_uid) do
task
|> Map.put(:uid, Ecto.UUID.generate())
|> Map.put(:start_time, DateTime.utc_now())
|> Map.put(:process_uid, process_uid)
end
defp spawn_subprocess_task(new_subprocess_task, state) do
# get initial subprocess task
process_name = new_subprocess_task.process
model = PS.get_process_model(process_name)
initial_task_name = model.initial_task
initial_task =
get_task_def_from_model(initial_task_name, model)
|> initialize_new_task(state.uid)
initial_task =
if initial_task.type == :user, do: update_user_task(initial_task, state), else: initial_task
if initial_task.type == :timer, do: set_timer_for(initial_task, initial_task.timer_duration)
Logger.info(
"New #{initial_task.type} task instance [#{initial_task.name}][#{initial_task.uid}]"
)
new_execution_frame = %ExecutionFrame{
process: new_subprocess_task.process,
data: state.data,
parent_task_uid: new_subprocess_task.uid,
open_tasks: %{initial_task.uid => initial_task}
}
state = Map.put(state, :execution_frames, [new_execution_frame | state.execution_frames])
state =
if initial_task.type == :conditional,
do: trigger_conditional_execution(state, initial_task),
else: state
state =
if initial_task.type == :repeat,
do: trigger_repeat_execution(state, initial_task),
else: state
state
end
defp trigger_conditional_execution(state, new_task) do
if apply(new_task.module, new_task.condition, [state.data]) do
first_task = get_new_task_instance(new_task.first, state)
process_new_task(state, first_task)
else
new_task = Map.put(new_task, :complete, true)
insert_open_task(state, new_task)
end
end
defp get_current_execution_frame(state) do
state.execution_frames |> hd()
end
def process_next_task_list(state, [], _parent_name) do
state
end
def process_next_task_list(state, [task_name | rest], parent_name) do
state = create_next_tasks(state, task_name)
process_next_task_list(state, rest, parent_name)
end
defp get_open_tasks_impl(state) do
get_current_execution_frame(state) |> Map.get(:open_tasks)
end
defp get_all_open_tasks_impl(state) do
Enum.map(state.execution_frames, fn ex_state -> Map.values(ex_state.open_tasks) end)
|> List.flatten()
end
def update_for_completed_task(state, task) do
now = DateTime.utc_now()
duration = DateTime.diff(now, task.start_time, :microsecond)
task =
task
|> Map.put(:finish_time, now)
|> Map.put(:duration, duration)
execution_frames = delete_open_task_from_execution_frame(task, state)
completed_tasks = state.completed_tasks ++ [task]
state
|> Map.put(:completed_tasks, completed_tasks)
|> Map.put(:execution_frames, execution_frames)
|> check_for_repeat_task_completion(task)
|> check_for_conditional_task_completion(task)
end
defp delete_open_task_from_execution_frame(task, state) do
execution_frame = hd(state.execution_frames)
open_tasks = execution_frame.open_tasks
open_task = Enum.find_value(open_tasks, fn {_k, t} -> if t.name == task.name, do: t end)
if open_task do
open_tasks = Map.delete(open_tasks, open_task.uid)
execution_frame = Map.put(execution_frame, :open_tasks, open_tasks)
[execution_frame | tl(state.execution_frames)]
else
state.execution_frames
end
end
defp check_for_repeat_task_completion(state, task) do
r_task = find_repeat_task_by_last_task(state, task.name)
if r_task, do: trigger_repeat_execution(state, r_task), else: state
end
defp check_for_conditional_task_completion(state, task) do
c_task = find_conditional_task_by_last_task(state, task.name)
if c_task do
c_task = Map.put(c_task, :complete, true)
insert_open_task(state, c_task)
else
state
end
end
defp find_repeat_task_by_last_task(state, task_name) do
Enum.find_value(get_open_tasks_impl(state), fn {_key, t} ->
if t.type == :repeat && t.last == task_name, do: t
end)
end
defp find_conditional_task_by_last_task(state, task_name) do
Enum.find_value(get_open_tasks_impl(state), fn {_key, t} ->
if t.type == :conditional && t.last == task_name, do: t
end)
end
def update_completed_task_state(state, task, next_task) do
state = update_for_completed_task(state, task)
if next_task, do: create_next_tasks(state, next_task), else: state
end
def get_process_from_state(state) do
current_state = get_current_execution_frame(state)
current_state.process
end
def get_task_instance(task_uid, state), do: Map.get(get_open_tasks_impl(state), task_uid)
defp get_complete_able_task(state) do
Enum.find_value(get_open_tasks_impl(state), fn {_uid, task} ->
if Task.completable(task), do: task
end)
end
defp complete_user_task_impl(state, task_uid, return_data) do
task_instance = get_task_instance(task_uid, state)
if task_instance do
data = Map.merge(state.data, return_data)
state = Map.put(state, :data, data)
state = update_for_completed_task(state, task_instance)
state =
if task_instance.next,
do: create_next_tasks(state, task_instance.next),
else: state
Logger.info("Complete user task [#{task_instance.name}][#{task_instance.uid}]")
execute_process(state)
else
state
end
end
defp work_remaining(state) do
get_open_tasks_impl(state) != %{}
end
def execute_process(state) do
if work_remaining(state) do
complete_able_task = get_complete_able_task(state)
if complete_able_task do
Task.complete_task(complete_able_task, state)
else
PS.persist_process_state(state)
state
end
else
# No work remaining in current execution frame.
now = DateTime.utc_now()
state =
Map.put(state, :complete, true)
|> Map.put(:end_time, now)
|> Map.put(:execute_duration, DateTime.diff(now, state.start_time, :microsecond))
if tl(state.execution_frames) == [] do
# This is the top level BPM process, so exit Elixir process
PS.update_for_completed_process(state)
PS.delete_process_state(state)
state
else
# Finished a BPM subprocess execution frame. Pop the frame and resume execution.
update_execution_frame_stack(state) |> execute_process()
end
end
end
defp update_execution_frame_stack(state) do
new_execution_frames = tl(state.execution_frames)
completed_execution_frame = hd(state.execution_frames)
current_execution_frame = hd(new_execution_frames)
open_tasks = current_execution_frame.open_tasks
spawning_task_uid = completed_execution_frame.parent_task_uid
complete_subprocess_task = Map.get(open_tasks, spawning_task_uid) |> Map.put(:complete, true)
open_tasks =
Map.put(open_tasks, complete_subprocess_task.uid, complete_subprocess_task)
current_execution_frame =
Map.put(current_execution_frame, :open_tasks, open_tasks)
Map.put(state, :execution_frames, [current_execution_frame | tl(new_execution_frames)])
end
end