Packages

Aerospike driver for Elixir with an OTP-native cluster runtime

Current section

Files

Jump to
aerospike_driver lib aerospike command scan_ops stream_runner.ex
Raw

lib/aerospike/command/scan_ops/stream_runner.ex

defmodule Aerospike.Command.ScanOps.StreamRunner do
@moduledoc false
alias Aerospike.Cluster.CircuitBreaker
alias Aerospike.Cluster.NodePool
alias Aerospike.Cluster.Tender
alias Aerospike.Command.ScanOps.PageRunner
alias Aerospike.Command.StreamingCommand
alias Aerospike.Error
alias Aerospike.ExecuteTask
alias Aerospike.Policy
alias Aerospike.Protocol.Response
alias Aerospike.Protocol.ScanQuery
alias Aerospike.Protocol.ScanResponse
alias Aerospike.Query
alias Aerospike.RetryPolicy
alias Aerospike.Runtime.StreamingExecutor
alias Aerospike.Scan
@spec stream(GenServer.server(), Scan.t() | Query.t(), keyword()) ::
{:ok, Enumerable.t()} | {:error, Error.t()}
def stream(tender, scannable, opts) when is_list(opts) do
stream_internal(tender, scannable, opts, nil, nil, &ScanResponse.parse_stream_chunk/3)
end
@spec stream_node(GenServer.server(), String.t(), Scan.t() | Query.t(), keyword()) ::
{:ok, Enumerable.t()} | {:error, Error.t()}
def stream_node(tender, node_name, scannable, opts)
when is_binary(node_name) and is_list(opts) do
stream_internal(tender, scannable, opts, node_name, nil, &ScanResponse.parse_stream_chunk/3)
end
@spec query_aggregate(GenServer.server(), Query.t(), keyword(), String.t(), String.t(), list()) ::
{:ok, Enumerable.t()} | {:error, Error.t()}
def query_aggregate(tender, %Query{} = query, opts, package, function, args)
when is_list(opts) and is_binary(package) and is_binary(function) and is_list(args) do
stream_internal(
tender,
query,
opts,
nil,
fn q, node_partitions, run_opts ->
ScanQuery.build_query_aggregate(q, node_partitions, package, function, args, run_opts)
end,
&ScanResponse.parse_aggregate_stream_chunk/3
)
end
@spec query_execute(GenServer.server(), Query.t(), list(), keyword()) ::
{:ok, ExecuteTask.t()} | {:error, Error.t()}
def query_execute(tender, %Query{} = query, ops, opts \\ [])
when is_list(ops) and is_list(opts) do
run_background_query(
tender,
query,
opts,
nil,
fn q, node_partitions, run_opts ->
ScanQuery.build_query_execute(q, node_partitions, ops, run_opts)
end,
:query_execute
)
end
@spec query_execute_node(GenServer.server(), String.t(), Query.t(), list(), keyword()) ::
{:ok, ExecuteTask.t()} | {:error, Error.t()}
def query_execute_node(tender, node_name, %Query{} = query, ops, opts \\ [])
when is_binary(node_name) and is_list(ops) and is_list(opts) do
run_background_query(
tender,
query,
opts,
node_name,
fn q, node_partitions, run_opts ->
ScanQuery.build_query_execute(q, node_partitions, ops, run_opts)
end,
:query_execute
)
end
@spec query_udf(GenServer.server(), Query.t(), String.t(), String.t(), list(), keyword()) ::
{:ok, ExecuteTask.t()} | {:error, Error.t()}
def query_udf(tender, %Query{} = query, package, function, args, opts \\ [])
when is_binary(package) and is_binary(function) and is_list(args) and is_list(opts) do
run_background_query(
tender,
query,
opts,
nil,
fn q, node_partitions, run_opts ->
ScanQuery.build_query_udf(q, node_partitions, package, function, args, run_opts)
end,
:query_udf
)
end
@spec query_udf_node(
GenServer.server(),
String.t(),
Query.t(),
String.t(),
String.t(),
list(),
keyword()
) :: {:ok, ExecuteTask.t()} | {:error, Error.t()}
def query_udf_node(tender, node_name, %Query{} = query, package, function, args, opts \\ [])
when is_binary(node_name) and is_binary(package) and is_binary(function) and is_list(args) and
is_list(opts) do
run_background_query(
tender,
query,
opts,
node_name,
fn q, node_partitions, run_opts ->
ScanQuery.build_query_udf(q, node_partitions, package, function, args, run_opts)
end,
:query_udf
)
end
defp stream_internal(tender, scannable, opts, node_filter, wire_builder, parser) do
with {:ok, runtime} <- PageRunner.runtime(tender, scannable),
{:ok, policy} <- Policy.scan_query_runtime(retry_policy(runtime), opts),
{:ok, _tracker, node_requests} <-
PageRunner.prepare_node_requests(runtime, scannable, node_filter, policy) do
command = stream_command(wire_builder, parser)
stream =
StreamingExecutor.run_node_requests(command, runtime, scannable, node_requests, policy)
|> Stream.flat_map(&flatten_task_result/1)
{:ok, stream}
end
end
defp run_background_query(tender, query, opts, node_filter, wire_builder, kind) do
with {:ok, runtime} <- PageRunner.runtime(tender, query),
{:ok, policy} <- Policy.scan_query_runtime(retry_policy(runtime), opts),
{:ok, _tracker, node_requests} <-
PageRunner.prepare_node_requests(runtime, query, node_filter, policy) do
task_timeout = policy.task_timeout
max_concurrency = max_concurrency(policy, length(node_requests))
results =
node_requests
|> Task.async_stream(
fn node_request ->
run_background_node_request(runtime, query, node_request, wire_builder)
end,
ordered: true,
max_concurrency: max_concurrency,
timeout: task_timeout,
on_timeout: :kill_task
)
|> Enum.to_list()
case background_results(results) do
:ok ->
{:ok,
%ExecuteTask{
conn: tender,
namespace: query.namespace,
set: query.set,
task_id: policy.task_id,
kind: kind,
node_name: node_filter
}}
{:error, %Error{} = err} ->
{:error, err}
end
end
end
defp retry_policy(%{tables: %{meta: meta}}) when is_atom(meta), do: RetryPolicy.load(meta)
defp request_partitions(%{parts_full: full, parts_partial: partials, record_max: record_max}) do
%{
parts_full: Enum.map(full, & &1.id),
parts_partial: Enum.map(partials, &partition_entry/1),
record_max: record_max
}
end
defp build_wire(%Scan{} = scan, node_partitions, %Policy.ScanQueryRuntime{} = policy) do
ScanQuery.build_scan(scan, request_partitions(node_partitions), policy)
end
defp build_wire(%Query{} = query, node_partitions, %Policy.ScanQueryRuntime{} = policy) do
ScanQuery.build_query(query, request_partitions(node_partitions), policy)
end
defp build_wire(
%Query{} = query,
node_partitions,
%Policy.ScanQueryRuntime{} = policy,
wire_builder
)
when is_function(wire_builder, 3) do
wire_builder.(query, request_partitions(node_partitions), policy)
end
defp build_wire(%Scan{} = scan, node_partitions, %Policy.ScanQueryRuntime{} = policy, nil),
do: build_wire(scan, node_partitions, policy)
defp build_wire(%Query{} = query, node_partitions, %Policy.ScanQueryRuntime{} = policy, nil),
do: build_wire(query, node_partitions, policy)
defp stream_command(wire_builder, parser) do
StreamingCommand.new!(
name: __MODULE__,
build_request: fn %{scannable: scannable, node_request: node_request} ->
build_wire(scannable, node_request.node_partitions, node_request.policy, wire_builder)
end,
init: fn _input -> [] end,
consume_frame: fn body, %{scannable: scannable}, acc ->
with {:ok, records, _partition_done, done?} <-
parser.(body, namespace(scannable), set(scannable)) do
next_acc = Enum.reverse(records, acc)
if done? do
{:halt, {:ok, Enum.reverse(next_acc)}}
else
{:cont, next_acc}
end
end
end,
finish: fn acc, _input -> {:ok, Enum.reverse(acc)} end,
error_result: fn err, _input -> {:error, err} end
)
end
defp namespace(%Scan{namespace: namespace}), do: namespace
defp namespace(%Query{namespace: namespace}), do: namespace
defp set(%Scan{set: set}), do: set
defp set(%Query{set: set}), do: set
defp partition_entry(%{id: id} = ps) do
%{
id: id,
digest: Map.get(ps, :digest),
bval: Map.get(ps, :bval)
}
end
defp flatten_task_result({:ok, {:ok, records}}), do: records
defp flatten_task_result({:ok, {:error, %Error{} = err}}) do
raise err
end
defp flatten_task_result({:exit, reason}) do
raise Error.from_result_code(:network_error, message: "scan task exited: #{inspect(reason)}")
end
defp background_results(results) do
case Enum.find(results, fn
{:ok, {:error, %Error{}}} -> true
{:exit, _} -> true
_ -> false
end) do
{:ok, {:error, %Error{} = err}} ->
{:error, err}
{:exit, reason} ->
{:error,
Error.from_result_code(:network_error,
message: "background task exited: #{inspect(reason)}"
)}
nil ->
:ok
end
end
defp run_background_node_request(
runtime,
query,
%{node_name: node_name} = node_request,
wire_builder
) do
case Tender.node_handle(runtime.tender, node_name) do
{:ok, handle} ->
case CircuitBreaker.allow?(handle.counters, handle.breaker) do
:ok ->
connect_and_run_background(runtime, query, node_request, handle, wire_builder)
{:error, %Error{} = err} ->
{:error, err}
end
{:error, :unknown_node} ->
{:error, unknown_node(node_name)}
end
end
defp connect_and_run_background(runtime, query, node_request, handle, wire_builder) do
transport = runtime.transport
timeout = transport_timeout(node_request.policy)
node_opts = [use_compression: handle.use_compression, attempt: 0]
request =
build_wire(query, node_request.node_partitions, node_request.policy, wire_builder)
NodePool.checkout!(
node_request.node_name,
handle.pool,
fn conn ->
case transport.command(conn, request, timeout, node_opts) do
{:ok, body} ->
result = decode_background_response(body)
{result, background_checkin_value(result, conn)}
{:error, %Error{} = err} ->
result = {:error, err}
{result, background_checkin_value(result, conn)}
end
end,
node_request.pool_checkout_timeout
)
end
defp decode_background_response(body) when is_binary(body) do
with {:ok, msg} <- Response.decode_as_msg(body) do
Response.parse_record_metadata_response(msg)
end
end
defp background_checkin_value(result, conn) do
case background_checkin_classification(result) do
%{close_connection?: true, node_failure?: true} -> {:close, :failure}
%{close_connection?: true} -> :close
_ -> conn
end
end
defp background_checkin_classification(result) do
case result do
{:ok, _} ->
%{close_connection?: false, node_failure?: false}
{:error, %Error{code: code}} when code in [:network_error, :timeout, :connection_error] ->
%{close_connection?: true, node_failure?: true}
{:error, %Error{}} ->
%{close_connection?: false, node_failure?: false}
end
end
defp max_concurrency(%Policy.ScanQueryRuntime{max_concurrent_nodes: 0}, fallback), do: fallback
defp max_concurrency(%Policy.ScanQueryRuntime{max_concurrent_nodes: n}, fallback)
when is_integer(n) and n > 0 do
min(n, fallback)
end
defp transport_timeout(%Policy.ScanQueryRuntime{timeout: total, socket_timeout: socket})
when total > 0 and socket > total do
total
end
defp transport_timeout(%Policy.ScanQueryRuntime{socket_timeout: timeout})
when timeout > 0,
do: timeout
defp transport_timeout(%Policy.ScanQueryRuntime{timeout: timeout}), do: timeout
defp unknown_node(node_name) do
Error.from_result_code(:invalid_node, message: "scan target node unavailable: #{node_name}")
end
end