triplex icon indicating copy to clipboard operation
triplex copied to clipboard

Running tenant migrations without Mix for apps deployed via mix release

Open haizop opened this issue 4 years ago • 4 comments

Scenario

We deploy using releases, thus Mix is not available and we cannot migrate in production (or other server environments) using the Triplex Mix tasks. We run our public schema migrations via a similar Release module to the one recommended in the Phoenix documentation. To that basic structure, we added an additional migrate_tenants function:

defmodule MyApp.Release do
  @app :my_app

  def migrate do
    load_app()

    for repo <- repos() do
      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
    end
  end

  def migrate_tenants do
    load_app()

    for repo <- repos() do
      for tenant <- Triplex.all(repo) do
        Triplex.migrate(tenant, repo)
      end
    end
  end

  defp repos do
    Application.fetch_env!(@app, :ecto_repos)
  end

  defp load_app do
    Application.load(@app)
  end
end

Problem

The migrate_tenants function succeeds when run via a remote IEx session or rpc, but fails when run via eval, ie bin/my_app eval "MyApp.Release.migrate_tenants()". This is in contrast to the migrate function which succeeds when run via either remote IEx, rpc, or eval. The error observed is the following:

** (RuntimeError) could not lookup Ecto repo MyApp.Repo because it was not started or it does not exist
    lib/ecto/repo/registry.ex:19: Ecto.Repo.Registry.lookup/1
    lib/ecto/adapter.ex:127: Ecto.Adapter.lookup_meta/1
    lib/ecto/adapters/sql.ex:404: Ecto.Adapters.SQL.query/4
    lib/ecto/adapters/sql.ex:362: Ecto.Adapters.SQL.query!/4
    lib/triplex.ex:289: Triplex.all/1
    (myapp 0.1.0) lib/myapp/release.ex:21: anonymous fn/2 in MyApp.Release.migrate_tenants/0
    (elixir 1.11.4) lib/enum.ex:2193: Enum."-reduce/3-lists^foldl/2-0-"/3
    (myapp 0.1.0) lib/myapp/release.ex:20: MyApp.Release.migrate_tenants/0

Questions

  • Is it possible to migrate tenants using Triplex via eval on a release?
  • Is there a known root cause for this issue?
  • Is there a recommended approach for using Triplex with Mix releases in general?

Notes:

  • This issue seems somewhat similar to https://github.com/ateliware/triplex/issues/66, but I have different questions so I opened a new issue.
  • Posted same scenario on Elixir forum: https://elixirforum.com/t/how-to-run-triplex-tenant-migrations-when-deploying-via-mix-release/42135

haizop avatar Aug 20 '21 19:08 haizop

Thanks for the report @haizop! Here are your answers:

Is it possible to migrate tenants using Triplex via eval on a release?

Should be possible, as it is basically doing pretty much the same that is done on your migrate function, with some extra options to run the migrations on the tenant prefixes.

Is there a known root cause for this issue?

By looking at the error, looks like the Repo is not started by the time Triplex.all is called, so it might be something to do with that. My guess is that your migrate function works because all the code that actually do something to DB is inside the Ecto.Migrator.with_repo, and by looking at that function you can see that they ensure the repo is started there. My suggestion is that you do the same manually like it's done there: https://github.com/elixir-ecto/ecto_sql/blob/master/lib/ecto/migrator.ex#L125

Is there a recommended approach for using Triplex with Mix releases in general?

There is nothing really specific to Triplex, the thing is that you kinda need to know some of the inner workings of ecto to actually make it work correctly, and that's an Ecto thing, not just specific to Triplex.

kelvinst avatar Oct 04 '21 13:10 kelvinst

@kelvinst . Thank you for the reply.

I hear you that this is not a problem specific to Triplex, but given that Mix releases are a pretty standard method of deployment, do you think that some deployment guidelines should be added to Triplex documentation?

This is what I have working now:

defmodule MyApp.Release do
  @moduledoc """
  Used for executing DB release tasks when run in production without Mix
  installed.
  """

  alias Ecto.Migrator
  alias MyApp.Repo
  alias MyApp.Tenants

  @app :my_app

  def migrate_public_schema do
    load_app()

    {:ok, _, _} = Migrator.with_repo(Repo, &Migrator.run(&1, :up, all: true))
  end

  def migrate_tenant_schemas do
    load_app()

    Migrator.with_repo(Repo, fn repo ->
      Tenants.list_tenants()
      |> Enum.each(
        &Migrator.run(repo, tenant_migrations_path(), :up, all: true, prefix: &1.schema_name)
      )
    end)
  end

  def rollback_public_schema(version) do
    load_app()

    {:ok, _, _} = Migrator.with_repo(Repo, &Migrator.run(&1, :down, to: version))
  end

  def rollback_tenant_schemas(version) do
    load_app()

    Migrator.with_repo(Repo, fn repo ->
      Tenants.list_tenants()
      |> Enum.each(
        &Migrator.run(repo, tenant_migrations_path(), :down, to: version, prefix: &1.schema_name)
      )
    end)
  end

  defp tenant_migrations_path() do
    Triplex.migrations_path(Repo)
  end

  defp load_app do
    Application.load(@app)
  end
end

haizop avatar Oct 04 '21 14:10 haizop

@haizop definitely! A PR with a guide for that is totally welcome. Will leave this issue open to remember to do exactly that.

kelvinst avatar Oct 04 '21 14:10 kelvinst

@kelvinst Will get you a PR in the next couple of weeks.

haizop avatar Oct 04 '21 14:10 haizop