Current section
Files
Jump to
Current section
Files
lib/aerospike/command/batch.ex
defmodule Aerospike.Command.Batch do
@moduledoc false
alias Aerospike.BatchResult
alias Aerospike.Cluster
alias Aerospike.Cluster.Tender
alias Aerospike.Command.BatchCommand
alias Aerospike.Command.BatchCommand.Entry
alias Aerospike.Command.BatchCommand.NodeRequest
alias Aerospike.Command.BatchCommand.NodeResult
alias Aerospike.Command.BatchCommand.Regroup
alias Aerospike.Command.BatchCommand.Result
alias Aerospike.Command.BatchRouter
alias Aerospike.Error
alias Aerospike.Policy
alias Aerospike.Protocol.Batch, as: BatchProtocol
alias Aerospike.Protocol.Response
alias Aerospike.Runtime.Executor
alias Aerospike.RuntimeMetrics
@type result :: {:ok, [Result.t()]} | {:error, Error.t()} | {:error, :cluster_not_ready}
@doc false
@spec to_public_results([Result.t()]) :: [BatchResult.t()]
def to_public_results(results) when is_list(results) do
BatchResult.from_command_results(results)
end
@spec execute(GenServer.server(), [Entry.t()], keyword()) :: result()
def execute(tender, entries, opts \\ [])
def execute(_tender, [], opts) when is_list(opts) do
with {:ok, _policy} <- Policy.batch(opts) do
{:ok, []}
end
end
def execute(tender, entries, opts) when is_list(entries) and is_list(opts) do
with :ok <- validate_entries(entries),
{:ok, _validated} <- Policy.batch(opts),
{:ok, policy} <- batch_policy(tender, opts),
{:ok, grouping} <- BatchRouter.group_entries(runtime_ctx(tender).tables, entries) do
runtime = runtime_ctx(tender)
executor = Executor.new!(policy: policy)
start_mono = System.monotonic_time()
result =
BatchCommand.run(
command(policy),
executor,
%{
entries: entries,
routing_failures: grouping.routing_failures,
allow_partial_results: policy.allow_partial_results
},
grouping.node_requests,
%{
tender: runtime.tender,
transport: runtime.transport,
max_concurrency: max_concurrency(policy, grouping.node_requests),
reroute_request: &reroute_request(runtime.tables, &1, &2, &3),
metrics_cluster: runtime.tender
}
)
RuntimeMetrics.record_command(runtime.tender, __MODULE__, start_mono, result)
result
end
end
defp command(%Policy.Batch{} = policy) do
BatchCommand.new!(
name: __MODULE__,
transport_mode: :command_stream,
build_request:
&BatchProtocol.encode_request(&1,
timeout: policy.timeout,
allow_inline: policy.allow_inline,
allow_inline_ssd: policy.allow_inline_ssd,
respond_all_keys: policy.respond_all_keys
),
parse_response: &Response.parse_batch_response/2,
merge_results: &merge_results/2
)
end
defp merge_results(node_results, %{
entries: entries,
routing_failures: routing_failures,
allow_partial_results: allow_partial_results
}) do
with :ok <- maybe_reject_partial_results(node_results, allow_partial_results) do
routing_failures
|> Enum.reduce(%{}, &put_routing_failure/2)
|> merge_node_results(node_results)
|> ordered_results(entries)
end
end
defp max_concurrency(%Policy.Batch{max_concurrent_nodes: 0}, node_requests) do
max(length(node_requests), 1)
end
defp max_concurrency(%Policy.Batch{max_concurrent_nodes: max_concurrent_nodes}, _node_requests),
do: max_concurrent_nodes
defp maybe_reject_partial_results(_node_results, true), do: :ok
defp maybe_reject_partial_results(node_results, false) do
case Enum.find(node_results, &node_result_error?/1) do
nil -> :ok
%NodeResult{result: {:error, %Error{}} = err} -> err
%NodeResult{result: {:error, reason}} when is_atom(reason) -> {:error, reason}
end
end
defp node_result_error?(%NodeResult{result: {:error, _reason}}), do: true
defp node_result_error?(%NodeResult{}), do: false
defp put_routing_failure(
%BatchRouter.RoutingFailure{entry: %Entry{} = entry, reason: reason},
acc
) do
Map.put(acc, entry.index, error_result(entry, reason))
end
defp merge_node_results(acc, node_results) do
Enum.reduce(node_results, acc, &put_node_result(&2, &1))
end
defp put_node_result(
acc,
%NodeResult{
request: %NodeRequest{entries: entries},
result: {:ok, %BatchProtocol.Reply{results: results}}
}
) do
result_map = Map.new(results, &{&1.index, &1})
Enum.reduce(entries, acc, fn %Entry{} = entry, next_acc ->
Map.put(next_acc, entry.index, Map.get(result_map, entry.index, missing_result(entry)))
end)
end
defp put_node_result(acc, %NodeResult{request: %NodeRequest{entries: entries}, result: error}) do
Enum.reduce(entries, acc, fn %Entry{} = entry, next_acc ->
Map.put(next_acc, entry.index, error_result(entry, error))
end)
end
defp ordered_results(outcomes, entries) do
case ordered_results_if_sorted(outcomes, entries, -1, []) do
{:ok, ordered} ->
{:ok, ordered}
:unsorted ->
sorted_ordered_results(outcomes, entries)
end
end
defp ordered_results_if_sorted(_outcomes, [], _previous_index, acc) do
{:ok, Enum.reverse(acc)}
end
defp ordered_results_if_sorted(
outcomes,
[%Entry{index: index} = entry | rest],
previous_index,
acc
)
when index >= previous_index do
result = Map.get(outcomes, index, missing_merge_result(entry))
ordered_results_if_sorted(outcomes, rest, index, [result | acc])
end
defp ordered_results_if_sorted(_outcomes, [%Entry{} | _rest], _previous_index, _acc) do
:unsorted
end
defp sorted_ordered_results(outcomes, entries) do
ordered =
entries
|> Enum.sort_by(& &1.index)
|> Enum.map(fn %Entry{index: index} = entry ->
Map.get(outcomes, index, missing_merge_result(entry))
end)
{:ok, ordered}
end
defp reroute_request(tables, _reroute_kind, %NodeRequest{} = node_request, next_attempt) do
entries = Enum.map(node_request.entries, &reroute_entry(&1, next_attempt))
case BatchRouter.group_entries(tables, entries) do
{:ok, grouping} ->
{:ok,
%Regroup{
node_requests: grouping.node_requests,
node_results:
Enum.map(
grouping.routing_failures,
&routing_failure_node_result(node_request, &1, next_attempt)
)
}}
{:error, :cluster_not_ready} = err ->
err
end
end
defp reroute_entry(%Entry{dispatch: {:read, replica_policy, _attempt}} = entry, next_attempt) do
%{entry | dispatch: {:read, replica_policy, next_attempt}}
end
defp reroute_entry(%Entry{} = entry, _next_attempt), do: entry
defp routing_failure_node_result(
%NodeRequest{node_name: node_name},
%BatchRouter.RoutingFailure{entry: %Entry{} = entry, reason: reason},
attempt
) do
%NodeResult{
request: %NodeRequest{node_name: node_name, entries: [entry], payload: nil},
result: {:error, reason},
attempt: attempt
}
end
defp error_result(%Entry{} = entry, {:error, %Error{} = error}) do
%Result{
index: entry.index,
key: entry.key,
kind: entry.kind,
status: :error,
record: nil,
error: error,
in_doubt: error.in_doubt
}
end
defp error_result(%Entry{} = entry, {:error, reason}) when is_atom(reason) do
%Result{
index: entry.index,
key: entry.key,
kind: entry.kind,
status: :error,
record: nil,
error: reason,
in_doubt: false
}
end
defp error_result(%Entry{} = entry, reason) when is_atom(reason) do
error_result(entry, {:error, reason})
end
defp missing_result(%Entry{} = entry) do
error_result(
entry,
{:error,
Error.from_result_code(:parse_error,
message: "batch reply omitted requested index #{entry.index}"
)}
)
end
defp missing_merge_result(%Entry{} = entry) do
error_result(
entry,
{:error,
Error.from_result_code(:parse_error,
message: "batch merge omitted requested index #{entry.index}"
)}
)
end
defp validate_entries(entries) do
if Enum.all?(entries, &match?(%Entry{}, &1)) do
:ok
else
invalid_argument("Aerospike.Command.Batch.execute/3 expects a list of batch entries")
end
end
defp batch_policy(tender, opts) do
tender
|> Cluster.retry_policy()
|> Policy.batch(opts)
end
defp runtime_ctx(tender) do
%{
tender: tender,
tables: Cluster.tables(tender),
transport: Tender.transport(tender)
}
end
defp invalid_argument(message) do
{:error, Error.from_result_code(:invalid_argument, message: message)}
end
end