defmodule Edeliver.Relup.InsertInstruction do @moduledoc """ Provides functions to insert relup instructions at a given position which can be used in `Edeliver.Relup.Instruction` behaviour implementations in the relup file. """ alias Edeliver.Relup.Instructions @doc """ Inserts instruction(s) before the point of no return. All instructions running before that point of no return which fail will cause the upgrade to fail, while failing instructions running after that point will cause the node to restart the release. """ @spec insert_before_point_of_no_return(Instructions.t|Instructions.instructions, new_instructions::Instructions.instruction|Instructions.instructions) :: updated_instructions::Instructions.t|Instructions.instructions def insert_before_point_of_no_return(instructions = %Instructions{}, new_instructions) do %{instructions| up_instructions: insert_before_point_of_no_return(instructions.up_instructions, new_instructions), down_instructions: insert_before_point_of_no_return(instructions.down_instructions, new_instructions) } end def insert_before_point_of_no_return(existing_instructions, new_instructions) do insert_before_instruction(existing_instructions, new_instructions, :point_of_no_return) end @doc """ Inserts instruction(s) right after the point of no return. This means that it is the first instruction which should not fail, because the release handler will restart the release if any instruction fails after the point of no return. """ @spec insert_after_point_of_no_return(Instructions.t|Instructions.instructions, new_instructions::Instructions.instruction|Instructions.instructions) :: updated_instructions::Instructions.t|Instructions.instructions def insert_after_point_of_no_return(instructions = %Instructions{}, new_instructions) do %{instructions| up_instructions: insert_after_point_of_no_return(instructions.up_instructions, new_instructions), down_instructions: insert_after_point_of_no_return(instructions.down_instructions, new_instructions) } end def insert_after_point_of_no_return(existing_instructions, new_instructions) do insert_after_instruction(existing_instructions, new_instructions, :point_of_no_return) end @doc """ Inserts instruction(s) right after the last `load_object_code` instruction which is usually before the "point of no return" and one of the first instructions. This means that it is the first custom instruction which is executed. It is executed twice, once when checking whether the upgrade can be installed and once when the upgrade is installed. """ @spec insert_after_load_object_code(Instructions.t|Instructions.instructions, new_instructions::Instructions.instruction|Instructions.instructions) :: updated_instructions::Instructions.t|Instructions.instructions def insert_after_load_object_code(instructions = %Instructions{}, new_instructions) do %{instructions| up_instructions: insert_after_load_object_code(instructions.up_instructions, new_instructions), down_instructions: insert_after_load_object_code(instructions.down_instructions, new_instructions) } end def insert_after_load_object_code(existing_instructions, new_instructions) do last_load_object_code_instruction = existing_instructions |> Enum.reverse |> List.keyfind(:load_object_code, 0) if last_load_object_code_instruction do insert_after_instruction(existing_instructions, new_instructions, last_load_object_code_instruction) else append(existing_instructions, new_instructions) end end @doc """ Appends instruction(s) to the instruction after the "point of no return" but before any instruction which: - loads or unloads new code, which means before any `load_module`, `load`, `add_module`, `delete_module`, `remove`, `purge` instruction and - before any instruction which updates, starts or stops any running processes, which means before any `code_change`, `update`, `start`, `stop` instruction and - before any instruction which (re-)starts or stops any application or the emulator, which means before any `add_application`, `remove_application`, `restart_application`, `restart_emulator` and `restart_new_emulator` instruction. It does not consider load-instructions for `Edeliver.Relup.RunnableInstruction`s as code loading instructions for the release. They are inserted by the `RunnableInstruction` itself to ensure that the code of the runnable instruction is loaded before the instruction is executed. See `Edeliver.Relup.ShiftInstruction.ensure_module_loaded_before_instruction/3`. """ @spec append_after_point_of_no_return(Instructions.t|Instructions.instructions, new_instructions::Instructions.instruction|Instructions.instructions) :: updated_instructions::Instructions.t|Instructions.instructions def append_after_point_of_no_return(instructions = %Instructions{}, new_instructions) do %{instructions| up_instructions: append_after_point_of_no_return(instructions.up_instructions, new_instructions), down_instructions: append_after_point_of_no_return(instructions.down_instructions, new_instructions) } end def append_after_point_of_no_return(existing_instructions, new_instruction) when is_list(existing_instructions) and not is_list(new_instruction) do append_after_point_of_no_return(existing_instructions, [new_instruction]) end def append_after_point_of_no_return(existing_instructions, new_instructions) when is_list(existing_instructions) do append_after_point_of_no_return(existing_instructions, new_instructions, false, []) end defp append_after_point_of_no_return(_existing_instructions = [:point_of_no_return|rest], new_instructions, _after_point_of_no_return = false, instructions_before_instruction) do append_after_point_of_no_return(rest, new_instructions, true, [:point_of_no_return|instructions_before_instruction]) end defp append_after_point_of_no_return(_existing_instructions = [instruction|rest], new_instructions, after_point_of_no_return = false, instructions_before_instruction) do append_after_point_of_no_return(rest, new_instructions, after_point_of_no_return, [instruction|instructions_before_instruction]) end # skip instructions which loads code and are inserted before a runnable instruction. see `Edeliver.Relup.RunnableInstruction` # and `Edeliver.Relup.Instruction.ensure_module_loaded_before_instruction/3`. That load instructions are inserted by the # `RunnableInstruction` itself and are not considered to be a 'real' code loading instruction for the running application. defp append_after_point_of_no_return(_existing_instructions = [load_runnable_instruction = {:load_module, module}, runnable_instruction = {:apply, {module, :run, _args}}|rest], new_instructions, after_point_of_no_return = true, instructions_before_instruction) do append_after_point_of_no_return(rest, new_instructions, after_point_of_no_return, [runnable_instruction, load_runnable_instruction|instructions_before_instruction]) end defp append_after_point_of_no_return(_existing_instructions = [load_runnable_instruction = {:load_module, module, _dep_mods}, runnable_instruction = {:apply, {module, :run, _args}}|rest], new_instructions, after_point_of_no_return = true, instructions_before_instruction) do append_after_point_of_no_return(rest, new_instructions, after_point_of_no_return, [runnable_instruction, load_runnable_instruction|instructions_before_instruction]) end defp append_after_point_of_no_return(_existing_instructions = [load_runnable_instruction = {:load_module, module, _pre_purge, _post_purge, _dep_mods}, runnable_instruction = {:apply, {module, :run, _args}}|rest], new_instructions, after_point_of_no_return = true, instructions_before_instruction) do append_after_point_of_no_return(rest, new_instructions, after_point_of_no_return, [runnable_instruction, load_runnable_instruction|instructions_before_instruction]) end defp append_after_point_of_no_return(_existing_instructions = [load_runnable_instruction = {:add_module, module}, runnable_instruction = {:apply, {module, :run, _args}}|rest], new_instructions, after_point_of_no_return = true, instructions_before_instruction) do append_after_point_of_no_return(rest, new_instructions, after_point_of_no_return, [runnable_instruction, load_runnable_instruction|instructions_before_instruction]) end defp append_after_point_of_no_return(_existing_instructions = [load_runnable_instruction = {:add_module, module, _dep_mods}, runnable_instruction = {:apply, {module, :run, _args}}|rest], new_instructions, after_point_of_no_return = true, instructions_before_instruction) do append_after_point_of_no_return(rest, new_instructions, after_point_of_no_return, [runnable_instruction, load_runnable_instruction|instructions_before_instruction]) end defp append_after_point_of_no_return(_existing_instructions = [load_runnable_instruction = {:load, {module, _pre_purge, _post_purge}}, runnable_instruction = {:apply, {module, :run, _args}}|rest], new_instructions, after_point_of_no_return = true, instructions_before_instruction) do append_after_point_of_no_return(rest, new_instructions, after_point_of_no_return, [runnable_instruction, load_runnable_instruction|instructions_before_instruction]) end # check whether the instruction is an instruction modifying code, processes or applications defp append_after_point_of_no_return(existing_instructions = [instruction|rest], new_instructions, after_point_of_no_return = true, instructions_before_instruction) do if modifies_code?(instruction) or modifies_processes?(instruction) or modifies_applications?(instruction) do Enum.reverse(instructions_before_instruction) ++ new_instructions ++ existing_instructions else append_after_point_of_no_return(rest, new_instructions, after_point_of_no_return, [instruction|instructions_before_instruction]) end end defp append_after_point_of_no_return(_existing_instructions = [], new_instructions, _after_point_of_no_return, instructions_before_instruction) do Enum.reverse(instructions_before_instruction) ++ new_instructions end @doc """ Appends instruction(s) to the list of other instructions. """ @spec append(Instructions.t|Instructions.instructions, new_instructions::Instructions.instruction|Instructions.instructions) :: updated_instructions::Instructions.t|Instructions.instructions def append(instructions = %Instructions{}, new_instructions) do %{instructions| up_instructions: append(instructions.up_instructions, new_instructions), down_instructions: append(instructions.down_instructions, new_instructions) } end def append(existing_instructions, new_instruction) when is_list(existing_instructions) and not is_list(new_instruction) do append(existing_instructions, [new_instruction]) end def append(existing_instructions, new_instructions) when is_list(existing_instructions) do existing_instructions ++ new_instructions end @doc """ Inserts instruction(s) before the given instruction. """ @spec insert_before_instruction(Instructions.t|Instructions.instructions, new_instructions::Instructions.instruction|Instructions.instructions, before_instruction::Instructions.instruction) :: updated_instructions::Instructions.t|Instructions.instructions def insert_before_instruction(instructions = %Instructions{}, new_instructions, before_instruction) do %{instructions| up_instructions: insert_before_instruction(instructions.up_instructions, new_instructions, before_instruction), down_instructions: insert_after_instruction(instructions.down_instructions, new_instructions, before_instruction) } end def insert_before_instruction(existing_instructions, new_instruction, before_instruction) when is_list(existing_instructions) and not is_list(new_instruction) do insert_before_instruction(existing_instructions, [new_instruction], before_instruction) end def insert_before_instruction(existing_instructions, new_instructions, before_instruction) when is_list(existing_instructions) do insert_before_instruction(existing_instructions, new_instructions, before_instruction, []) end defp insert_before_instruction(existing_instructions = [before_instruction|_], new_instructions, before_instruction, instructions_before_instruction) do Enum.reverse(instructions_before_instruction) ++ new_instructions ++ existing_instructions end defp insert_before_instruction(_existing_instructions = [no_point_of_no_return_instruction|rest], new_instructions, before_instruction, instructions_before_instruction) do insert_before_instruction(rest, new_instructions, before_instruction, [no_point_of_no_return_instruction|instructions_before_instruction]) end defp insert_before_instruction(_existing_instructions = [], new_instructions, _before_instruction, instructions_before_instruction) do Enum.reverse(instructions_before_instruction) ++ new_instructions end @doc """ Inserts instruction(s) after the given instruction. """ @spec insert_after_instruction(Instructions.t|Instructions.instructions, new_instructions::Instructions.instruction|Instructions.instructions, after_instruction::Instructions.instruction) :: updated_instructions::Instructions.t|Instructions.instructions def insert_after_instruction(instructions = %Instructions{}, new_instructions, after_instruction) do %{instructions| up_instructions: insert_after_instruction(instructions.up_instructions, new_instructions, after_instruction), down_instructions: insert_before_instruction(instructions.down_instructions, new_instructions, after_instruction) } end def insert_after_instruction(existing_instructions, new_instruction, after_instruction) when is_list(existing_instructions) and not is_list(new_instruction) do insert_after_instruction(existing_instructions, [new_instruction], after_instruction) end def insert_after_instruction(existing_instructions, new_instructions, after_instruction) when is_list(existing_instructions) do insert_after_instruction(existing_instructions, new_instructions, after_instruction, []) end defp insert_after_instruction(_existing_instructions = [after_instruction|rest], new_instructions, after_instruction, instructions_before_instruction) do Enum.reverse(instructions_before_instruction) ++ [after_instruction|new_instructions] ++ rest end defp insert_after_instruction(_existing_instructions = [no_point_of_no_return_instruction|rest], new_instructions, after_instruction, instructions_before_instruction) do insert_after_instruction(rest, new_instructions, after_instruction, [no_point_of_no_return_instruction|instructions_before_instruction]) end defp insert_after_instruction(_existing_instructions = [], new_instructions, _after_instruction, instructions_before_instruction) do Enum.reverse(instructions_before_instruction) ++ new_instructions end @doc """ Returns true if the given instruction is an instruction which modifies an application by either (re-)starting or stopping it or by restarting the emulator. It returns `true` for the `add_application`, `remove_application`, `restart_new_emulator` and the `restart_emulator`, relup instructions. """ @spec modifies_applications?(Instructions.instruction) :: boolean def modifies_applications?({:add_application, _application}), do: true def modifies_applications?({:add_application, _application, _type}), do: true def modifies_applications?({:remove_application, _application}), do: true def modifies_applications?({:restart_application, _application}), do: true def modifies_applications?(:restart_new_emulator), do: true def modifies_applications?(:restart_emulator), do: true def modifies_applications?(_), do: false @doc """ Returns true if the given instruction is an instruction which modifies code by loading, unloading or purging it. It returns `true` for the `load_module`, `add_module` `delete_module`, `load`, `remove` and `purge` relup instructions. """ @spec modifies_code?(Instructions.instruction) :: boolean def modifies_code?({:load_module, _module}), do: true def modifies_code?({:load_module, _module, _dep_mods}), do: true def modifies_code?({:load_module, _module, _pre_purge, _post_purge, _dep_mods}), do: true def modifies_code?({:add_module, _module}), do: true def modifies_code?({:add_module, _module, _dep_mods}), do: true def modifies_code?({:load, {_module, _pre_purge, _post_purge}}), do: true def modifies_code?({:purge, [_module]}), do: true def modifies_code?({:remove, {_module, _pre_purge, _post_purge}}), do: true def modifies_code?({:delete_module, _module}), do: true def modifies_code?({:delete_module, _module, _dep_mods}), do: true def modifies_code?(_), do: false @doc """ Returns true if the given instruction is an instruction which modifies any process by either by sending the `code_change` sys event or by starting or stopping any process. It returns `true` for the `code_change`, `start`, `stop` and `update` relup instructions. """ @spec modifies_processes?(Instructions.instruction) :: boolean def modifies_processes?({:update, _mod}), do: true def modifies_processes?({:update, _mod, :supervisor}), do: true def modifies_processes?({:update, _mod, _change_or_dep_mods}), do: true def modifies_processes?({:update, _mod, _change, _dep_mods}), do: true def modifies_processes?({:update, _mod, _change, _pre_purge, _post_purge, _dep_mods}), do: true def modifies_processes?({:update, _mod, Timeout, _change, _pre_purge, _post_purge, _dep_mods}), do: true def modifies_processes?({:update, _mod, ModType, Timeout, _change, _pre_purge, _post_purge, _dep_mods}), do: true def modifies_processes?({:code_change, [{_mod, _extra}]}), do: true def modifies_processes?({:code_change, _mode, [{_mod, _extra}]}), do: true def modifies_processes?({:start, [_mod]}), do: true def modifies_processes?({:stop, [_mod]}), do: true def modifies_processes?(_), do: false end