Packages

Aerospike driver for Elixir with an OTP-native cluster runtime

Current section

Files

Jump to
aerospike_driver lib aerospike runtime txn_roll.ex
Raw

lib/aerospike/runtime/txn_roll.ex

defmodule Aerospike.Runtime.TxnRoll do
@moduledoc false
import Bitwise
alias Aerospike.Command.UnaryCommand
alias Aerospike.Command.UnarySupport
alias Aerospike.Error
alias Aerospike.Key
alias Aerospike.Protocol.AsmMsg
alias Aerospike.Protocol.AsmMsg.Field
alias Aerospike.Protocol.Message
alias Aerospike.Protocol.Response
alias Aerospike.Runtime.TxnMonitor
alias Aerospike.Runtime.TxnOps
alias Aerospike.Txn
@doc false
@spec commit(atom(), Txn.t(), keyword()) ::
{:ok, :committed | :already_committed} | {:error, Error.t()}
def commit(conn_name, %Txn{} = txn, opts \\ []) do
case TxnOps.get_tracking(conn_name, txn) do
{:ok, %{state: :committed}} ->
{:ok, :already_committed}
{:ok, %{state: :aborted}} ->
{:error, Error.from_result_code(:txn_already_aborted)}
{:ok, %{state: :verified}} ->
handle_roll_forward_mark(conn_name, txn, opts)
{:ok, _} ->
do_commit(conn_name, txn, opts)
{:error, :not_found} ->
{:error, %Error{code: :parameter_error, message: "transaction not initialized"}}
end
end
@doc false
@spec abort(atom(), Txn.t(), keyword()) ::
{:ok, :aborted | :already_aborted} | {:error, Error.t()}
def abort(conn_name, %Txn{} = txn, opts \\ []) do
case TxnOps.get_tracking(conn_name, txn) do
{:ok, %{state: :aborted}} ->
{:ok, :already_aborted}
{:ok, %{state: :committed}} ->
{:error, Error.from_result_code(:txn_already_committed)}
{:ok, _} ->
do_abort(conn_name, txn, opts)
{:error, :not_found} ->
{:error, %Error{code: :parameter_error, message: "transaction not initialized"}}
end
end
@doc false
@spec txn_status(atom(), Txn.t()) ::
{:ok, :open | :verified | :committed | :aborted} | {:error, Error.t()}
def txn_status(conn_name, %Txn{} = txn) do
case TxnOps.get_tracking(conn_name, txn) do
{:ok, %{state: state}} ->
{:ok, state}
{:error, :not_found} ->
{:error, %Error{code: :parameter_error, message: "transaction not initialized"}}
end
end
@doc false
@spec transaction(atom(), Txn.t() | keyword(), (Txn.t() -> term())) ::
{:ok, term()} | {:error, Error.t()}
def transaction(conn_name, txn_or_opts, fun)
def transaction(conn_name, %Txn{} = txn, fun) when is_function(fun, 1) do
TxnOps.init_tracking(conn_name, txn)
run_transaction(conn_name, txn, fun)
end
def transaction(conn_name, opts, fun) when is_list(opts) and is_function(fun, 1) do
txn = Txn.new(opts)
TxnOps.init_tracking(conn_name, txn)
run_transaction(conn_name, txn, fun)
end
defp run_transaction(conn_name, txn, fun) do
result =
try do
{:ok, fun.(txn)}
rescue
e in Aerospike.Error ->
abort(conn_name, txn, [])
{:error, e}
e ->
abort(conn_name, txn, [])
{:raise, e, __STACKTRACE__}
catch
kind, reason ->
abort(conn_name, txn, [])
{:throw_or_exit, kind, reason, __STACKTRACE__}
end
case result do
{:ok, value} ->
case commit(conn_name, txn, []) do
{:ok, _} ->
{:ok, value}
{:error, _} = err ->
best_effort_abort(conn_name, txn)
err
end
{:error, _} = err ->
err
{:raise, e, stacktrace} ->
reraise e, stacktrace
{:throw_or_exit, kind, reason, stacktrace} ->
:erlang.raise(kind, reason, stacktrace)
end
end
defp do_commit(conn_name, txn, opts) do
case verify_reads(conn_name, txn, opts) do
:ok ->
TxnOps.set_state(conn_name, txn, :verified)
handle_roll_forward_mark(conn_name, txn, opts)
{:error, _} = verify_err ->
TxnOps.set_state(conn_name, txn, :aborted)
roll_writes(conn_name, txn, opts, :back)
close_if_needed(conn_name, txn, opts)
TxnOps.cleanup(conn_name, txn)
verify_err
end
end
defp handle_roll_forward_mark(conn_name, txn, opts) do
if TxnOps.monitor_exists?(conn_name, txn) do
case TxnMonitor.mark_roll_forward(conn_name, txn, opts) do
:ok ->
finish_commit(conn_name, txn, opts)
{:error, %Error{code: :mrt_committed}} ->
finish_commit(conn_name, txn, opts)
{:error, %Error{code: :mrt_aborted}} ->
TxnOps.set_state(conn_name, txn, :aborted)
{:error, Error.from_result_code(:mrt_aborted)}
{:error, %Error{} = err} ->
maybe_set_in_doubt(conn_name, txn, err)
{:error, err}
end
else
finish_commit(conn_name, txn, opts)
end
end
defp maybe_set_in_doubt(conn_name, txn, %Error{in_doubt: true}),
do: TxnOps.set_write_in_doubt(conn_name, txn)
defp maybe_set_in_doubt(_conn_name, _txn, _err), do: :ok
defp finish_commit(conn_name, txn, opts) do
TxnOps.set_state(conn_name, txn, :committed)
roll_writes(conn_name, txn, opts, :forward)
close_if_needed(conn_name, txn, opts)
TxnOps.cleanup(conn_name, txn)
{:ok, :committed}
end
defp do_abort(conn_name, txn, opts) do
TxnOps.set_state(conn_name, txn, :aborted)
roll_writes(conn_name, txn, opts, :back)
close_if_needed(conn_name, txn, opts)
TxnOps.cleanup(conn_name, txn)
{:ok, :aborted}
end
defp verify_reads(conn_name, txn, opts) do
reads = TxnOps.get_reads(conn_name, txn)
Enum.reduce_while(reads, :ok, fn {key, version}, :ok ->
case run_verify_command(conn_name, key, opts, %{key: key, version: version}) do
{:ok, _} -> {:cont, :ok}
{:error, _} = err -> {:halt, err}
end
end)
end
defp run_verify_command(conn_name, route_key, opts, input) do
with {:ok, policy} <- UnarySupport.read_policy(conn_name, opts) do
UnarySupport.run_command(conn_name, route_key, policy, verify_command(), input)
end
end
defp roll_writes(conn_name, txn, opts, direction) do
writes = TxnOps.get_writes(conn_name, txn)
Enum.each(writes, fn key ->
_ =
run_roll_command(conn_name, key, opts, %{key: key, txn_id: txn.id, direction: direction})
end)
end
defp best_effort_abort(conn_name, txn) do
_ = abort(conn_name, txn, [])
_ = TxnOps.cleanup(conn_name, txn)
:ok
end
defp close_if_needed(conn_name, txn, opts) do
if TxnOps.close_monitor?(conn_name, txn) do
TxnMonitor.close(conn_name, txn, opts)
end
end
defp verify_command do
UnaryCommand.new!(
name: __MODULE__,
dispatch: :read,
build_request: &encode_verify_request/1,
parse_response: &parse_verify_response/2
)
end
defp roll_command do
UnaryCommand.new!(
name: __MODULE__,
dispatch: :write,
build_request: &encode_roll_request/1,
parse_response: &parse_roll_response/2
)
end
defp run_roll_command(conn_name, route_key, opts, input) do
with {:ok, policy} <- UnarySupport.write_policy(conn_name, opts) do
UnarySupport.run_command(conn_name, route_key, policy, roll_command(), input)
end
end
defp encode_verify_request(%{key: %Key{} = key, version: version}) do
encode_verify_msg(key, version)
end
defp encode_roll_request(%{key: %Key{} = key, txn_id: txn_id, direction: direction}) do
encode_roll_msg(key, txn_id, direction)
end
defp parse_verify_response(body, _input) do
UnarySupport.parse_as_msg(body, fn msg ->
case Response.parse_record_metadata_response(msg) do
{:ok, _} -> {:ok, :ok}
{:error, _} = err -> err
end
end)
end
defp parse_roll_response(body, _input) do
UnarySupport.parse_as_msg(body, fn msg ->
case Response.parse_record_metadata_response(msg) do
{:ok, _} -> {:ok, :ok}
{:error, _} = err -> err
end
end)
end
@doc false
def encode_verify_msg(%Key{} = key, version) do
%AsmMsg{
info1: AsmMsg.info1_read() ||| AsmMsg.info1_nobindata(),
info3: AsmMsg.info3_sc_read_type(),
info4: AsmMsg.info4_mrt_verify_read(),
fields: [
Field.namespace(key.namespace),
Field.set(key.set),
Field.digest(key.digest),
Field.record_version(version)
]
}
|> AsmMsg.encode()
|> Message.encode_as_msg_iodata()
end
@doc false
def encode_roll_msg(%Key{} = key, txn_id, :forward) do
encode_roll_msg_with_info4(key, txn_id, AsmMsg.info4_mrt_roll_forward())
end
@doc false
def encode_roll_msg(%Key{} = key, txn_id, :back) do
encode_roll_msg_with_info4(key, txn_id, AsmMsg.info4_mrt_roll_back())
end
defp encode_roll_msg_with_info4(%Key{} = key, txn_id, info4) do
%AsmMsg{
info2: AsmMsg.info2_write() ||| AsmMsg.info2_durable_delete(),
info4: info4,
fields: [
Field.namespace(key.namespace),
Field.set(key.set),
Field.digest(key.digest),
Field.mrt_id(txn_id)
]
}
|> AsmMsg.encode()
|> Message.encode_as_msg_iodata()
end
end