Current section
Files
Jump to
Current section
Files
lib/aerospike/runtime/streaming_executor.ex
defmodule Aerospike.Runtime.StreamingExecutor do
@moduledoc false
alias Aerospike.Cluster.CircuitBreaker
alias Aerospike.Cluster.Tender
alias Aerospike.Command.StreamingCommand
alias Aerospike.Error
alias Aerospike.Protocol.Message
alias Aerospike.Query
alias Aerospike.Scan
@type runtime :: %{
required(:tender) => GenServer.server(),
required(:transport) => module()
}
@type resolve_handle_fun ::
(GenServer.server(), String.t() ->
{:ok, Tender.node_handle()} | {:error, :unknown_node})
@type allow_dispatch_fun :: (term(), term() -> :ok | {:error, Error.t()})
@type command_ctx :: %{
required(:scannable) => Scan.t() | Query.t(),
required(:node_request) => map(),
optional(:resolve_handle) => resolve_handle_fun(),
optional(:allow_dispatch) => allow_dispatch_fun()
}
@type task_result :: {:ok, term()} | {:exit, term()}
@type task_runner ::
([map()], (map() -> term()), keyword() -> Enumerable.t())
@spec run_node_requests(
StreamingCommand.t(),
runtime(),
Scan.t() | Query.t(),
[map()],
%{
required(:task_timeout) => timeout(),
required(:max_concurrent_nodes) => integer()
},
keyword()
) :: Enumerable.t()
def run_node_requests(
%StreamingCommand{} = command,
runtime,
scannable,
node_requests,
%{task_timeout: task_timeout} = policy,
opts \\ []
)
when is_map(runtime) and is_list(node_requests) and is_list(opts) do
ctx_overrides = Keyword.get(opts, :ctx_overrides, %{})
task_runner = Keyword.get(opts, :task_runner, &Task.async_stream/3)
task_runner.(
node_requests,
&run_command(
command,
runtime,
Map.merge(ctx_overrides, %{scannable: scannable, node_request: &1})
),
ordered: true,
max_concurrency: max_concurrency(policy, length(node_requests)),
timeout: task_timeout,
on_timeout: :kill_task
)
end
@spec run_command(StreamingCommand.t(), runtime(), command_ctx()) :: term()
def run_command(%StreamingCommand{} = command, runtime, ctx)
when is_map(runtime) and is_map(ctx) do
%{node_request: %{node_name: node_name}} = ctx
resolve_handle = Map.get(ctx, :resolve_handle, &Tender.node_handle/2)
case resolve_handle.(runtime.tender, node_name) do
{:ok, handle} ->
check_breaker(command, runtime, ctx, handle)
{:error, :unknown_node} ->
{:error, unknown_node(ctx.scannable, node_name)}
end
end
defp check_breaker(command, runtime, ctx, handle) do
allow_dispatch = Map.get(ctx, :allow_dispatch, &CircuitBreaker.allow?/2)
case allow_dispatch.(handle.counters, handle.breaker) do
:ok ->
connect_and_stream(command, runtime.transport, ctx, handle)
{:error, %Error{} = err} ->
{:error, err}
end
end
defp connect_and_stream(command, transport, ctx, handle) do
timeout = transport_timeout(ctx.node_request.policy)
node_opts = [use_compression: handle.use_compression, attempt: 0]
request = StreamingCommand.build_request(command, ctx)
case transport.connect(handle.host, handle.port, handle.connect_opts) do
{:ok, conn} ->
case transport.stream_open(conn, request, timeout, node_opts) do
{:ok, stream} ->
read_stream(command, transport, stream, timeout, ctx)
{:error, %Error{} = err} ->
StreamingCommand.error_result(command, err, ctx)
end
{:error, %Error{} = err} ->
StreamingCommand.error_result(command, err, ctx)
end
end
defp read_stream(command, transport, stream, timeout, ctx) do
result =
do_read_stream(
command,
transport,
stream,
timeout,
ctx,
StreamingCommand.init(command, ctx)
)
_ = transport.stream_close(stream)
result
end
defp do_read_stream(command, transport, stream, timeout, ctx, acc) do
case transport.stream_read(stream, timeout) do
{:ok, frame} ->
handle_stream_frame(command, transport, stream, timeout, ctx, acc, frame)
:done ->
StreamingCommand.finish(command, acc, ctx)
{:error, %Error{} = err} ->
StreamingCommand.error_result(command, err, ctx)
end
end
defp handle_stream_frame(command, transport, stream, timeout, ctx, acc, frame) do
case decode_stream_body(frame) do
{:ok, body} ->
handle_decoded_stream_body(command, transport, stream, timeout, ctx, acc, body)
{:error, %Error{} = err} ->
StreamingCommand.error_result(command, err, ctx)
end
end
defp handle_decoded_stream_body(command, transport, stream, timeout, ctx, acc, body) do
case StreamingCommand.consume_frame(command, body, ctx, acc) do
{:cont, next_acc} ->
do_read_stream(command, transport, stream, timeout, ctx, next_acc)
{:halt, result} ->
result
{:error, %Error{} = err} ->
StreamingCommand.error_result(command, err, ctx)
end
end
defp unknown_node(%Scan{}, node_name) do
Error.from_result_code(:invalid_node, message: "scan target node unavailable: #{node_name}")
end
defp unknown_node(%Query{}, node_name) do
Error.from_result_code(:invalid_node, message: "query target node unavailable: #{node_name}")
end
defp decode_stream_body(frame) do
case Message.decode(frame) do
{:ok, {2, 3, body}} ->
{:ok, body}
{:ok, {_version, _type, _body}} ->
{:error,
Error.from_result_code(:parse_error, message: "unexpected stream frame type from server")}
{:error, _reason} ->
{:error, Error.from_result_code(:parse_error, message: "invalid stream frame")}
end
end
defp max_concurrency(%{max_concurrent_nodes: 0}, fallback), do: fallback
defp max_concurrency(%{max_concurrent_nodes: n}, fallback) when is_integer(n) and n > 0 do
min(n, fallback)
end
defp transport_timeout(%{timeout: total, socket_timeout: socket})
when is_integer(total) and total > 0 and is_integer(socket) and socket > total do
total
end
defp transport_timeout(%{socket_timeout: timeout}) when is_integer(timeout) and timeout > 0,
do: timeout
defp transport_timeout(%{timeout: timeout}), do: timeout
end