Skip to content

Commit 386aeb3

Browse files
authored
chore: Reuse containers during tests (#1376)
* chore: add support to reuse containers during tests ♥ ❯ mix test test/realtime/database_test.exs:53 [test_helper.exs] Time to start tests: 10791 ms ♥ ❯ REUSE_CONTAINERS=true mix test test/realtime/database_test.exs:53 [test_helper.exs] Time to start tests: 371 ms Now instead of waiting 10 seconds to run the first test we can wait less than half a second.
1 parent 042b150 commit 386aeb3

File tree

5 files changed

+131
-68
lines changed

5 files changed

+131
-68
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
- main
66
paths:
77
- "lib/**"
8+
- "test/**"
89
- "config/**"
910
- "priv/**"
1011
- "assets/**"

test/support/containers.ex

Lines changed: 110 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,72 @@ defmodule Containers do
55
alias Realtime.RateCounter
66
alias Realtime.Tenants.Migrations
77

8-
def initialize(tenant) do
9-
name = "realtime-test-#{tenant.external_id}"
10-
%{port: port} = Database.from_tenant(tenant, "realtime_test", :stop)
8+
use GenServer
119

12-
{_, 0} =
13-
System.cmd("docker", [
14-
"run",
15-
"-d",
16-
"--name",
17-
name,
18-
"-e",
19-
"POSTGRES_HOST=/var/run/postgresql",
20-
"-e",
21-
"POSTGRES_PASSWORD=postgres",
22-
"-p",
23-
"#{port}:5432",
24-
"supabase/postgres:15.8.1.040",
25-
"postgres",
26-
"-c",
27-
"config_file=/etc/postgresql/postgresql.conf"
28-
])
10+
@image "supabase/postgres:15.8.1.040"
11+
12+
def start_container(), do: GenServer.call(__MODULE__, :start_container, 10_000)
13+
def port(), do: GenServer.call(__MODULE__, :port, 10_000)
14+
15+
def start_link(max_cases), do: GenServer.start_link(__MODULE__, max_cases, name: __MODULE__)
16+
17+
def init(max_cases) do
18+
existing_containers = existing_containers("realtime-test-*")
19+
ports = for {_, port} <- existing_containers, do: port
20+
available_ports = Enum.shuffle(5501..9000) -- ports
21+
22+
{:ok, %{existing_containers: existing_containers, ports: available_ports}, {:continue, {:pool, max_cases}}}
23+
end
24+
25+
def handle_continue({:pool, max_cases}, state) do
26+
{:ok, _pid} =
27+
:poolboy.start_link(
28+
[name: {:local, Containers.Pool}, size: max_cases + 1, max_overflow: 0, worker_module: Containers.Container],
29+
[]
30+
)
31+
32+
{:noreply, state}
33+
end
34+
35+
def handle_call(:port, _from, state) do
36+
[port | ports] = state.ports
37+
{:reply, port, %{state | ports: ports}}
38+
end
39+
40+
def handle_call(:start_container, _from, state) do
41+
case state.existing_containers do
42+
[{name, port} | rest] ->
43+
{:reply, {:ok, name, port}, %{state | existing_containers: rest}}
44+
45+
[] ->
46+
[port | ports] = state.ports
47+
name = "realtime-test-#{System.unique_integer([:positive])}"
48+
49+
docker_run!(name, port)
50+
51+
{:reply, {:ok, name, port}, %{state | ports: ports}}
52+
end
53+
end
54+
55+
def initialize(external_id) do
56+
name = "realtime-tenant-test-#{external_id}"
57+
58+
port =
59+
case existing_containers(name) do
60+
[{^name, port}] ->
61+
port
62+
63+
[] ->
64+
port = 5500
65+
docker_run!(name, port)
66+
port
67+
end
2968

3069
check_container_ready(name)
3170

71+
opts = %{external_id: external_id, name: external_id, port: port, jwt_secret: "secure_jwt_secret"}
72+
tenant = Generators.tenant_fixture(opts)
73+
3274
Migrations.run_migrations(tenant)
3375
{:ok, pid} = Database.connect(tenant, "realtime_test", :stop)
3476
:ok = Migrations.create_partitions(pid)
@@ -39,10 +81,10 @@ defmodule Containers do
3981

4082
@doc "Return port for a container that can be used"
4183
def checkout() do
42-
with container when is_pid(container) <- :poolboy.checkout(Containers, true, 5_000),
84+
with container when is_pid(container) <- :poolboy.checkout(Containers.Pool, true, 5_000),
4385
port <- Container.port(container) do
4486
# Automatically checkin the container at the end of the test
45-
ExUnit.Callbacks.on_exit(fn -> :poolboy.checkin(Containers, container) end)
87+
ExUnit.Callbacks.on_exit(fn -> :poolboy.checkin(Containers.Pool, container) end)
4688

4789
{:ok, port}
4890
else
@@ -52,13 +94,13 @@ defmodule Containers do
5294

5395
# Might be worth changing this to {:ok, tenant}
5496
def checkout_tenant(opts \\ []) do
55-
with container when is_pid(container) <- :poolboy.checkout(Containers, true, 5_000),
97+
with container when is_pid(container) <- :poolboy.checkout(Containers.Pool, true, 5_000),
5698
port <- Container.port(container) do
5799
tenant = Generators.tenant_fixture(%{port: port, migrations_ran: 0})
58100
run_migrations? = Keyword.get(opts, :run_migrations, false)
59101

60102
# Automatically checkin the container at the end of the test
61-
ExUnit.Callbacks.on_exit(fn -> :poolboy.checkin(Containers, container) end)
103+
ExUnit.Callbacks.on_exit(fn -> :poolboy.checkin(Containers.Pool, container) end)
62104

63105
settings = Database.from_tenant(tenant, "realtime_test", :stop)
64106
settings = %{settings | max_restarts: 0, ssl: false}
@@ -111,6 +153,30 @@ defmodule Containers do
111153
end
112154
end
113155

156+
def stop_container(external_id) do
157+
name = "realtime-tenant-test-#{external_id}"
158+
System.cmd("docker", ["rm", "-f", name])
159+
end
160+
161+
defp existing_containers(pattern) do
162+
{containers, 0} = System.cmd("docker", ["ps", "-a", "--format", "{{json .}}", "--filter", "name=#{pattern}"])
163+
164+
containers
165+
|> String.split("\n", trim: true)
166+
|> Enum.map(fn container ->
167+
container = Jason.decode!(container)
168+
# Ports" => "0.0.0.0:6445->5432/tcp, [::]:6445->5432/tcp"
169+
regex = ~r/(?<=:)\d+(?=->)/
170+
171+
[port] =
172+
Regex.scan(regex, container["Ports"])
173+
|> List.flatten()
174+
|> Enum.uniq()
175+
176+
{container["Names"], String.to_integer(port)}
177+
end)
178+
end
179+
114180
defp check_container_ready(name, attempts \\ 50)
115181
defp check_container_ready(name, 0), do: raise("Container #{name} is not ready")
116182

@@ -155,4 +221,24 @@ defmodule Containers do
155221
end
156222
end)
157223
end
224+
225+
defp docker_run!(name, port) do
226+
{_, 0} =
227+
System.cmd("docker", [
228+
"run",
229+
"-d",
230+
"--name",
231+
name,
232+
"-e",
233+
"POSTGRES_HOST=/var/run/postgresql",
234+
"-e",
235+
"POSTGRES_PASSWORD=postgres",
236+
"-p",
237+
"#{port}:5432",
238+
@image,
239+
"postgres",
240+
"-c",
241+
"config_file=/etc/postgresql/postgresql.conf"
242+
])
243+
end
158244
end

