Skip to content

Commit 504d9df

Browse files
authored
feat(dashboard): backfill schema_migrations (#1913)
1 parent e23f1e0 commit 504d9df

2 files changed

Lines changed: 164 additions & 13 deletions

File tree

lib/realtime_web/dashboard/tenant_migrations.ex

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ defmodule RealtimeWeb.Dashboard.TenantMigrations do
1212
alias Realtime.Api
1313
alias Realtime.Api.Tenant
1414
alias Realtime.Database
15+
alias Realtime.Tenants.Migrations
1516

1617
@pg_delta_filter ~s"""
1718
{
@@ -84,17 +85,9 @@ defmodule RealtimeWeb.Dashboard.TenantMigrations do
8485
end
8586

8687
@impl true
87-
def handle_event(
88-
"apply_plan",
89-
_params,
90-
%{
91-
assigns: %{
92-
tenant: %Tenant{} = tenant,
93-
external_id: ref,
94-
pg_delta: {:ok, %{status: :changes, sql: sql}}
95-
}
96-
} = socket
97-
) do
88+
def handle_event("apply_plan", _params, socket) do
89+
%{tenant: %Tenant{} = tenant, external_id: ref, pg_delta: {:ok, %{status: :changes, sql: sql}}} = socket.assigns
90+
9891
case apply_pg_delta(tenant, sql) do
9992
:ok ->
10093
{:noreply, push_patch(socket, to: "/admin/dashboard/tenant_migrations?external_id=#{URI.encode(ref)}")}
@@ -104,6 +97,19 @@ defmodule RealtimeWeb.Dashboard.TenantMigrations do
10497
end
10598
end
10699

100+
@impl true
101+
def handle_event("backfill_migrations", _params, socket) do
102+
%{tenant: %Tenant{} = tenant, external_id: ref, pg_delta: {:ok, %{status: :no_changes}}} = socket.assigns
103+
104+
case backfill_schema_migrations(tenant) do
105+
:ok ->
106+
{:noreply, push_patch(socket, to: "/admin/dashboard/tenant_migrations?external_id=#{URI.encode(ref)}")}
107+
108+
{:error, msg} ->
109+
{:noreply, assign(socket, error: msg)}
110+
end
111+
end
112+
107113
@impl true
108114
def render(assigns) do
109115
~H"""
@@ -136,6 +142,8 @@ defmodule RealtimeWeb.Dashboard.TenantMigrations do
136142
137143
<h6 class="mt-4">pg-delta plan vs baseline</h6>
138144
<%= pg_delta_plan(@pg_delta) %>
145+
146+
<%= backfill_section(@pg_delta, @schema_migrations) %>
139147
<% end %>
140148
</div>
141149
"""
@@ -256,6 +264,76 @@ defmodule RealtimeWeb.Dashboard.TenantMigrations do
256264
"""
257265
end
258266

267+
defp backfill_section(pg_delta, schema_migrations) do
268+
total = length(Migrations.migrations())
269+
270+
{show, applied} =
271+
case {pg_delta, schema_migrations} do
272+
{{:ok, %{status: :no_changes}}, {:ok, rows}} ->
273+
applied = length(rows)
274+
{applied < total, applied}
275+
276+
_ ->
277+
{false, nil}
278+
end
279+
280+
assigns = %{show: show, applied: applied, total: total}
281+
282+
~H"""
283+
<div :if={@show}>
284+
<h6 class="mt-4">Backfill schema_migrations</h6>
285+
<div class="alert alert-warning">
286+
realtime.schema_migrations has <%= @applied %> of <%= @total %> versions applied and pg-delta reports no drift.
287+
Backfill inserts the missing rows and sets tenants.migrations_ran to <%= @total %>.
288+
</div>
289+
<div class="d-flex justify-content-end mt-3">
290+
<button
291+
type="button"
292+
class="btn btn-primary"
293+
phx-click="backfill_migrations"
294+
phx-disable-with="Backfilling..."
295+
data-confirm={"Insert missing version(s) into realtime.schema_migrations and set tenants.migrations_ran to #{@total}?"}
296+
>
297+
Backfill schema_migrations
298+
</button>
299+
</div>
300+
</div>
301+
"""
302+
end
303+
304+
@doc false
305+
def backfill_schema_migrations(%Tenant{external_id: external_id} = tenant) do
306+
versions = Enum.map(Migrations.migrations(), fn {v, _mod} -> v end)
307+
total = length(versions)
308+
309+
with {:ok, settings} <- Database.from_tenant(tenant, @application_name, :stop),
310+
{:ok, db_conn} <- Database.connect_db(settings),
311+
:ok <- insert_versions(db_conn, versions),
312+
{:ok, _} <- Api.update_migrations_ran(external_id, total) do
313+
:ok
314+
else
315+
{:error, %{postgres: %{message: message}}} ->
316+
log_warning("TenantMigrationsBackfillFailed", message)
317+
{:error, "Backfill failed: #{message}"}
318+
319+
{:error, reason} ->
320+
log_warning("TenantMigrationsBackfillFailed", reason)
321+
{:error, "Backfill failed: #{inspect(reason)}"}
322+
end
323+
end
324+
325+
defp insert_versions(db_conn, versions) do
326+
backfill_insert =
327+
"INSERT INTO realtime.schema_migrations (version, inserted_at) VALUES ($1, NOW()) ON CONFLICT (version) DO NOTHING"
328+
329+
Enum.reduce_while(versions, :ok, fn version, _acc ->
330+
case Postgrex.query(db_conn, backfill_insert, [version], timeout: @query_timeout) do
331+
{:ok, _} -> {:cont, :ok}
332+
{:error, _} = err -> {:halt, err}
333+
end
334+
end)
335+
end
336+
259337
defp assign_error(socket, ref, msg) do
260338
assign(socket,
261339
external_id: ref,
@@ -346,12 +424,17 @@ defmodule RealtimeWeb.Dashboard.TenantMigrations do
346424
end
347425
end
348426

349-
defp apply_pg_delta(%Tenant{} = tenant, sql) do
427+
@doc false
428+
def apply_pg_delta(%Tenant{external_id: external_id} = tenant, sql) do
350429
opts = [query_type: :text, timeout: @query_timeout]
430+
versions = Enum.map(Migrations.migrations(), fn {v, _mod} -> v end)
431+
total = length(versions)
351432

352433
with {:ok, settings} <- Database.from_tenant(tenant, @application_name, :stop),
353434
{:ok, db_conn} <- Database.connect_db(settings),
354-
{:ok, _} <- Postgrex.query(db_conn, sql, [], opts) do
435+
{:ok, _} <- Postgrex.query(db_conn, sql, [], opts),
436+
:ok <- insert_versions(db_conn, versions),
437+
{:ok, _} <- Api.update_migrations_ran(external_id, total) do
355438
:ok
356439
else
357440
{:error, %{postgres: %{message: message}}} ->

test/realtime_web/dashboard/tenant_migrations_test.exs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ defmodule RealtimeWeb.Dashboard.TenantMigrationsTest do
22
use RealtimeWeb.ConnCase, async: false
33
import Phoenix.LiveViewTest
44

5+
alias Realtime.Api
56
alias Realtime.Database
7+
alias Realtime.Tenants.Migrations
68
alias RealtimeWeb.Dashboard.TenantMigrations
79

810
setup do
@@ -69,6 +71,72 @@ defmodule RealtimeWeb.Dashboard.TenantMigrationsTest do
6971
assert has_element?(view, "h6", "pg-delta plan vs baseline")
7072
end
7173

74+
describe "backfill_schema_migrations/1" do
75+
test "inserts missing versions and updates tenants.migrations_ran", %{tenant: tenant} do
76+
{:ok, db_conn} = Database.connect(tenant, "realtime_test", :stop)
77+
78+
Postgrex.query!(
79+
db_conn,
80+
"DELETE FROM realtime.schema_migrations WHERE version > 20211116213934",
81+
[]
82+
)
83+
84+
{:ok, _} = Api.update_migrations_ran(tenant.external_id, 7)
85+
86+
assert :ok = TenantMigrations.backfill_schema_migrations(tenant)
87+
88+
%{rows: [[count]]} =
89+
Postgrex.query!(db_conn, "SELECT count(*)::int FROM realtime.schema_migrations", [])
90+
91+
total = length(Migrations.migrations())
92+
assert count == total
93+
94+
updated = Api.get_tenant_by_external_id(tenant.external_id, use_replica?: false)
95+
assert updated.migrations_ran == total
96+
end
97+
98+
test "running twice keeps the row count and migrations_ran stable", %{tenant: tenant} do
99+
{:ok, db_conn} = Database.connect(tenant, "realtime_test", :stop)
100+
total = length(Migrations.migrations())
101+
102+
assert :ok = TenantMigrations.backfill_schema_migrations(tenant)
103+
assert :ok = TenantMigrations.backfill_schema_migrations(tenant)
104+
105+
%{rows: [[count]]} =
106+
Postgrex.query!(db_conn, "SELECT count(*)::int FROM realtime.schema_migrations", [])
107+
108+
assert count == total
109+
110+
updated = Api.get_tenant_by_external_id(tenant.external_id, use_replica?: false)
111+
assert updated.migrations_ran == total
112+
end
113+
end
114+
115+
describe "apply_pg_delta/2" do
116+
test "runs the sql plan and backfills schema_migrations", %{tenant: tenant} do
117+
{:ok, db_conn} = Database.connect(tenant, "realtime_test", :stop)
118+
119+
Postgrex.query!(
120+
db_conn,
121+
"DELETE FROM realtime.schema_migrations WHERE version > 20211116213934",
122+
[]
123+
)
124+
125+
{:ok, _} = Api.update_migrations_ran(tenant.external_id, 7)
126+
127+
assert :ok = TenantMigrations.apply_pg_delta(tenant, "SELECT 1")
128+
129+
%{rows: [[count]]} =
130+
Postgrex.query!(db_conn, "SELECT count(*)::int FROM realtime.schema_migrations", [])
131+
132+
total = length(Migrations.migrations())
133+
assert count == total
134+
135+
updated = Api.get_tenant_by_external_id(tenant.external_id, use_replica?: false)
136+
assert updated.migrations_ran == total
137+
end
138+
end
139+
72140
describe "postgres_url/1" do
73141
test "builds a valid URL for IPv4 hosts" do
74142
assert TenantMigrations.postgres_url(%Database{

0 commit comments

Comments
 (0)