How to: Tweak mix release to work with hot code reloading

I’ve spent some time understanding how to do hot code reloading with releases built using mix release, and here I’d like to detail the steps needed, in hopes that it will help someone.

So, in order to make hot-reloadable release you need:

  1. Write an appup file (the Erlang’s Appup cookbook is a great resource for that: Appup Cookbook — Erlang System Documentation v27.0.1).
  2. Tweak the release generated by mix release, by copying the .rel & .appup files into expected places and generating the relup file. This can be done by hand, but I’ve wrote a simple function that can do that right inside the mix release task:
def project do
  [
    releases: [
      app_name: [
        include_executables_for: [:unix],
        steps: [:assemble, &release_fixup_step/1, :tar],
      ],
    ],
    ...
  ]
end

defp release_fixup_step(release) do
  releases_dir = Path.join(release.path, "releases")
  rel_file = Path.join([releases_dir, release.version, "#{release.name}.rel"])
  :ok = :release_handler.create_RELEASES(releases_dir, rel_file, [])

  # Yes, we have three copies of a same file in the same place.
  # This is necessary to appease the release_handler.
  File.cp!(rel_file, Path.join([releases_dir, release.version, "#{release.name}-#{release.version}.rel"]))
  File.cp!(rel_file, Path.join([releases_dir, "#{release.name}-#{release.version}.rel"]))

  # Copy appup file into the correct location and generate the relup.
  appup_file = Path.join("appups", "#{release.version}.appup")
  if File.exists?(Path.join("appups", "#{release.version}.appup")) do
    File.cp!(
      appup_file,
      Path.join([release.path, "lib", "#{release.name}-#{release.version}", "ebin", "#{release.name}.appup"])
    )

    {:ok, appup} = :file.consult(appup_file)
    [{appup_release_version, [{version_up_from, _}], [{version_down_to, _}]}] = appup

    if appup_release_version != to_charlist(release.version) do
      raise "Unexpected version in appup: #{appup_release_version} (expected #{release.version})"
    end

    if version_up_from != version_down_to do
      raise "Unexpected versions in appup instructions: #{version_up_from} != #{version_down_to}"
    end

    :systools.make_relup(
      ~c"#{release.name}-#{release.version}",
      [~c"#{release.name}-#{version_up_from}"],
      [~c"#{release.name}-#{version_down_to}"],
      path: [to_charlist(Path.join(release.path, "releases/*")), to_charlist(Path.join(release.path, "lib/*/ebin"))],
      outdir: to_charlist(release.version_path)
    )
  end

  release
end

Now, after running mix release you will get a release archive in _build/<env>/app_name-<version>.tar.gz.

  1. Copy this archive into the releases/ directory of your running release.
  2. Connect to the Iex shell of the running release.
  3. Carefully run the commands:
:release_handler.unpack_release(~c"app_name-<version>")
:release_handler.install_release(~c"<version>")
:release_handler.make_permanent(~c"<version>")
10 Likes