test/support/containers/container.ex

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
defmodule Containers.Container do
22
use GenServer
33

4-
@image "supabase/postgres:15.8.1.040"
5-
64
def start_link(args \\ [], opts \\ []) do
75
GenServer.start_link(__MODULE__, args, opts)
86
end
@@ -15,29 +13,15 @@ defmodule Containers.Container do
1513
end
1614

1715
@impl true
18-
def init(args) do
19-
port = Keyword.get(args, :port, Generators.port())
20-
name = "realtime-test-#{System.unique_integer([:positive])}"
21-
22-
{_, 0} =
23-
System.cmd("docker", [
24-
"run",
25-
"-d",
26-
"--name",
27-
name,
28-
"-e",
29-
"POSTGRES_HOST=/var/run/postgresql",
30-
"-e",
31-
"POSTGRES_PASSWORD=postgres",
32-
"-p",
33-
"#{port}:5432",
34-
@image,
35-
"postgres",
36-
"-c",
37-
"config_file=/etc/postgresql/postgresql.conf"
38-
])
39-
40-
{:ok, %{name: name, port: port}, {:continue, :check_container_ready}}
16+
def init(_args) do
17+
{:ok, %{}, {:continue, :start_container}}
18+
end
19+
20+
@impl true
21+
def handle_continue(:start_container, _state) do
22+
{:ok, name, port} = Containers.start_container()
23+
24+
{:noreply, %{name: name, port: port}, {:continue, :check_container_ready}}
4125
end
4226

