Skip to content

Commit 562276c

Browse files
authored
fix: adds shutdown endpoint (#1693)
1 parent 14c26f4 commit 562276c

File tree

4 files changed

+96
-28
lines changed

4 files changed

+96
-28
lines changed

lib/realtime_web/controllers/fallback_controller.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ defmodule RealtimeWeb.FallbackController do
1111
import RealtimeWeb.ErrorHelpers
1212

1313
def call(conn, {:error, :not_found}) do
14+
log_error("TenantNotFound", "Tenant not found")
15+
1416
conn
1517
|> put_status(:not_found)
1618
|> put_view(RealtimeWeb.ErrorView)
17-
|> render("error.json", message: "Not found")
19+
|> render("error.json", message: "not found")
1820
end
1921

2022
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do

lib/realtime_web/controllers/tenant_controller.ex

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ defmodule RealtimeWeb.TenantController do
2828

2929
action_fallback(RealtimeWeb.FallbackController)
3030

31-
plug :set_observability_attributes when action in [:show, :edit, :update, :delete, :reload, :health]
31+
plug :set_observability_attributes when action in [:show, :edit, :update, :delete, :reload, :shutdown, :health]
3232

3333
operation(:index,
3434
summary: "List tenants",
@@ -77,13 +77,8 @@ defmodule RealtimeWeb.TenantController do
7777
tenant = Api.get_tenant_by_external_id(id)
7878

7979
case tenant do
80-
%Tenant{} = tenant ->
81-
render(conn, "show.json", tenant: tenant)
82-
83-
nil ->
84-
conn
85-
|> put_status(404)
86-
|> render("not_found.json", tenant: nil)
80+
%Tenant{} = tenant -> render(conn, "show.json", tenant: tenant)
81+
nil -> {:error, :not_found}
8782
end
8883
end
8984

@@ -201,7 +196,6 @@ defmodule RealtimeWeb.TenantController do
201196
send_resp(conn, 204, "")
202197
else
203198
nil ->
204-
log_error("TenantNotFound", "Tenant not found")
205199
send_resp(conn, 204, "")
206200

207201
err ->
@@ -233,11 +227,7 @@ defmodule RealtimeWeb.TenantController do
233227
def reload(conn, %{"tenant_id" => tenant_id}) do
234228
case Api.get_tenant_by_external_id(tenant_id, use_replica?: false) do
235229
nil ->
236-
log_error("TenantNotFound", "Tenant not found")
237-
238-
conn
239-
|> put_status(404)
240-
|> render("not_found.json", tenant: nil)
230+
{:error, :not_found}
241231

242232
tenant ->
243233
PostgresCdc.stop_all(tenant, @stop_timeout)
@@ -247,6 +237,37 @@ defmodule RealtimeWeb.TenantController do
247237
end
248238
end
249239

240+
operation(:shutdown,
241+
summary: "Shutdowns the Connect module for a tenant",
242+
parameters: [
243+
token: [
244+
in: :header,
245+
name: "Authorization",
246+
schema: %OpenApiSpex.Schema{type: :string},
247+
required: true,
248+
example:
249+
"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2ODAxNjIxNTR9.U9orU6YYqXAtpF8uAiw6MS553tm4XxRzxOhz2IwDhpY"
250+
],
251+
tenant_id: [in: :path, description: "Tenant ID", type: :string]
252+
],
253+
responses: %{
254+
204 => EmptyResponse.response(),
255+
403 => EmptyResponse.response(),
256+
404 => NotFoundResponse.response()
257+
}
258+
)
259+
260+
def shutdown(conn, %{"tenant_id" => tenant_id}) do
261+
case Api.get_tenant_by_external_id(tenant_id, use_replica?: false) do
262+
nil ->
263+
{:error, :not_found}
264+
265+
tenant ->
266+
Connect.shutdown(tenant.external_id)
267+
send_resp(conn, 204, "")
268+
end
269+
end
270+
250271
operation(:health,
251272
summary: "Tenant health",
252273
parameters: [
@@ -269,18 +290,9 @@ defmodule RealtimeWeb.TenantController do
269290

270291
def health(conn, %{"tenant_id" => tenant_id}) do
271292
case Tenants.health_check(tenant_id) do
272-
{:ok, response} ->
273-
json(conn, %{data: response})
274-
275-
{:error, %{healthy: false} = response} ->
276-
json(conn, %{data: response})
277-
278-
{:error, :tenant_not_found} ->
279-
log_error("TenantNotFound", "Tenant not found")
280-
281-
conn
282-
|> put_status(404)
283-
|> render("not_found.json", tenant: nil)
293+
{:ok, response} -> json(conn, %{data: response})
294+
{:error, %{healthy: false} = response} -> json(conn, %{data: response})
295+
{:error, :tenant_not_found} -> {:error, :not_found}
284296
end
285297
end
286298

lib/realtime_web/router.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ defmodule RealtimeWeb.Router do
9090

9191
resources("/tenants", TenantController, param: "tenant_id", except: [:edit, :new])
9292
post("/tenants/:tenant_id/reload", TenantController, :reload)
93+
post("/tenants/:tenant_id/shutdown", TenantController, :shutdown)
9394
get("/tenants/:tenant_id/health", TenantController, :health)
9495
end
9596

test/realtime_web/controllers/tenant_controller_test.exs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ defmodule RealtimeWeb.TenantControllerTest do
4949
test "returns not found on non existing tenant", %{conn: conn} do
5050
conn = get(conn, ~p"/api/tenants/no")
5151
response = json_response(conn, 404)
52-
assert response == %{"error" => "not found"}
52+
assert response == %{"message" => "not found"}
5353
end
5454

5555
test "sets appropriate observability metadata", %{conn: conn, tenant: tenant} do
@@ -331,6 +331,59 @@ defmodule RealtimeWeb.TenantControllerTest do
331331
end
332332
end
333333

334+
describe "shutdown Connect module for tenant" do
335+
setup [:with_tenant]
336+
337+
test "shuts down Connect process when tenant exists", %{conn: conn, tenant: %{external_id: external_id}} do
338+
Phoenix.PubSub.subscribe(Realtime.PubSub, "realtime:operations:" <> external_id)
339+
340+
{:ok, connect_pid} = Connect.lookup_or_start_connection(external_id)
341+
Process.monitor(connect_pid)
342+
343+
assert Process.alive?(connect_pid)
344+
345+
%{status: status} = post(conn, ~p"/api/tenants/#{external_id}/shutdown")
346+
347+
assert status == 204
348+
assert_receive {:DOWN, _, :process, ^connect_pid, _}
349+
refute Process.alive?(connect_pid)
350+
end
351+
352+
test "returns 204 when tenant exists but Connect is not running", %{conn: conn, tenant: %{external_id: external_id}} do
353+
%{status: status} = post(conn, ~p"/api/tenants/#{external_id}/shutdown")
354+
assert status == 204
355+
end
356+
357+
test "returns 404 when tenant does not exist", %{conn: conn} do
358+
%{status: status} = post(conn, ~p"/api/tenants/nope/shutdown")
359+
assert status == 404
360+
end
361+
362+
test "returns 403 when jwt is invalid", %{conn: conn, tenant: tenant} do
363+
conn = put_req_header(conn, "authorization", "Bearer potato")
364+
conn = post(conn, ~p"/api/tenants/#{tenant.external_id}/shutdown")
365+
assert response(conn, 403) == ""
366+
end
367+
368+
test "sets appropriate observability metadata", %{conn: conn, tenant: tenant} do
369+
external_id = tenant.external_id
370+
371+
Tracer.with_span "test" do
372+
Task.async(fn ->
373+
post(conn, ~p"/api/tenants/#{tenant.external_id}/shutdown")
374+
375+
assert Logger.metadata()[:external_id] == external_id
376+
assert Logger.metadata()[:project] == external_id
377+
end)
378+
|> Task.await()
379+
end
380+
381+
assert_receive {:span, span(name: "POST /api/tenants/:tenant_id/shutdown", attributes: attributes)}
382+
383+
assert attributes(map: %{external_id: ^external_id}) = attributes
384+
end
385+
end
386+
334387
describe "health check tenant" do
335388
setup [:with_tenant]
336389

0 commit comments

Comments
 (0)