defmodule Aerospike.Protocol.BatchEncoder do @moduledoc false # Encodes batch-any requests (BATCH_INDEX field, type 41) for multi-key commands. import Bitwise alias Aerospike.Batch alias Aerospike.Exp alias Aerospike.Key alias Aerospike.Protocol.AsmMsg alias Aerospike.Protocol.AsmMsg.Field alias Aerospike.Protocol.AsmMsg.Operation alias Aerospike.Protocol.AsmMsg.Value alias Aerospike.Protocol.Message alias Aerospike.Protocol.MessagePack alias Aerospike.Protocol.OperateFlags @batch_msg_repeat 0x01 @batch_msg_info 0x02 @batch_msg_gen 0x04 @batch_msg_ttl 0x08 @doc false @spec encode_batch_get([{non_neg_integer(), Key.t()}], keyword()) :: iodata() def encode_batch_get(indexed_keys, merged_opts) when is_list(indexed_keys) do {read_attr, bin_ops} = read_attr_and_ops_for_get(merged_opts) op_count = length(bin_ops) entry_fn = fn %Key{} = key, prev -> read_entry_bytes(key, read_attr, op_count, bin_ops, merged_opts, prev) end body = batch_index_body_keys(indexed_keys, merged_opts, entry_fn) wrap_batch_message(body, merged_opts) end @doc false @spec encode_batch_exists([{non_neg_integer(), Key.t()}], keyword()) :: iodata() def encode_batch_exists(indexed_keys, merged_opts) when is_list(indexed_keys) do read_attr = AsmMsg.info1_read() ||| AsmMsg.info1_nobindata() entry_fn = fn %Key{} = key, prev -> read_entry_bytes(key, read_attr, 0, [], merged_opts, prev) end body = batch_index_body_keys(indexed_keys, merged_opts, entry_fn) wrap_batch_message(body, merged_opts) end defp read_attr_and_ops_for_get(opts) do cond do Keyword.get(opts, :header_only) == true -> {AsmMsg.info1_read() ||| AsmMsg.info1_nobindata(), []} is_list(Keyword.get(opts, :bins)) and Keyword.get(opts, :bins) != [] -> ops = opts |> Keyword.fetch!(:bins) |> Enum.map(&bin_to_string/1) |> Enum.sort() |> Enum.map(&Operation.read/1) {AsmMsg.info1_read(), ops} true -> {AsmMsg.info1_read() ||| AsmMsg.info1_get_all(), []} end end defp bin_to_string(b) when is_binary(b), do: b defp bin_to_string(a) when is_atom(a), do: Atom.to_string(a) defp read_entry_bytes(key, read_attr, op_count, bin_ops, opts, prev) do if repeat_ns_set?(prev, key, false, opts) do <<@batch_msg_repeat::8>> else exp = Keyword.get(opts, :read_touch_ttl_percent, 0) fields_part = batch_key_fields(key, 0, op_count, opts) ops_io = if op_count == 0, do: [], else: Enum.map(bin_ops, &Operation.encode/1) [ <<@batch_msg_info ||| @batch_msg_ttl::8, read_attr::8, 0::8, 0::8, exp::32-big>>, fields_part, ops_io ] end end defp repeat_ns_set?(%Key{} = a, %Key{} = b, send_key?, opts) do send_key? == Keyword.get(opts, :send_key, false) and a.namespace == b.namespace and a.set == b.set end defp repeat_ns_set?(nil, _b, _, _), do: false @doc false @spec encode_batch_operate([{non_neg_integer(), Batch.t()}], keyword()) :: iodata() def encode_batch_operate(indexed_ops, merged_opts) when is_list(indexed_ops) do body = batch_index_body_operate(indexed_ops, merged_opts) wrap_batch_message(body, merged_opts) end defp batch_index_body_keys(indexed_keys, merged_opts, entry_fn) do max = length(indexed_keys) flags = batch_flags_byte(merged_opts) {entries_io, _} = Enum.reduce(indexed_keys, {[], nil}, fn {idx, key}, {acc, prev} -> inner = entry_fn.(key, prev) {[acc, <>, inner], key} end) [<>, entries_io] end defp batch_index_body_operate(indexed_ops, global_opts) do max = length(indexed_ops) flags = batch_flags_byte(global_opts) # Do not carry `prev` across heterogeneous records. Reusing the batch "repeat" # byte between e.g. read -> write would inherit the wrong sub-message shape and # the server can mis-handle later ops (writes/deletes appearing to succeed locally # while the record is unchanged). entries_io = Enum.map(indexed_ops, fn {idx, batch_rec} -> {chunk, _} = operate_chunk(idx, batch_rec, global_opts, nil) chunk end) [<>, entries_io] end defp operate_chunk(idx, %Batch.Read{key: key, opts: o}, global_opts, prev) do merged = Keyword.merge(global_opts, o) {read_attr, bin_ops} = read_attr_and_ops_for_get(merged) op_count = length(bin_ops) inner = if repeat_ns_set?(prev, key, false, merged) do <<@batch_msg_repeat::8>> else read_entry_bytes(key, read_attr, op_count, bin_ops, merged, nil) end {[<>, inner], key} end defp operate_chunk(idx, %Batch.Put{key: key, bins: bins, opts: o}, global_opts, prev) do merged = Keyword.merge(global_opts, o) ops = Value.encode_bin_operations(bins) inner = if repeat_ns_set?(prev, key, Keyword.get(merged, :send_key, false), merged) do <<@batch_msg_repeat::8>> else {r, w, i, gen, exp} = put_write_tuple(merged) fields = batch_key_fields(key, 0, length(ops), merged) ops_io = Enum.map(ops, &Operation.encode/1) [ <<@batch_msg_info ||| @batch_msg_gen ||| @batch_msg_ttl::8, r::8, w::8, i::8, gen::16-big, exp::32-big>>, fields, ops_io ] end {[<>, inner], key} end defp operate_chunk(idx, %Batch.Delete{key: key, opts: o}, global_opts, prev) do merged = Keyword.merge(global_opts, o) inner = if repeat_ns_set?(prev, key, Keyword.get(merged, :send_key, false), merged) do <<@batch_msg_repeat::8>> else w = AsmMsg.info2_write() ||| AsmMsg.info2_respond_all_ops() ||| AsmMsg.info2_delete() w = if Keyword.get(merged, :durable_delete), do: w ||| AsmMsg.info2_durable_delete(), else: w fields = batch_key_fields(key, 0, 0, merged) [ <<@batch_msg_info ||| @batch_msg_gen ||| @batch_msg_ttl::8, 0::8, w::8, 0::8, 0::16-big, 0::32-big>>, fields ] end {[<>, inner], key} end defp operate_chunk(idx, %Batch.Operate{key: key, ops: ops, opts: o}, global_opts, prev) do merged = Keyword.merge(global_opts, o) st = OperateFlags.scan_ops(ops) inner = if repeat_ns_set?(prev, key, Keyword.get(merged, :send_key, false), merged) do <<@batch_msg_repeat::8>> else operate_inner_bytes(key, ops, st, merged) end {[<>, inner], key} end defp operate_chunk( idx, %Batch.UDF{key: key, package: pkg, function: f, args: args, opts: o}, global_opts, prev ) do merged = Keyword.merge(global_opts, o) inner = if repeat_ns_set?(prev, key, Keyword.get(merged, :send_key, false), merged) do <<@batch_msg_repeat::8>> else w = AsmMsg.info2_write() w = if Keyword.get(merged, :durable_delete), do: w ||| AsmMsg.info2_durable_delete(), else: w i = if Keyword.get(merged, :commit_master), do: AsmMsg.info3_commit_master(), else: 0 exp = Keyword.get(merged, :ttl, 0) fields_inner = batch_key_fields(key, 3, 0, merged) udf_fields = udf_extra_fields(pkg, f, args) [ <<@batch_msg_info ||| @batch_msg_gen ||| @batch_msg_ttl::8, 0::8, w::8, i::8, 0::16-big, exp::32-big>>, fields_inner, udf_fields ] end {[<>, inner], key} end defp operate_inner_bytes(key, ops, st, merged) do info1 = if st.header_only? do st.info1 ||| AsmMsg.info1_nobindata() else st.info1 end info2 = if st.has_write?, do: st.info2 ||| AsmMsg.info2_write(), else: st.info2 info2 = if respond_all_ops?(merged, st, info1) do info2 ||| AsmMsg.info2_respond_all_ops() else info2 end ops_io = Enum.map(ops, &Operation.encode/1) fields = batch_key_fields(key, 0, length(ops), merged) if st.has_write? do r = if st.read_bin? or st.read_header?, do: info1, else: 0 {w, i, gen} = inner_write_flags(merged, info2) exp_rw = Keyword.get(merged, :ttl, 0) [ <<@batch_msg_info ||| @batch_msg_gen ||| @batch_msg_ttl::8, r::8, w::8, i::8, gen::16-big, exp_rw::32-big>>, fields, ops_io ] else exp = Keyword.get(merged, :read_touch_ttl_percent, 0) [ <<@batch_msg_info ||| @batch_msg_ttl::8, info1::8, 0::8, 0::8, exp::32-big>>, fields, ops_io ] end end defp inner_write_flags(merged, w0) do w = w0 |> maybe_gen_bits(merged) |> maybe_durable(merged) |> maybe_create_only(merged) i = case Keyword.get(merged, :exists) do :update_only -> AsmMsg.info3_update_only() :replace_only -> AsmMsg.info3_replace_only() :create_or_replace -> AsmMsg.info3_create_or_replace() _ -> 0 end gen = case Keyword.get(merged, :generation) do g when is_integer(g) -> g _ -> 0 end {w, i, gen} end defp maybe_gen_bits(w, opts) do g = Keyword.get(opts, :generation) pol = case Keyword.get(opts, :gen_policy) do nil when is_integer(g) -> :expect_gen_equal nil -> :none other -> other end case {pol, g} do {:expect_gen_equal, gv} when is_integer(gv) -> w ||| AsmMsg.info2_generation() {:expect_gen_gt, gv} when is_integer(gv) -> w ||| AsmMsg.info2_generation_gt() _ -> w end end defp maybe_durable(w, opts) do if Keyword.get(opts, :durable_delete), do: w ||| AsmMsg.info2_durable_delete(), else: w end defp maybe_create_only(w, opts) do if Keyword.get(opts, :exists) == :create_only, do: w ||| AsmMsg.info2_create_only(), else: w end defp put_write_tuple(opts) do w0 = AsmMsg.info2_write() ||| AsmMsg.info2_respond_all_ops() {w, i, gen} = inner_write_flags(opts, w0) exp = Keyword.get(opts, :ttl, 0) {0, w, i, gen, exp} end defp respond_all_ops?(merged, st, info1) do want? = st.respond_all? or Keyword.get(merged, :respond_per_each_op, false) get_all? = (info1 &&& AsmMsg.info1_get_all()) != 0 want? and not get_all? end defp udf_extra_fields(package, function, args) do arg_body = args |> Enum.map(&pack_udf_arg/1) |> MessagePack.pack!() [ Field.encode(%Field{type: Field.type_udf_package_name(), data: package}), Field.encode(%Field{type: Field.type_udf_function(), data: function}), Field.encode(%Field{type: Field.type_udf_arglist(), data: arg_body}) ] end defp pack_udf_arg(s) when is_binary(s), do: {:particle_string, s} defp pack_udf_arg({:bytes, b}) when is_binary(b), do: {:bytes, b} defp pack_udf_arg(nil), do: nil defp pack_udf_arg(true), do: true defp pack_udf_arg(false), do: false defp pack_udf_arg(n) when is_integer(n), do: n defp pack_udf_arg(f) when is_float(f), do: f defp pack_udf_arg(list) when is_list(list), do: Enum.map(list, &pack_udf_arg/1) defp pack_udf_arg(%{} = map) do Map.new(map, fn {k, v} -> {pack_udf_arg(k), pack_udf_arg(v)} end) end defp batch_flags_byte(opts) do if Keyword.get(opts, :respond_all_keys, true), do: 0x04, else: 0 end defp batch_key_fields(%Key{} = key, extra_fc, op_count, opts) do send_key? = Keyword.get(opts, :send_key, false) fc_arg = extra_fc + if(send_key? and key.user_key != nil, do: 1, else: 0) written_fc = fc_arg + 2 base = [ <>, Field.encode(Field.namespace(key.namespace)), Field.encode(Field.set(key.set)) ] if send_key? and key.user_key != nil do case Field.key_from_user_key(%{user_key: key.user_key}) do nil -> base kf -> [base, Field.encode(kf)] end else base end end defp wrap_batch_message(batch_index_body, opts) do timeout = Keyword.get(opts, :timeout, 0) filter_io = case Keyword.get(opts, :filter) do %Exp{wire: wire} when is_binary(wire) -> Field.encode(%Field{type: Field.type_filter_exp(), data: wire}) _ -> [] end field_count = if filter_io == [], do: 1, else: 2 batch_field_data_len = IO.iodata_length(batch_index_body) + 1 as_body = [ <<22::8, AsmMsg.info1_batch()::8, 0::8, 0::8, 0::8, 0::8, 0::32-big, 0::32-big, timeout::32-signed-big, field_count::16-big, 0::16-big>>, <>, batch_index_body, filter_io ] Message.encode_as_msg_iodata(as_body) end end