Current section
Files
Jump to
Current section
Files
lib/flow/materialize.ex
alias Experimental.{GenStage, Flow}
defmodule Flow.Materialize do
@moduledoc false
@compile :inline_list_funcs
@map_reducer_opts [:buffer_keep, :buffer_size, :dispatcher]
@dispatcher_opts [:hash]
def materialize(%{producers: nil}, _, _, _) do
raise ArgumentError, "cannot execute a flow without producers, " <>
"please call \"from_enumerable\" or \"from_stage\" accordingly"
end
def materialize(%{operations: operations, options: options, producers: producers, window: window},
start_link, type, type_options) do
options = Keyword.merge(type_options, options)
ops = split_operations(operations)
{producers, consumers, ops, window} = start_producers(producers, ops, start_link, window, options)
{producers, start_stages(ops, window, consumers, start_link, type, options)}
end
## Helpers
@doc """
Splits the flow operations into layers of stages.
"""
def split_operations([]) do
:none
end
def split_operations(operations) do
split_operations(:lists.reverse(operations), :mapper, [])
end
defp split_operations([{:mapper, _, _} = op | ops], :mapper, acc_ops) do
split_operations(ops, :mapper, [op | acc_ops])
end
defp split_operations([op | ops], _type, acc_ops) do
split_operations(ops, :reducer, [op | acc_ops])
end
defp split_operations([], :mapper, ops) do
{:mapper, mapper_ops(ops), :lists.reverse(ops)}
end
defp split_operations([], :reducer, ops) do
ops = :lists.reverse(ops)
{:reducer, reducer_ops(ops), ops}
end
defp start_stages(:none, window, producers, _start_link, _type, _options) do
if window != Flow.Window.global do
raise ArgumentError, "a window was set but no computation is happening on this partition"
end
producers
end
defp start_stages({_mr, compiled_ops, _ops}, window, producers, start_link, type, opts) do
{acc, reducer, trigger} = window_ops(window, compiled_ops)
{stages, opts} = Keyword.pop(opts, :stages)
{init_opts, subscribe_opts} = Keyword.split(opts, @map_reducer_opts)
for i <- 0..stages-1 do
subscriptions =
for {producer, producer_opts} <- producers do
{producer, [partition: i] ++ Keyword.merge(subscribe_opts, producer_opts)}
end
arg = {type, [subscribe_to: subscriptions] ++ init_opts, {i, stages}, trigger, acc, reducer}
{:ok, pid} = start_link.(Flow.MapReducer, arg, [])
{pid, []}
end
end
## Producers
defp start_producers({:join, kind, left, right, left_key, right_key, join},
ops, start_link, window, options) do
partitions = Keyword.fetch!(options, :stages)
{left_producers, left_consumers} = start_join(:left, left, left_key, partitions, start_link)
{right_producers, right_consumers} = start_join(:right, right, right_key, partitions, start_link)
{type, {acc, fun, trigger}, ops} = ensure_ops(ops)
window =
case window do
%{by: by} -> %{window | by: fn x -> by.(elem(x, 1)) end}
%{} -> window
end
{left_producers ++ right_producers,
left_consumers ++ right_consumers,
{type, join_ops(kind, join, acc, fun, trigger), ops},
window}
end
defp start_producers({:departition, flow, acc_fun, merge_fun, done_fun},
ops, start_link, window, options) do
{producers, consumers} = materialize(flow, start_link, :producer_consumer, options)
{type, {acc, fun, trigger}, ops} = ensure_ops(ops)
stages = Keyword.fetch!(flow.options, :stages)
partitions = Enum.to_list(0..stages-1)
{producers, consumers,
{type, departition_ops(acc, fun, trigger, partitions, acc_fun, merge_fun, done_fun), ops},
window}
end
defp start_producers({:flows, flows}, ops, start_link, window, options) do
options = partition(options)
{producers, consumers} =
Enum.reduce(flows, {[], []}, fn flow, {producers_acc, consumers_acc} ->
{producers, consumers} = materialize(flow, start_link, :producer_consumer, options)
{producers ++ producers_acc, consumers ++ consumers_acc}
end)
{producers, consumers, ensure_ops(ops), window}
end
defp start_producers({:stages, producers}, ops, _start_link, window, options) do
producers = for producer <- producers, do: {producer, []}
# If there are no more stages and there is a need for a custom
# dispatcher, we need to wrap the sources in a custom stage.
if Keyword.has_key?(options, :dispatcher) do
{producers, producers, ensure_ops(ops), window}
else
{producers, producers, ops, window}
end
end
defp start_producers({:enumerables, enumerables}, ops, start_link, window, options) do
# options configures all stages before partition, so it effectively
# controls the number of stages consuming the enumerables.
stages = Keyword.fetch!(options, :stages)
case ops do
{:mapper, _compiled_ops, mapper_ops} when stages < length(enumerables) ->
# Fuse mappers into enumerables if we have more enumerables than stages.
producers = start_enumerables(enumerables, mapper_ops, partition(options), start_link)
{producers, producers, :none, window}
:none ->
# If there are no ops, just start the enumerables with the options.
producers = start_enumerables(enumerables, [], options, start_link)
{producers, producers, :none, window}
_ ->
# Otherwise it is a regular producer consumer with demand dispatcher.
# In this case, options is used by subsequent mapper/reducer stages.
producers = start_enumerables(enumerables, [], [], start_link)
{producers, producers, ops, window}
end
end
defp start_enumerables(enumerables, ops, opts, start_link) do
opts = [consumers: :permanent, demand: :accumulate] ++ Keyword.take(opts, @map_reducer_opts)
for enumerable <- enumerables do
stream =
:lists.foldl(fn {:mapper, fun, args}, acc ->
apply(Stream, fun, [acc | args])
end, enumerable, ops)
{:ok, pid} = start_link.(GenStage.Streamer, {stream, opts}, opts)
{pid, []}
end
end
defp partition(options) do
stages = Keyword.fetch!(options, :stages)
dispatcher_opts = [partitions: 0..stages-1] ++ Keyword.take(options, @dispatcher_opts)
[dispatcher: {GenStage.PartitionDispatcher, dispatcher_opts}]
end
defp ensure_ops(:none),
do: {:mapper, mapper_ops([]), []}
defp ensure_ops(ops),
do: ops
## Departition
defp departition_ops(acc, fun, trigger, partitions, acc_fun, merge_fun, done_fun) do
acc = fn -> {acc.(), %{}} end
events = fn ref, events, {acc, windows}, index ->
{events, windows} = dispatch_departition(events, windows, partitions, acc_fun, merge_fun, done_fun)
{events, acc} = fun.(ref, :lists.reverse(events), acc, index)
{events, {acc, windows}}
end
trigger = fn {acc, windows}, index, op, name ->
{events, acc} = trigger.(acc, index, op, name)
{events, {acc, windows}}
end
{acc, events, trigger}
end
defp dispatch_departition(events, windows, partitions, acc_fun, merge_fun, done_fun) do
:lists.foldl(fn {state, partition, {_, window, name}}, {events, windows} ->
{partitions, acc} = get_window_data(windows, window, partitions, acc_fun)
partitions = remove_partition_on_done(name, partitions, partition)
acc = merge_fun.(state, acc)
case partitions do
[] ->
{[done_fun.(acc) | events], Map.delete(windows, window)}
_ ->
{events, Map.put(windows, window, {partitions, acc})}
end
end, {[], windows}, events)
end
defp remove_partition_on_done(:done, partitions, partition) do
List.delete(partitions, partition)
end
defp remove_partition_on_done(_, partitions, _) do
partitions
end
defp get_window_data(windows, window, partitions, acc_fun) do
case windows do
%{^window => value} -> value
%{} -> {partitions, acc_fun.()}
end
end
## Joins
defp start_join(side, flow, key_fun, stages, start_link) do
hash = fn event ->
key = key_fun.(event)
{{key, event}, :erlang.phash2(key, stages)}
end
opts = [dispatcher: {GenStage.PartitionDispatcher, partitions: 0..stages-1, hash: hash}]
{producers, consumers} = materialize(flow, start_link, :producer_consumer, opts)
{producers,
for {consumer, consumer_opts} <- consumers do
{consumer, [tag: side] ++ consumer_opts}
end}
end
defp join_ops(kind, join, acc, fun, trigger) do
acc = fn -> {%{}, %{}, acc.()} end
events = fn ref, events, {left, right, acc}, index ->
{events, left, right} = dispatch_join(events, Process.get(ref), left, right, join, [])
{events, acc} = fun.(ref, events, acc, index)
{events, {left, right, acc}}
end
ref = make_ref()
trigger = fn
{left, right, acc}, index, op, {_, _, :done} = name ->
{kind_events, acc} =
case kind do
:inner ->
{[], acc}
:left_outer ->
fun.(ref, left_events(Map.keys(left), Map.keys(right), left, join), acc, index)
:right_outer ->
fun.(ref, right_events(Map.keys(right), Map.keys(left), right, join), acc, index)
:full_outer ->
left_keys = Map.keys(left)
right_keys = Map.keys(right)
{left_events, acc} = fun.(ref, left_events(left_keys, right_keys, left, join), acc, index)
{right_events, acc} = fun.(ref, right_events(right_keys, left_keys, right, join), acc, index)
{left_events ++ right_events, acc}
end
{trigger_events, acc} = trigger.(acc, index, op, name)
{kind_events ++ trigger_events, {left, right, acc}}
{left, right, acc}, index, op, name ->
{events, acc} = trigger.(acc, index, op, name)
{events, {left, right, acc}}
end
{acc, events, trigger}
end
defp left_events(left, right, source, join) do
for key <- left -- right, entry <- Map.fetch!(source, key), do: join.(entry, nil)
end
defp right_events(right, left, source, join) do
for key <- right -- left, entry <- Map.fetch!(source, key), do: join.(nil, entry)
end
defp dispatch_join([{key, left} | rest], :left, left_acc, right_acc, join, acc) do
acc =
case right_acc do
%{^key => rights} ->
:lists.foldl(fn right, acc -> [join.(left, right) | acc] end, acc, rights)
%{} -> acc
end
left_acc = Map.update(left_acc, key, [left], &[left | &1])
dispatch_join(rest, :left, left_acc, right_acc, join, acc)
end
defp dispatch_join([{key, right} | rest], :right, left_acc, right_acc, join, acc) do
acc =
case left_acc do
%{^key => lefties} ->
:lists.foldl(fn left, acc -> [join.(left, right) | acc] end, acc, lefties)
%{} -> acc
end
right_acc = Map.update(right_acc, key, [right], &[right | &1])
dispatch_join(rest, :right, left_acc, right_acc, join, acc)
end
defp dispatch_join([], _, left_acc, right_acc, _join, acc) do
{:lists.reverse(acc), left_acc, right_acc}
end
## Windows
defp window_ops(%{trigger: trigger, periodically: periodically} = window,
{reducer_acc, reducer_fun, reducer_trigger}) do
{window_acc, window_fun, window_trigger} =
window_trigger(trigger, reducer_acc, reducer_fun, reducer_trigger)
{type_acc, type_fun, type_trigger} =
window.__struct__.materialize(window, window_acc, window_fun, window_trigger)
{window_periodically(type_acc, periodically), type_fun, type_trigger}
end
defp window_trigger(nil, reducer_acc, reducer_fun, reducer_trigger) do
{reducer_acc, reducer_fun, reducer_trigger}
end
defp window_trigger({punctuation_acc, punctuation_fun},
reducer_acc, reducer_fun, reducer_trigger) do
{fn -> {punctuation_acc.(), reducer_acc.()} end,
build_punctuated_reducer(punctuation_fun, reducer_fun, reducer_trigger),
build_punctuated_trigger(reducer_trigger)}
end
defp build_punctuated_reducer(punctuation_fun, red_fun, trigger) do
fn ref, events, {pun_acc, red_acc}, index, name ->
maybe_punctuate(ref, events, punctuation_fun, pun_acc, red_acc, red_fun, index, name, trigger, [])
end
end
defp build_punctuated_trigger(trigger) do
fn {trigger_acc, red_acc}, index, op, name ->
{events, red_acc} = trigger.(red_acc, index, op, name)
{events, {trigger_acc, red_acc}}
end
end
defp maybe_punctuate(ref, events, punctuation_fun, pun_acc, red_acc,
red_fun, index, name, trigger, collected) do
case punctuation_fun.(events, pun_acc) do
{:trigger, trigger_name, pre, op, pos, pun_acc} ->
{red_events, red_acc} = red_fun.(ref, pre, red_acc, index)
{trigger_events, red_acc} = trigger.(red_acc, index, op, put_elem(name, 2, trigger_name))
maybe_punctuate(ref, pos, punctuation_fun, pun_acc, red_acc,
red_fun, index, name, trigger, collected ++ trigger_events ++ red_events)
{:cont, pun_acc} ->
{red_events, red_acc} = red_fun.(ref, events, red_acc, index)
{collected ++ red_events, {pun_acc, red_acc}}
end
end
defp window_periodically(window_acc, []) do
window_acc
end
defp window_periodically(window_acc, periodically) do
fn ->
for {time, keep_or_reset, name} <- periodically do
{:ok, _} = :timer.send_interval(time, self(), {:trigger, keep_or_reset, name})
end
window_acc.()
end
end
## Reducers
defp reducer_ops(ops) do
case take_mappers(ops, []) do
{mappers, [{:reduce, reducer_acc, reducer_fun} | ops]} ->
{reducer_acc, build_reducer(mappers, reducer_fun), build_trigger(ops, reducer_acc)}
{mappers, [{:uniq, uniq_by} | ops]} ->
{acc, reducer, trigger} = reducer_ops(ops)
{fn -> {%{}, acc.()} end,
build_uniq_reducer(mappers, reducer, uniq_by),
build_uniq_trigger(trigger)}
{mappers, ops} ->
{fn -> [] end, build_reducer(mappers, &[&1 | &2]), build_trigger(ops, fn -> [] end)}
end
end
defp build_reducer(mappers, fun) do
reducer = :lists.foldl(&mapper/2, fun, mappers)
fn _ref, events, acc, _index ->
{[], :lists.foldl(reducer, acc, events)}
end
end
@protocol_undefined "if you would like to emit a modified state from flow, like " <>
"a counter or a custom data-structure, please call Flow.emit/2 accordingly"
defp build_trigger(ops, acc_fun) do
map_states = merge_map_state(ops)
fn acc, index, op, name ->
events = :lists.foldl(& &1.(&2, index, name), acc, map_states)
try do
Enum.to_list(events)
rescue
e in Protocol.UndefinedError ->
msg = @protocol_undefined
e = update_in e.description, fn
"" -> msg
dc -> dc <> " (#{msg})"
end
reraise e, System.stacktrace
else
events ->
case op do
:keep -> {events, acc}
:reset -> {events, acc_fun.()}
end
end
end
end
defp build_uniq_reducer(mappers, reducer, uniq_by) do
uniq_by = :lists.foldl(&mapper/2, uniq_by_reducer(uniq_by), mappers)
fn ref, events, {set, acc}, index ->
{set, events} = :lists.foldl(uniq_by, {set, []}, events)
{events, acc} = reducer.(ref, :lists.reverse(events), acc, index)
{events, {set, acc}}
end
end
defp uniq_by_reducer(uniq_by) do
fn event, {set, acc} ->
key = uniq_by.(event)
case set do
%{^key => true} -> {set, acc}
%{} -> {Map.put(set, key, true), [event | acc]}
end
end
end
defp build_uniq_trigger(trigger) do
fn {set, acc}, index, op, name ->
{events, acc} = trigger.(acc, index, op, name)
{events, {set, acc}}
end
end
defp merge_map_state(ops) do
case take_mappers(ops, []) do
{[], [{:map_state, fun} | ops]} ->
[fun | merge_map_state(ops)]
{[], [{:uniq, by} | ops]} ->
[fn acc, _, _ -> Enum.uniq_by(acc, by) end | merge_map_state(ops)]
{[], []} ->
[]
{mappers, ops} ->
reducer = :lists.foldl(&mapper/2, &[&1 | &2], mappers)
[fn old_acc, _, _ -> Enum.reduce(old_acc, [], reducer) end | merge_map_state(ops)]
end
end
## Mappers
defp mapper_ops(ops) do
reducer = :lists.foldl(&mapper/2, &[&1 | &2], ops)
{fn -> [] end,
fn _ref, events, [], _index -> {:lists.reverse(:lists.foldl(reducer, [], events)), []} end,
fn _acc, _index, _op, _trigger -> {[], []} end}
end
defp mapper({:mapper, :each, [each]}, fun) do
fn x, acc -> each.(x); fun.(x, acc) end
end
defp mapper({:mapper, :filter, [filter]}, fun) do
fn x, acc ->
if filter.(x) do
fun.(x, acc)
else
acc
end
end
end
defp mapper({:mapper, :filter_map, [filter, mapper]}, fun) do
fn x, acc ->
if filter.(x) do
fun.(mapper.(x), acc)
else
acc
end
end
end
defp mapper({:mapper, :flat_map, [flat_mapper]}, fun) do
fn x, acc ->
Enum.reduce(flat_mapper.(x), acc, fun)
end
end
defp mapper({:mapper, :map, [mapper]}, fun) do
fn x, acc -> fun.(mapper.(x), acc) end
end
defp mapper({:mapper, :reject, [filter]}, fun) do
fn x, acc ->
if filter.(x) do
acc
else
fun.(x, acc)
end
end
end
defp take_mappers([{:mapper, _, _} = mapper | ops], acc),
do: take_mappers(ops, [mapper | acc])
defp take_mappers(ops, acc),
do: {acc, ops}
end