Skip to content

Commit f157051

Browse files
authored
feat: load tenant database dump (#1998)
* feat: load tenant database dump Shortcut to load the entire Tenant DB schema at once instead of running migrations sequentially that adds overhead to tenant creatiion workflow. Applies only to new tenants. Ref REAL-861
1 parent e454c91 commit f157051

10 files changed

Lines changed: 3334 additions & 36 deletions

File tree

.github/workflows/update-tenant-db-catalog.yml

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
- "lib/realtime/tenants/repo/migrations/**"
77
- "lib/realtime/tenants/migrations.ex"
88
- "lib/mix/tasks/realtime.export_tenant_db_catalog.ex"
9+
- "lib/mix/tasks/realtime.export_tenant_db_dump.ex"
910
- "mix.lock"
1011
- "mise.toml"
1112
- "compose.dbs.yml"
@@ -20,8 +21,8 @@ concurrency:
2021
cancel-in-progress: true
2122

2223
jobs:
23-
update-tenant-db-catalog:
24-
name: Export catalog (pg${{ matrix.pg_major }})
24+
update-tenant-dump:
25+
name: Export catalog & dump (pg${{ matrix.pg_major }})
2526
runs-on: blacksmith-2vcpu-ubuntu-2404
2627
if: github.event.pull_request.head.repo.full_name == github.repository
2728
strategy:
@@ -54,6 +55,18 @@ jobs:
5455
- name: Install dependencies
5556
run: mix deps.get
5657

58+
- name: Install PostgreSQL client
59+
run: |
60+
sudo install -d /usr/share/postgresql-common/pgdg
61+
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail \
62+
https://www.postgresql.org/media/keys/ACCC4CF8.asc
63+
. /etc/os-release
64+
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt ${VERSION_CODENAME}-pgdg main" \
65+
| sudo tee /etc/apt/sources.list.d/pgdg.list
66+
sudo apt-get update -y
67+
sudo apt-get install -y "postgresql-client-${{ matrix.pg_major }}"
68+
echo "/usr/lib/postgresql/${{ matrix.pg_major }}/bin" >> "$GITHUB_PATH"
69+
5770
- name: Cache Docker images
5871
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
5972
id: docker-cache
@@ -75,22 +88,31 @@ jobs:
7588
- name: Start Postgres
7689
run: docker compose -f compose.dbs.yml up -d --wait
7790

91+
# Force re-generating fresh dump and catalogs
92+
- name: Remove committed db dump
93+
run: rm -f priv/repo/tenant_db_dump_${{ matrix.pg_major }}.sql
94+
7895
- name: Set up realtime DB and migrate tenant DB
7996
run: mix ecto.setup
8097

81-
- name: Export catalog snapshot
98+
- name: Export Tenant DB catalog
8299
run: mix realtime.export_tenant_db_catalog
83100

84-
- name: Upload catalog artifact
101+
- name: Export Tenant DB dump
102+
run: mix realtime.export_tenant_db_dump --pg-major ${{ matrix.pg_major }}
103+
104+
- name: Upload artifact
85105
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
86106
with:
87-
name: tenant-catalog-${{ matrix.pg_major }}
88-
path: priv/repo/tenant_db_catalog_${{ matrix.pg_major }}.json
89-
if-no-files-found: error
107+
name: tenant-dump-${{ matrix.pg_major }}
108+
path: |
109+
priv/repo/tenant_db_dump_${{ matrix.pg_major }}.sql
110+
priv/repo/tenant_db_catalog_${{ matrix.pg_major }}.json
111+
if-no-files-found: ignore
90112

91113
commit-changes:
92-
name: Commit catalog snapshots
93-
needs: update-tenant-db-catalog
114+
name: Commit catalog and dump snapshots
115+
needs: update-tenant-dump
94116
runs-on: blacksmith-2vcpu-ubuntu-2404
95117
if: github.event.pull_request.head.repo.full_name == github.repository
96118

@@ -108,31 +130,32 @@ jobs:
108130
repository: ${{ github.event.pull_request.head.repo.full_name }}
109131
token: ${{ steps.app-token.outputs.token }}
110132

111-
- name: Download catalog artifacts
133+
- name: Download artifacts
112134
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
113135
with:
114-
pattern: tenant-catalog-*
136+
pattern: tenant-dump-*
115137
path: artifacts
116138

117139
- name: Move artifacts into place
118-
run: find artifacts -type f -name '*.json' -exec cp {} priv/repo/ \;
140+
run: find artifacts -type f \( -name '*.sql' -o -name '*.json' \) -exec cp {} priv/repo/ \;
119141

120-
- name: Check if catalog changed
142+
- name: Check if catalog or dumps changed
121143
id: check-changes
122144
run: |
123-
git add priv/repo/tenant_db_catalog_15.json priv/repo/tenant_db_catalog_17.json
145+
git add priv/repo/tenant_db_catalog_15.json priv/repo/tenant_db_catalog_17.json \
146+
priv/repo/tenant_db_dump_15.sql priv/repo/tenant_db_dump_17.sql
124147
if git diff --cached --quiet; then
125148
echo "changed=false" >> "$GITHUB_OUTPUT"
126149
else
127150
echo "changed=true" >> "$GITHUB_OUTPUT"
128151
fi
129152
130-
- name: Commit and push catalog
153+
- name: Commit and push catalog and dumps
131154
if: steps.check-changes.outputs.changed == 'true'
132155
env:
133156
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
134157
run: |
135158
git config user.name "github-actions[bot]"
136159
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
137-
git commit -m "chore: update tenant db catalog snapshots"
160+
git commit -m "chore: update tenant db catalog and dumps"
138161
git push origin "HEAD:$PR_HEAD_REF"
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
defmodule Mix.Tasks.Realtime.ExportTenantDbDump do
2+
@shortdoc "Regenerate priv/repo/tenant_db_dump_<pg_major>.sql"
3+
4+
@moduledoc """
5+
Dumps the tenant database's `realtime` schema to `priv/repo/tenant_db_dump_<pg_major>.sql`
6+
and the `realtime.schema_migrations` rows.
7+
8+
Usage:
9+
10+
mix realtime.export_tenant_db_dump --pg-major 17
11+
12+
The target tenant DB is expected to already have all tenant migrations applied,
13+
so make sure it is in a good state before generating it:
14+
15+
mise task run db-rm
16+
mise task run db-start
17+
mix setup
18+
19+
The target DB is read from `DB_HOST` / `DB_PORT` / `DB_NAME` / `DB_USER` / `DB_PASSWORD` env vars.
20+
21+
Requires `pg_dump` matching the target's major version on `$PATH`.
22+
"""
23+
use Mix.Task
24+
25+
@impl Mix.Task
26+
def run(args) do
27+
{:ok, _} = Application.ensure_all_started(:postgrex)
28+
29+
{opts, _, _} = OptionParser.parse(args, strict: [pg_major: :integer])
30+
pg_major = opts[:pg_major] || Mix.raise("--pg-major is required, e.g. --pg-major 17")
31+
32+
host = System.get_env("DB_HOST", "127.0.0.1")
33+
port = Realtime.Env.get_integer("DB_PORT", 5433)
34+
database = System.get_env("DB_NAME", "postgres")
35+
user = System.get_env("DB_USER", "supabase_admin")
36+
password = System.get_env("DB_PASSWORD", "postgres")
37+
path = dump_path(pg_major)
38+
39+
Mix.shell().info("[export_tenant_db_dump] target: #{host}:#{port}/#{database} (pg#{pg_major})")
40+
41+
pg_dump!(host, port, database, user, password, path)
42+
append_schema_migrations!(host, port, database, user, password, path)
43+
postprocess!(path)
44+
45+
Mix.shell().info("[export_tenant_db_dump] wrote #{path}")
46+
end
47+
48+
defp dump_path(pg_major), do: Application.app_dir(:realtime, "priv/repo/tenant_db_dump_#{pg_major}.sql")
49+
50+
defp pg_dump!(host, port, database, user, password, path) do
51+
pg_dump = System.find_executable("pg_dump") || Mix.raise("pg_dump not found on $PATH")
52+
53+
args = [
54+
"--host",
55+
host,
56+
"--port",
57+
to_string(port),
58+
"--username",
59+
user,
60+
"--dbname",
61+
database,
62+
"--schema-only",
63+
"--schema",
64+
"realtime",
65+
"--file",
66+
path
67+
]
68+
69+
case System.cmd(pg_dump, args, env: [{"PGPASSWORD", password}], stderr_to_stdout: true) do
70+
{_output, 0} -> :ok
71+
{output, code} -> Mix.raise("pg_dump exited #{code}:\n#{output}")
72+
end
73+
end
74+
75+
defp append_schema_migrations!(host, port, database, user, password, path) do
76+
{:ok, conn} =
77+
Postgrex.start_link(hostname: host, port: port, database: database, username: user, password: password)
78+
79+
{:ok, %{rows: rows}} =
80+
Postgrex.query(conn, ~s(SELECT version FROM realtime."schema_migrations" ORDER BY version), [])
81+
82+
GenServer.stop(conn)
83+
84+
inserts =
85+
Enum.map_join(rows, fn [version] ->
86+
"INSERT INTO realtime.\"schema_migrations\" (version) VALUES (#{version});\n"
87+
end)
88+
89+
sql = "ALTER TABLE realtime.schema_migrations ALTER COLUMN inserted_at SET DEFAULT now();\n" <> inserts
90+
91+
File.write!(path, sql, [:append])
92+
end
93+
94+
defp postprocess!(path) do
95+
tmp_path = path <> ".tmp"
96+
97+
path
98+
|> File.stream!()
99+
|> Stream.reject(&String.starts_with?(&1, ["\\restrict ", "\\unrestrict "]))
100+
|> Stream.map(fn
101+
"CREATE SCHEMA realtime;\n" -> "CREATE SCHEMA IF NOT EXISTS realtime;\n"
102+
line -> line
103+
end)
104+
|> Stream.into(File.stream!(tmp_path))
105+
|> Stream.run()
106+
107+
File.rename!(tmp_path, path)
108+
end
109+
end

lib/realtime/api.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,6 @@ defmodule Realtime.Api do
258258
end
259259
end
260260

261-
@spec preload_counters(nil | Realtime.Api.Tenant.t(), any()) :: nil | Realtime.Api.Tenant.t()
262261
@doc """
263262
Updates the migrations_ran field for a tenant.
264263
"""
@@ -285,6 +284,7 @@ defmodule Realtime.Api do
285284
end
286285
end
287286

287+
@spec preload_counters(nil | Realtime.Api.Tenant.t(), any()) :: nil | Realtime.Api.Tenant.t()
288288
def preload_counters(nil), do: nil
289289

290290
def preload_counters(%Tenant{} = tenant) do

lib/realtime/telemetry/logger.ex

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,23 +42,31 @@ defmodule Realtime.Telemetry.Logger do
4242
def handle_event([:realtime, :tenants, :migrations, :start], _measurements, metadata, _config) do
4343
Logger.info(
4444
"Applying migrations to #{metadata.hostname}",
45+
external_id: metadata.external_id,
4546
project: metadata.external_id
4647
)
4748
end
4849

4950
def handle_event([:realtime, :tenants, :migrations, :stop], measurements, metadata, _config) do
5051
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
5152

52-
Logger.info(
53-
"Finished applying #{metadata.migrations_executed} migrations for tenant #{metadata.external_id} in #{duration_ms}ms",
54-
project: metadata.external_id
55-
)
53+
message =
54+
case metadata.source do
55+
:dump ->
56+
"Finished loading the dump of #{metadata.migrations_executed} migrations for tenant #{metadata.external_id} in #{duration_ms}ms"
57+
58+
:migrator ->
59+
"Finished applying #{metadata.migrations_executed} migrations for tenant #{metadata.external_id} in #{duration_ms}ms"
60+
end
61+
62+
Logger.info(message, external_id: metadata.external_id, project: metadata.external_id)
5663
end
5764

5865
def handle_event([:realtime, :tenants, :migrations, :exception], _measurements, metadata, _config) do
5966
log_error(
6067
"MigrationsFailedToRun",
6168
metadata.reason,
69+
external_id: metadata.external_id,
6270
project: metadata.external_id,
6371
error_code: metadata.error_code
6472
)
@@ -68,6 +76,7 @@ defmodule Realtime.Telemetry.Logger do
6876
log_warning(
6977
"MigrationCountMismatch",
7078
"Reconciling migrations_ran for tenant #{metadata.external_id} cached=#{metadata.cached_migrations_ran} database=#{metadata.database_migrations_ran}",
79+
external_id: metadata.external_id,
7180
project: metadata.external_id
7281
)
7382
end
@@ -76,6 +85,7 @@ defmodule Realtime.Telemetry.Logger do
7685
log_error(
7786
"MigrationCountMismatchReconcileFailed",
7887
metadata.reason,
88+
external_id: metadata.external_id,
7989
project: metadata.external_id
8090
)
8191
end

0 commit comments

Comments
 (0)