4327
@impl true

test/support/generators.ex

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@ defmodule Generators do
77
alias Realtime.Crypto
88
alias Realtime.Database
99

10-
def port() do
11-
Agent.get_and_update(:available_db_ports, fn ports ->
12-
[port | ports] = ports
13-
{port, ports}
14-
end)
15-
end
10+
def port(), do: Containers.port()
1611

1712
@spec tenant_fixture(map()) :: Realtime.Api.Tenant.t()
1813
def tenant_fixture(override \\ %{}) do

test/test_helper.exs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,27 @@ alias Realtime.Database
55
ExUnit.start(exclude: [:failing], max_cases: 1, capture_log: true)
66

77
max_cases = ExUnit.configuration()[:max_cases]
8-
Containers.stop_containers()
98

10-
for tenant <- Api.list_tenants(), do: Api.delete_tenant(tenant)
9+
if System.get_env("REUSE_CONTAINERS") != "true" do
10+
Containers.stop_containers()
11+
Containers.stop_container("dev_tenant")
12+
end
13+
14+
{:ok, _pid} = Containers.start_link(max_cases)
1115

12-
{:ok, _pid} = Agent.start_link(fn -> Enum.shuffle(5500..9000) end, name: :available_db_ports)
16+
for tenant <- Api.list_tenants(), do: Api.delete_tenant(tenant)
1317

1418
tenant_name = "dev_tenant"
19+
tenant = Containers.initialize(tenant_name)
1520
publication = "supabase_realtime_test"
16-
port = Generators.port()
17-
opts = %{external_id: tenant_name, name: tenant_name, port: port, jwt_secret: "secure_jwt_secret"}
18-
tenant = Generators.tenant_fixture(opts)
1921

2022
# Start dev_realtime container to be used in integration tests
21-
Containers.initialize(tenant)
2223
{:ok, conn} = Database.connect(tenant, "realtime_seed", :stop)
2324

2425
Database.transaction(conn, fn db_conn ->
2526
queries = [
27+
"DROP TABLE IF EXISTS public.test",
28+
"DROP PUBLICATION IF EXISTS #{publication}",
2629
"create sequence if not exists test_id_seq;",
2730
"""
2831
create table "public"."test" (
@@ -39,12 +42,6 @@ Database.transaction(conn, fn db_conn ->
3942
Enum.each(queries, &Postgrex.query!(db_conn, &1, []))
4043
end)
4144

42-
{:ok, _pid} =
43-
:poolboy.start_link(
44-
[name: {:local, Containers}, size: max_cases + 1, max_overflow: 0, worker_module: Containers.Container],
45-
[]
46-
)
47-
4845
Ecto.Adapters.SQL.Sandbox.mode(Realtime.Repo, :manual)
4946

5047
end_time = :os.system_time(:millisecond)

0 commit comments

Comments
 (0)