Packages
FerricFlow durable workflows and queues with native-protocol storage, Raft durability, and Bitcask persistence.
Current section
Files
Jump to
Current section
Files
lib/ferricstore/flow/pipeline_read_command.ex
defmodule Ferricstore.Flow.PipelineReadCommand do
@moduledoc false
alias Ferricstore.Flow.Codec
alias Ferricstore.Store.Router
@default_max_count 10_000
@default_payload_return_max_bytes 64 * 1024
def command(_ctx, {:get, id, opts}) when is_binary(id) and is_list(opts) do
with :ok <- validate_id(id),
:ok <- validate_opts(opts),
{:ok, payload_return} <- payload_return_opts(opts, false),
{:ok, partition_key} <- optional_partition_key(opts),
:ok <- validate_key_size(Ferricstore.Flow.Keys.state_key(id, partition_key)) do
{:get, id, partition_key, payload_return}
end
end
def command(ctx, {:flow_get, id, opts}) do
command(ctx, {:get, id, opts})
end
def command(_ctx, {:history, id, opts}) when is_binary(id) and is_list(opts) do
with {:ok, {partition_key, history_key, query, include_cold?, consistent?, value_return}} <-
history_query_attrs(id, opts) do
{:history, id, partition_key, history_key, query, include_cold?, consistent?, value_return}
end
end
def command(ctx, {:flow_history, id, opts}) do
command(ctx, {:history, id, opts})
end
def command(ctx, {:list, type, opts}) when is_binary(type) and is_list(opts),
do: read_result(fn -> Ferricstore.Flow.list(ctx, type, opts) end)
def command(ctx, {:flow_list, type, opts}) do
command(ctx, {:list, type, opts})
end
def command(ctx, {:terminals, type, opts}) when is_binary(type) and is_list(opts),
do: read_result(fn -> Ferricstore.Flow.terminals(ctx, type, opts) end)
def command(ctx, {:flow_terminals, type, opts}) do
command(ctx, {:terminals, type, opts})
end
def command(ctx, {:failures, type, opts}) when is_binary(type) and is_list(opts),
do: read_result(fn -> Ferricstore.Flow.failures(ctx, type, opts) end)
def command(ctx, {:flow_failures, type, opts}) do
command(ctx, {:failures, type, opts})
end
def command(ctx, {:by_parent, parent_flow_id, opts})
when is_binary(parent_flow_id) and is_list(opts),
do: read_result(fn -> Ferricstore.Flow.by_parent(ctx, parent_flow_id, opts) end)
def command(ctx, {:flow_by_parent, parent_flow_id, opts}) do
command(ctx, {:by_parent, parent_flow_id, opts})
end
def command(ctx, {:by_root, root_flow_id, opts})
when is_binary(root_flow_id) and is_list(opts),
do: read_result(fn -> Ferricstore.Flow.by_root(ctx, root_flow_id, opts) end)
def command(ctx, {:flow_by_root, root_flow_id, opts}) do
command(ctx, {:by_root, root_flow_id, opts})
end
def command(ctx, {:by_correlation, correlation_id, opts})
when is_binary(correlation_id) and is_list(opts),
do: read_result(fn -> Ferricstore.Flow.by_correlation(ctx, correlation_id, opts) end)
def command(ctx, {:flow_by_correlation, correlation_id, opts}) do
command(ctx, {:by_correlation, correlation_id, opts})
end
def command(ctx, {:info, type, opts}) when is_binary(type) and is_list(opts),
do: read_result(fn -> Ferricstore.Flow.info(ctx, type, opts) end)
def command(ctx, {:flow_info, type, opts}) do
command(ctx, {:info, type, opts})
end
def command(ctx, {:stuck, type, opts}) when is_binary(type) and is_list(opts),
do: read_result(fn -> Ferricstore.Flow.stuck(ctx, type, opts) end)
def command(ctx, {:flow_stuck, type, opts}) do
command(ctx, {:stuck, type, opts})
end
def command(_ctx, _op), do: {:error, "ERR unsupported flow pipeline read command"}
def decode_get(nil), do: {:ok, nil}
def decode_get(value) when is_binary(value) do
{:ok, Codec.decode_record(value)}
rescue
_ -> {:ok, nil}
end
def decode_get({:error, _reason} = error), do: error
def decode_get(_other), do: {:ok, nil}
def history_results(history_ops, ctx) do
Ferricstore.Flow.PipelineHistoryRead.results(history_ops, ctx, %{
read: &Ferricstore.Flow.HistoryRead.read/8,
fetch_count: &Ferricstore.Flow.HistoryRead.query_fetch_count/1,
hot_range: &Ferricstore.Flow.HistoryRead.hot_range/5,
rank_range_many: &Router.flow_index_rank_range_many/2,
fallback: &Ferricstore.Flow.HistoryRead.hot_fallback_scan/4,
from_event_ids: &Ferricstore.Flow.HistoryRead.from_event_ids/6,
apply_query: &Ferricstore.Flow.HistoryRead.apply_query/2
})
end
defp read_result(read_fun), do: {:other, read_fun}
defp history_query_attrs(id, opts) do
with :ok <- validate_opts(opts),
:ok <- validate_id(id),
{:ok, partition_key} <- optional_partition_key(opts),
history_key = Ferricstore.Flow.Keys.history_key(id, partition_key),
:ok <- validate_key_size(history_key),
{:ok, count} <- flow_count(opts),
{:ok, include_cold?} <- optional_boolean(opts, :include_cold, true),
{:ok, consistent?} <- optional_boolean(opts, :consistent_projection, true),
{:ok, value_return} <- history_value_return_opts(opts),
{:ok, query} <- flow_history_query_opts(opts, count) do
{:ok, {partition_key, history_key, query, include_cold?, consistent?, value_return}}
end
end
defp validate_opts(opts) do
if Keyword.keyword?(opts), do: :ok, else: {:error, "ERR flow opts must be a keyword list"}
end
defp validate_id(id) when is_binary(id) and id != "", do: :ok
defp validate_id(_id), do: {:error, "ERR flow id must be a non-empty string"}
defp validate_key_size(key) do
if byte_size(key) <= Router.max_key_size() do
:ok
else
{:error, "ERR key too large (max #{Router.max_key_size()} bytes)"}
end
end
defp flow_count(opts) do
case Keyword.get(opts, :count, 100) do
value when is_integer(value) and value > 0 ->
max_count = flow_max_count()
if value <= max_count do
{:ok, value}
else
{:error, "ERR flow count exceeds maximum #{max_count}"}
end
_ ->
{:error, "ERR flow count must be a positive integer"}
end
end
defp flow_max_count do
case Application.get_env(:ferricstore, :flow_max_count, @default_max_count) do
value when is_integer(value) and value > 0 -> value
_ -> @default_max_count
end
end
defp payload_return_opts(opts, default_enabled?) do
with {:ok, full?} <- optional_boolean(opts, :full, default_enabled?),
{:ok, enabled?} <- optional_boolean(opts, :payload, full?),
{:ok, max_bytes} <-
optional_non_neg_integer(
opts,
:payload_max_bytes,
flow_payload_return_max_bytes()
) do
{:ok, %{enabled?: enabled?, max_bytes: max_bytes}}
end
end
defp history_value_return_opts(opts) do
with {:ok, enabled?} <- optional_boolean(opts, :values, false),
{:ok, max_bytes} <-
optional_non_neg_integer(
opts,
:payload_max_bytes,
flow_payload_return_max_bytes()
) do
{:ok, %{enabled?: enabled?, max_bytes: max_bytes}}
end
end
defp flow_payload_return_max_bytes do
case Application.get_env(
:ferricstore,
:flow_payload_return_max_bytes,
@default_payload_return_max_bytes
) do
value when is_integer(value) and value >= 0 -> value
_ -> @default_payload_return_max_bytes
end
end
defp flow_history_query_opts(opts, count) do
with {:ok, from_event} <- optional_binary_or_nil(opts, :from_event, nil),
{:ok, to_event} <- optional_binary_or_nil(opts, :to_event, nil),
{:ok, from_ms} <- optional_non_neg_integer(opts, :from_ms, nil),
{:ok, to_ms} <- optional_non_neg_integer(opts, :to_ms, nil),
{:ok, from_version} <- optional_non_neg_integer(opts, :from_version, nil),
{:ok, to_version} <- optional_non_neg_integer(opts, :to_version, nil),
{:ok, rev?} <- optional_boolean(opts, :rev, false),
{:ok, event} <- optional_binary_or_nil(opts, :event, nil),
{:ok, worker} <- optional_binary_or_nil(opts, :worker, nil),
:ok <- Ferricstore.Flow.HistoryQuery.validate_ms_range(from_ms, to_ms),
:ok <- Ferricstore.Flow.HistoryQuery.validate_version_range(from_version, to_version),
:ok <-
Ferricstore.Flow.HistoryQuery.validate_event_range(
from_event,
to_event,
&Ferricstore.Flow.HistoryEvent.ms/1
) do
{:ok,
%{
count: count,
from_event: from_event,
to_event: to_event,
from_ms: from_ms,
to_ms: to_ms,
from_version: from_version,
to_version: to_version,
rev?: rev?,
event: event,
worker: worker
}}
end
end
defp optional_partition_key(opts) do
case Keyword.get(opts, :partition_key, nil) do
nil -> {:ok, nil}
:global -> {:ok, nil}
value when is_binary(value) and value != "" -> {:ok, value}
_ -> {:error, "ERR flow partition_key must be a non-empty string or :global"}
end
end
defp optional_binary_or_nil(opts, key, default) do
case Keyword.get(opts, key, default) do
nil -> {:ok, nil}
value when is_binary(value) -> {:ok, value}
_ -> {:error, "ERR flow #{key} must be a string"}
end
end
defp optional_boolean(opts, key, default) do
case Keyword.get(opts, key, default) do
value when is_boolean(value) -> {:ok, value}
_ -> {:error, "ERR flow #{key} must be a boolean"}
end
end
defp optional_non_neg_integer(opts, key, default) do
case Keyword.fetch(opts, key) do
{:ok, value} when is_integer(value) and value >= 0 -> {:ok, value}
{:ok, _} -> {:error, "ERR flow #{key} must be a non-negative integer"}
:error when is_integer(default) and default >= 0 -> {:ok, default}
:error when is_nil(default) -> {:ok, nil}
:error -> {:error, "ERR flow #{key} must be a non-negative integer"}
end
end
end