Packages

Aerospike driver for Elixir with an OTP-native cluster runtime

Current section

Files

Jump to
aerospike_driver lib aerospike command scan_ops page_runner.ex
Raw

lib/aerospike/command/scan_ops/page_runner.ex

defmodule Aerospike.Command.ScanOps.PageRunner do
@moduledoc false
alias Aerospike.Cluster
alias Aerospike.Cluster.PartitionMap
alias Aerospike.Cluster.Router
alias Aerospike.Cluster.Tender
alias Aerospike.Command.NodePartitions
alias Aerospike.Command.PartitionTracker
alias Aerospike.Command.ScanOps
alias Aerospike.Command.StreamingCommand
alias Aerospike.Cursor
alias Aerospike.Error
alias Aerospike.Page
alias Aerospike.PartitionFilter
alias Aerospike.Policy
alias Aerospike.Protocol.ScanQuery
alias Aerospike.Protocol.ScanResponse
alias Aerospike.Query
alias Aerospike.Record
alias Aerospike.RetryPolicy
alias Aerospike.Runtime.StreamingExecutor
alias Aerospike.Scan
@typep runtime :: %{
tender: GenServer.server(),
transport: module(),
tables: Router.tables()
}
@spec page(GenServer.server(), Scan.t() | Query.t(), keyword()) ::
{:ok, Page.t()} | {:error, Error.t()}
def page(tender, scannable, opts) when is_list(opts) do
{cursor, opts2} = Keyword.pop(opts, :cursor)
with {:ok, scannable2} <- apply_optional_cursor(scannable, cursor) do
page_internal(tender, scannable2, opts2, nil)
end
end
@spec page_node(GenServer.server(), String.t(), Scan.t() | Query.t(), keyword()) ::
{:ok, Page.t()} | {:error, Error.t()}
def page_node(tender, node_name, scannable, opts)
when is_binary(node_name) and is_list(opts) do
{cursor, opts2} = Keyword.pop(opts, :cursor)
with {:ok, scannable2} <- apply_optional_cursor(scannable, cursor) do
page_internal(tender, scannable2, opts2, node_name)
end
end
@spec all(GenServer.server(), Query.t(), keyword()) ::
{:ok, [Record.t()]} | {:error, Error.t()}
def all(tender, %Query{} = query, opts) when is_list(opts) do
with :ok <- require_max_records(query),
{:ok, first_page} <- page(tender, query, opts) do
collect_all(tender, query, opts, first_page.records, first_page)
end
end
@spec all_node(GenServer.server(), String.t(), Query.t(), keyword()) ::
{:ok, [Record.t()]} | {:error, Error.t()}
def all_node(tender, node_name, %Query{} = query, opts)
when is_binary(node_name) and is_list(opts) do
with :ok <- require_max_records(query),
{:ok, first_page} <- page_node(tender, node_name, query, opts) do
collect_all_node(tender, node_name, query, opts, first_page.records, first_page)
end
end
@doc false
@spec runtime(GenServer.server(), Scan.t() | Query.t()) ::
{:ok, runtime()} | {:error, Error.t()}
def runtime(tender, scannable) do
if Cluster.ready?(tender) do
{:ok,
%{
tender: tender,
transport: Tender.transport(tender),
tables: Cluster.tables(tender)
}}
else
{:error,
Error.from_result_code(
:cluster_not_ready,
message: "#{operation_name(scannable)} requires a ready cluster"
)}
end
end
@doc false
@spec prepare_node_requests(
runtime(),
Scan.t() | Query.t(),
String.t() | nil,
keyword() | Policy.ScanQueryRuntime.t()
) :: {:ok, PartitionTracker.t(), [map()]} | {:error, Error.t()}
def prepare_node_requests(runtime, scannable, node_filter, opts) when is_list(opts) do
with {:ok, policy} <- Policy.scan_query_runtime(retry_policy(runtime), opts) do
prepare_node_requests(runtime, scannable, node_filter, policy)
end
end
def prepare_node_requests(runtime, scannable, node_filter, %Policy.ScanQueryRuntime{} = policy) do
do_prepare_node_requests(runtime, scannable, node_filter, policy)
end
defp do_prepare_node_requests(runtime, scannable, node_filter, policy) do
with {:ok, node_names} <- active_nodes(runtime.tender, node_filter, scannable),
{:ok, tracker} <- new_tracker(scannable, node_names, node_filter, policy),
{:ok, partition_map} <- partition_map(runtime, scannable),
{:ok, tracker, node_partitions} <-
PartitionTracker.assign_partitions_to_nodes(tracker, partition_map) do
tracker = activate_record_budget(tracker, scannable)
{:ok, tracker, build_node_requests(node_partitions, scannable, policy)}
end
end
defp collect_all(_tender, _query, _opts, acc, %Page{done?: true}), do: {:ok, acc}
defp collect_all(_tender, _query, _opts, acc, %Page{cursor: nil}), do: {:ok, acc}
defp collect_all(tender, query, opts, acc, %Page{cursor: cursor}) do
case page(tender, query, Keyword.put(opts, :cursor, cursor)) do
{:ok, %Page{} = page} ->
collect_all(tender, attach_cursor(query, cursor), opts, acc ++ page.records, page)
{:error, %Error{} = err} ->
{:error, err}
end
end
defp collect_all_node(_tender, _node_name, _query, _opts, acc, %Page{done?: true}),
do: {:ok, acc}
defp collect_all_node(_tender, _node_name, _query, _opts, acc, %Page{cursor: nil}),
do: {:ok, acc}
defp collect_all_node(tender, node_name, query, opts, acc, %Page{cursor: cursor}) do
case page_node(tender, node_name, query, Keyword.put(opts, :cursor, cursor)) do
{:ok, %Page{} = page} ->
collect_all_node(
tender,
node_name,
attach_cursor(query, cursor),
opts,
acc ++ page.records,
page
)
{:error, %Error{} = err} ->
{:error, err}
end
end
defp page_internal(tender, scannable, opts, node_filter) do
with {:ok, runtime} <- runtime(tender, scannable),
{:ok, policy} <- Policy.scan_query_runtime(retry_policy(runtime), opts),
{:ok, tracker, node_requests} <-
prepare_node_requests(runtime, scannable, node_filter, policy) do
run_tracker_page(tender, scannable, node_filter, runtime, policy, tracker, node_requests)
end
end
defp active_nodes(tender, nil, scannable) do
tender
|> Cluster.active_nodes()
|> case do
[] ->
{:error,
Error.from_result_code(
:cluster_not_ready,
message: "#{operation_name(scannable)} requires active nodes"
)}
active ->
{:ok, active}
end
end
defp active_nodes(tender, node_name, _scannable) do
if Cluster.active_node?(tender, node_name),
do: {:ok, [node_name]},
else: {:error, unknown_node(node_name)}
end
defp new_tracker(%Scan{} = scan, node_names, node_filter, %Policy.ScanQueryRuntime{} = policy) do
filter = scan.partition_filter || PartitionFilter.all()
{:ok,
PartitionTracker.new(
filter,
tracker_opts(node_names, scan.max_records || 0, node_filter, policy)
)}
rescue
err in [ArgumentError] ->
{:error, Error.from_result_code(:invalid_argument, message: err.message)}
end
defp new_tracker(%Query{} = query, node_names, node_filter, %Policy.ScanQueryRuntime{} = policy) do
filter = query.partition_filter || PartitionFilter.all()
{:ok,
PartitionTracker.new(
filter,
tracker_opts(node_names, query.max_records || 0, node_filter, policy)
)}
rescue
err in [ArgumentError] ->
{:error, Error.from_result_code(:invalid_argument, message: err.message)}
end
defp partition_map(runtime, %Scan{namespace: namespace}) do
partition_map_for_namespace(runtime, namespace)
end
defp partition_map(runtime, %Query{namespace: namespace}) do
partition_map_for_namespace(runtime, namespace)
end
defp partition_map_for_namespace(runtime, namespace) do
owners_tab = runtime.tables.owners
count = PartitionMap.partition_count()
0..(count - 1)
|> Enum.reduce_while({:ok, %{}}, fn partition_id, {:ok, acc} ->
case PartitionMap.owners(owners_tab, namespace, partition_id) do
{:ok, %{replicas: replicas}} ->
{:cont, {:ok, Map.put(acc, partition_id, replicas)}}
{:error, :unknown_partition} ->
{:halt,
{:error,
Error.from_result_code(:cluster_not_ready, message: "query requires a ready cluster")}}
end
end)
end
defp build_node_requests(node_partitions, scannable, %Policy.ScanQueryRuntime{} = policy) do
Enum.map(node_partitions, fn node_partitions_item ->
%{
node_name: node_partitions_item.node,
node_partitions: node_partitions_item,
policy: policy,
scannable: scannable,
pool_checkout_timeout: policy.pool_checkout_timeout
}
end)
end
defp retry_policy(%{tables: %{meta: meta}}) when is_atom(meta), do: RetryPolicy.load(meta)
defp retry_policy(_runtime), do: RetryPolicy.defaults()
defp tracker_opts(node_names, max_records, node_filter, %Policy.ScanQueryRuntime{} = policy) do
[
nodes: node_names,
max_records: max_records,
node_filter: node_filter
] ++ tracker_policy_opts(policy)
end
defp tracker_policy_opts(%Policy.ScanQueryRuntime{retry: retry} = policy) do
[
replica: retry.replica_policy,
max_retries: retry.max_retries,
sleep_between_retries: retry.sleep_between_retries_ms,
socket_timeout: effective_socket_timeout(policy),
total_timeout: policy.timeout
]
end
defp effective_socket_timeout(%Policy.ScanQueryRuntime{timeout: total, socket_timeout: socket})
when total > 0 and socket > total do
total
end
defp effective_socket_timeout(%Policy.ScanQueryRuntime{socket_timeout: timeout})
when timeout > 0,
do: timeout
defp effective_socket_timeout(%Policy.ScanQueryRuntime{timeout: timeout}), do: timeout
defp run_tracker_page(
tender,
scannable,
node_filter,
runtime,
policy,
tracker,
node_requests,
acc \\ []
) do
case run_page_jobs(
runtime,
scannable,
tracker,
node_requests,
policy
) do
{:ok, tracker, node_partitions_list, page_records} ->
tracker = %{tracker | node_partitions_list: node_partitions_list}
all_records = acc ++ page_records
case PartitionTracker.is_complete?(tracker, true) do
{:complete, filter, _tracker2} ->
{:ok,
%Page{records: all_records, cursor: cursor_from_filter(filter), done?: filter.done?}}
{:continue, tracker2} ->
maybe_sleep_between_iterations(tracker2)
continue_tracker_page(
tender,
scannable,
node_filter,
runtime,
policy,
tracker2,
all_records
)
{:error, %Error{} = err, _tracker2} ->
{:error, err}
end
{:error, %Error{} = err} ->
{:error, err}
end
end
defp prepare_iteration(tender, scannable, tracker, %Policy.ScanQueryRuntime{} = policy) do
with {:ok, partition_map} <- partition_map(%{tables: Cluster.tables(tender)}, scannable),
{:ok, tracker, node_partitions_list} <-
PartitionTracker.assign_partitions_to_nodes(tracker, partition_map) do
tracker =
activate_record_budget(%{tracker | node_partitions_list: node_partitions_list}, scannable)
{:ok, tracker, build_node_requests(node_partitions_list, scannable, policy)}
end
end
defp continue_tracker_page(
tender,
scannable,
node_filter,
runtime,
policy,
tracker,
all_records
) do
case prepare_iteration(tender, scannable, tracker, policy) do
{:ok, next_tracker, next_node_requests} ->
run_tracker_page(
tender,
scannable,
node_filter,
runtime,
policy,
next_tracker,
next_node_requests,
all_records
)
{:error, %Error{} = err} ->
{:error, err}
end
end
defp run_page_jobs(
runtime,
scannable,
tracker,
node_requests,
policy
) do
command = page_command()
StreamingExecutor.run_node_requests(command, runtime, scannable, node_requests, policy)
|> Enum.reduce_while({:ok, tracker, [], []}, fn
{:ok, {:ok, records, parts, np}}, {:ok, tracker_acc, node_parts_acc, records_acc} ->
{tracker2, np2, kept_records} =
fold_successful_page_result(tracker_acc, np, records, parts)
{:cont, {:ok, tracker2, [np2 | node_parts_acc], records_acc ++ kept_records}}
{:ok, {:error, %Error{code: code} = err, np}},
{:ok, tracker_acc, node_parts_acc, records_acc}
when code in [:invalid_node, :pool_timeout] ->
{tracker2, np2} = mark_node_partitions_unavailable(tracker_acc, np, err)
{:cont, {:ok, tracker2, [np2 | node_parts_acc], records_acc}}
{:ok, {:error, %Error{} = err, np}}, {:ok, tracker_acc, node_parts_acc, records_acc} ->
case PartitionTracker.should_retry?(tracker_acc, np, err) do
{true, tracker2, np2} ->
{:cont, {:ok, tracker2, [np2 | node_parts_acc], records_acc}}
{false, _tracker2, _np2} ->
{:halt, {:error, err}}
end
{:exit, reason}, _acc ->
{:halt, {:error, Error.from_result_code(:network_error, message: inspect(reason))}}
end)
|> case do
{:ok, tracker, node_partitions, page_records} ->
{:ok, tracker, Enum.reverse(node_partitions), page_records}
{:error, %Error{} = err} ->
{:error, err}
end
end
defp fold_successful_page_result(tracker, np, records, parts) do
{tracker, np} = fold_result_records(tracker, np, records)
{tracker, np, kept_records} = ScanOps.allow_record_fold(tracker, np, records)
{tracker, np} = do_fold_partition_done_info(tracker, np, parts)
{tracker, np, kept_records}
end
defp fold_result_records(tracker, np, records) do
Enum.reduce(records, {tracker, np}, fn %Record{key: key}, {tracker_acc, np_acc} ->
PartitionTracker.set_digest(
tracker_acc,
np_acc,
{Aerospike.Key.partition_id(key), key.digest}
)
end)
end
defp do_fold_partition_done_info(tracker, np, parts) do
Enum.reduce(parts, {tracker, np}, fn part, {tracker_acc, np_acc} ->
cond do
Map.get(part, :unavailable?, false) ->
PartitionTracker.partition_unavailable(tracker_acc, np_acc, part.id)
is_binary(Map.get(part, :digest)) and is_integer(Map.get(part, :bval)) ->
PartitionTracker.set_last(tracker_acc, np_acc, part.id, {part.digest, part.bval})
is_binary(Map.get(part, :digest)) ->
PartitionTracker.set_digest(tracker_acc, np_acc, {part.id, part.digest})
true ->
{tracker_acc, np_acc}
end
end)
end
defp mark_node_partitions_unavailable(tracker, np, err) do
ids = Enum.map(np.parts_full, & &1.id) ++ Enum.map(np.parts_partial, & &1.id)
{tracker, np} =
Enum.reduce(ids, {tracker, np}, fn id, {tracker_acc, np_acc} ->
PartitionTracker.partition_unavailable(tracker_acc, np_acc, id)
end)
{%{tracker | exceptions: tracker.exceptions ++ [err]}, np}
end
defp maybe_sleep_between_iterations(tracker) do
case PartitionTracker.should_sleep_for(tracker) do
ms when is_integer(ms) and ms > 0 ->
Process.sleep(ms)
:ok
_ ->
:ok
end
end
defp cursor_from_filter(%PartitionFilter{done?: true}), do: nil
defp cursor_from_filter(%PartitionFilter{partitions: parts}), do: %Cursor{partitions: parts}
defp attach_cursor(%Query{} = query, %Cursor{partitions: partitions}) do
%{query | partition_filter: %{PartitionFilter.all() | partitions: partitions}}
end
defp require_max_records(%Query{max_records: n}) when is_integer(n) and n > 0, do: :ok
defp require_max_records(_), do: {:error, Error.from_result_code(:max_records_required)}
defp activate_record_budget(tracker, %Scan{max_records: n}) when is_integer(n) and n > 0 do
%{tracker | record_count: 0}
end
defp activate_record_budget(tracker, %Query{max_records: n}) when is_integer(n) and n > 0 do
%{tracker | record_count: 0}
end
defp activate_record_budget(tracker, _scannable), do: tracker
defp apply_optional_cursor(scannable, nil), do: {:ok, scannable}
defp apply_optional_cursor(scannable, %Cursor{} = cursor),
do: {:ok, attach_cursor_partition_filter(scannable, cursor)}
defp apply_optional_cursor(scannable, bin) when is_binary(bin) do
case Cursor.decode(bin) do
{:ok, cursor} -> {:ok, attach_cursor_partition_filter(scannable, cursor)}
{:error, _} = err -> err
end
end
defp apply_optional_cursor(_scannable, other) do
{:error,
Error.from_result_code(:parameter_error, message: "invalid cursor: #{inspect(other)}")}
end
defp attach_cursor_partition_filter(%Query{} = query, %Cursor{partitions: partitions}) do
%{query | partition_filter: %{PartitionFilter.all() | partitions: partitions}}
end
defp attach_cursor_partition_filter(%Scan{} = scan, %Cursor{partitions: partitions}) do
%{scan | partition_filter: %{PartitionFilter.all() | partitions: partitions}}
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 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 page_command do
StreamingCommand.new!(
name: __MODULE__,
build_request: fn %{scannable: scannable, node_request: node_request} ->
build_wire(scannable, node_request.node_partitions, node_request.policy)
end,
init: fn _input -> {[], []} end,
consume_frame: fn body,
%{scannable: scannable, node_request: node_request},
{records, parts} ->
with {:ok, new_records, new_parts, done?} <-
ScanResponse.parse_stream_chunk(body, namespace(scannable), set(scannable)) do
next_records = Enum.reverse(new_records, records)
next_parts = Enum.reverse(new_parts, parts)
if done? do
{:halt, page_result(next_records, next_parts, node_request)}
else
{:cont, {next_records, next_parts}}
end
end
end,
finish: fn {records, parts}, %{node_request: node_request} ->
page_result(records, parts, node_request)
end,
error_result: fn err, %{node_request: node_request} ->
{:error, err, node_request.node_partitions}
end
)
end
defp page_result(records, parts, node_request) do
{:ok, Enum.reverse(records), Enum.reverse(parts), node_request.node_partitions}
end
defp request_partitions(%NodePartitions{
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 partition_entry(%{id: id} = ps) do
%{
id: id,
digest: Map.get(ps, :digest),
bval: Map.get(ps, :bval)
}
end
defp unknown_node(node_name) do
Error.from_result_code(:invalid_node, message: "query target node unavailable: #{node_name}")
end
defp operation_name(%Scan{}), do: "scan"
defp operation_name(%Query{}), do: "query"
end