Skip to content

Commit bd2efde

Browse files
authored
Only commit migration transaction if migration can be inserted into the DB (#30)
1 parent 2f7f4bc commit bd2efde

File tree

3 files changed

+111
-49
lines changed

3 files changed

+111
-49
lines changed

.travis.yml

+6-3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ otp_release:
55
- 20.2
66
addons:
77
apt:
8+
sources:
9+
- mysql-5.7-trusty
810
packages:
9-
- mysql-server-5.6
10-
- mysql-client-core-5.6
11-
- mysql-client-5.6
11+
- mysql-server
12+
- mysql-client
1213
before_install:
1314
- sudo service postgresql stop
1415
- sudo apt-get -y -qq --purge remove postgresql libpq-dev libpq5 postgresql-client-common postgresql-common
@@ -44,6 +45,8 @@ before_script:
4445
- echo "host all postgrex_md5_pw 127.0.0.1/32 md5" | sudo tee -a /etc/postgresql/$PGVERSION/main/pg_hba.conf
4546
- echo "host all postgrex_cleartext_pw 127.0.0.1/32 password" | sudo tee -a /etc/postgresql/$PGVERSION/main/pg_hba.conf
4647
- sudo service postgresql restart
48+
- sudo mysql_upgrade
49+
- sudo service mysql --full-restart
4750
notifications:
4851
recipients:
4952
- jose.valim@plataformatec.com.br

integration_test/sql/migrator.exs

+59-15
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ defmodule Ecto.Integration.MigratorTest do
44
use Ecto.Integration.Case
55

66
import Support.FileHelpers
7-
import Ecto.Migrator, only: [migrated_versions: 1]
7+
import ExUnit.CaptureLog
8+
import Ecto.Migrator
89

910
alias Ecto.Integration.PoolRepo
1011
alias Ecto.Migration.SchemaMigration
@@ -14,15 +15,38 @@ defmodule Ecto.Integration.MigratorTest do
1415
:ok
1516
end
1617

18+
defmodule AnotherSchemaMigration do
19+
use Ecto.Migration
20+
21+
def change do
22+
execute PoolRepo.create_prefix("bad_schema_migrations"),
23+
PoolRepo.drop_prefix("bad_schema_migrations")
24+
25+
create table(:schema_migrations, prefix: "bad_schema_migrations") do
26+
add :version, :string
27+
add :inserted_at, :integer
28+
end
29+
end
30+
end
31+
32+
defmodule BrokenLinkMigration do
33+
use Ecto.Migration
34+
35+
def change do
36+
Task.start_link(fn -> raise "oops" end)
37+
Process.sleep(:infinity)
38+
end
39+
end
40+
1741
defmodule GoodMigration do
1842
use Ecto.Migration
1943

2044
def up do
21-
:ok
45+
create table(:good_migration)
2246
end
2347

2448
def down do
25-
:ok
49+
drop table(:good_migration)
2650
end
2751
end
2852

@@ -34,20 +58,14 @@ defmodule Ecto.Integration.MigratorTest do
3458
end
3559
end
3660

37-
import Ecto.Migrator
38-
39-
test "schema migration" do
40-
up(PoolRepo, 30, GoodMigration, log: false)
41-
42-
[migration] = PoolRepo.all(SchemaMigration)
43-
assert migration.version == 30
44-
assert migration.inserted_at
45-
end
46-
4761
test "migrations up and down" do
4862
assert migrated_versions(PoolRepo) == []
4963
assert up(PoolRepo, 31, GoodMigration, log: false) == :ok
5064

65+
[migration] = PoolRepo.all(SchemaMigration)
66+
assert migration.version == 31
67+
assert migration.inserted_at
68+
5169
assert migrated_versions(PoolRepo) == [31]
5270
assert up(PoolRepo, 31, GoodMigration, log: false) == :already_up
5371
assert migrated_versions(PoolRepo) == [31]
@@ -57,10 +75,37 @@ defmodule Ecto.Integration.MigratorTest do
5775
assert migrated_versions(PoolRepo) == []
5876
end
5977

60-
test "bad migration" do
78+
test "does not commit migration if insert into schema migration fails" do
79+
# First we create a new schema migration table in another prefix
80+
assert up(PoolRepo, 33, AnotherSchemaMigration, log: false) == :ok
81+
assert migrated_versions(PoolRepo) == [33]
82+
83+
assert capture_log(fn ->
84+
catch_error(up(PoolRepo, 34, GoodMigration, log: false, prefix: "bad_schema_migrations"))
85+
catch_error(PoolRepo.all("good_migration"))
86+
catch_error(PoolRepo.all("good_migration", prefix: "bad_schema_migrations"))
87+
end) =~ "Could not update schema migrations"
88+
89+
assert down(PoolRepo, 33, AnotherSchemaMigration, log: false) == :ok
90+
end
91+
92+
test "bad execute migration" do
6193
assert catch_error(up(PoolRepo, 31, BadMigration, log: false))
6294
end
6395

96+
test "broken link migration" do
97+
Process.flag(:trap_exit, true)
98+
99+
assert capture_log(fn ->
100+
{:ok, pid} = Task.start_link(fn -> up(PoolRepo, 31, BrokenLinkMigration, log: false) end)
101+
assert_receive {:EXIT, ^pid, _}
102+
end) =~ "oops"
103+
104+
assert capture_log(fn ->
105+
catch_exit(up(PoolRepo, 31, BrokenLinkMigration, log: false))
106+
end) =~ "oops"
107+
end
108+
64109
test "run up to/step migration" do
65110
in_tmp fn path ->
66111
create_migration(47)
@@ -140,7 +185,6 @@ defmodule Ecto.Integration.MigratorTest do
140185
defmodule #{module} do
141186
use Ecto.Migration
142187
143-
144188
def up do
145189
update &[#{num}|&1]
146190
end

lib/ecto/migrator.ex

+46-31
Original file line numberDiff line numberDiff line change
@@ -108,21 +108,12 @@ defmodule Ecto.Migrator do
108108
end
109109

110110
defp do_up(repo, version, module, opts) do
111-
run_maybe_in_transaction(repo, module, fn ->
111+
async_migrate_maybe_in_transaction(repo, version, module, :up, opts, fn ->
112112
attempt(repo, version, module, :forward, :up, :up, opts)
113113
|| attempt(repo, version, module, :forward, :change, :up, opts)
114114
|| {:error, Ecto.MigrationError.exception(
115115
"#{inspect module} does not implement a `up/0` or `change/0` function")}
116116
end)
117-
|> case do
118-
:ok ->
119-
verbose_schema_migration repo, "update schema migrations", fn ->
120-
SchemaMigration.up(repo, version, opts[:prefix])
121-
end
122-
:ok
123-
error ->
124-
error
125-
end
126117
end
127118

128119
@doc """
@@ -153,41 +144,65 @@ defmodule Ecto.Migrator do
153144
end
154145

155146
defp do_down(repo, version, module, opts) do
156-
run_maybe_in_transaction(repo, module, fn ->
147+
async_migrate_maybe_in_transaction(repo, version, module, :down, opts, fn ->
157148
attempt(repo, version, module, :forward, :down, :down, opts)
158149
|| attempt(repo, version, module, :backward, :change, :down, opts)
159150
|| {:error, Ecto.MigrationError.exception(
160151
"#{inspect module} does not implement a `down/0` or `change/0` function")}
161152
end)
162-
|> case do
163-
:ok ->
153+
end
154+
155+
defp async_migrate_maybe_in_transaction(repo, version, module, direction, opts, fun) do
156+
parent = self()
157+
ref = make_ref()
158+
task = Task.async(fn -> run_maybe_in_transaction(parent, ref, repo, module, fun) end)
159+
160+
if migrated_successfully?(ref, task.pid) do
161+
try do
162+
# The table with schema migrations can only be updated from
163+
# the parent process because it has a lock on the table
164164
verbose_schema_migration repo, "update schema migrations", fn ->
165-
SchemaMigration.down(repo, version, opts[:prefix])
165+
apply(SchemaMigration, direction, [repo, version, opts[:prefix]])
166166
end
167-
:ok
168-
error ->
169-
error
167+
catch
168+
kind, error ->
169+
Task.shutdown(task, :brutal_kill)
170+
:erlang.raise(kind, error, System.stacktrace())
171+
end
170172
end
173+
174+
send(task.pid, ref)
175+
Task.await(task, :infinity)
171176
end
172177

173-
defp run_maybe_in_transaction(repo, module, fun) do
174-
fn -> do_run_maybe_in_transaction(repo, module, fun) end
175-
|> Task.async()
176-
|> Task.await(:infinity)
178+
defp migrated_successfully?(ref, pid) do
179+
receive do
180+
{^ref, :ok} -> true
181+
{^ref, _} -> false
182+
{:EXIT, ^pid, _} -> false
183+
end
177184
end
178185

179-
defp do_run_maybe_in_transaction(repo, module, fun) do
180-
cond do
181-
module.__migration__[:disable_ddl_transaction] ->
182-
fun.()
183-
repo.__adapter__.supports_ddl_transaction? ->
184-
{:ok, result} = repo.transaction(fun, log: false, timeout: :infinity)
185-
result
186-
true ->
187-
fun.()
186+
defp run_maybe_in_transaction(parent, ref, repo, module, fun) do
187+
if module.__migration__[:disable_ddl_transaction] ||
188+
not repo.__adapter__.supports_ddl_transaction? do
189+
send_and_receive(parent, ref, fun.())
190+
else
191+
{:ok, result} =
192+
repo.transaction(
193+
fn -> send_and_receive(parent, ref, fun.()) end,
194+
log: false, timeout: :infinity
195+
)
196+
197+
result
188198
end
189199
catch kind, reason ->
190-
{kind, reason, System.stacktrace}
200+
send_and_receive(parent, ref, {kind, reason, System.stacktrace})
201+
end
202+
203+
defp send_and_receive(parent, ref, value) do
204+
send parent, {ref, value}
205+
receive do: (^ref -> value)
191206
end
192207

193208
defp attempt(repo, version, module, direction, operation, reference, opts) do

0 commit comments

Comments
 (0)