neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
Ankyra: deleting one works, and its worker stops with it
17c8ef1 · Gabe Morell · 2026-11-04 22:51
Message
{commit_body(@commit)}
Files changed
modified
apps/room_sanctum/lib/room_sanctum/accounts.ex
+30
−1
@@ -86,6 +86,12 @@ defmodule RoomSanctum.Accounts do
| 86 | 86 | @doc """ |
| 87 | 87 | Deletes a rabbit_user. |
| 88 | 88 | |
| 89 | + The row is all there is to undo. Nothing is set up on the broker when one | |
| 90 | + is created: RabbitMQ asks room_hermes's HTTP auth backend about every | |
| 91 | + login and topic, and that answers from this table, so once the row is gone | |
| 92 | + the username and password stop working. Its Ankyra worker is told, so it | |
| 93 | + stops rather than finding out on its next re-read. | |
| 94 | + | |
| 89 | 95 | ## Examples |
| 90 | 96 | |
| 91 | 97 | iex> delete_rabbit_user(rabbit_user) |
@@ -96,9 +102,32 @@ defmodule RoomSanctum.Accounts do
| 96 | 102 | |
| 97 | 103 | """ |
| 98 | 104 | def delete_rabbit_user(%RabbitUser{} = rabbit_user) do |
| 99 | − Repo.delete(rabbit_user) | |
| 105 | + rabbit_user |> Repo.delete() |> tap(&stop_worker/1) | |
| 106 | + end | |
| 107 | + | |
| 108 | + @doc """ | |
| 109 | + Deletes a rabbit_user as `delete_rabbit_user/1` does, raising if it cannot | |
| 110 | + -- `Ecto.StaleEntryError` when it is already gone. | |
| 111 | + | |
| 112 | + ## Examples | |
| 113 | + | |
| 114 | + iex> delete_rabbit_user!(rabbit_user) | |
| 115 | + %RabbitUser{} | |
| 116 | + | |
| 117 | + """ | |
| 118 | + def delete_rabbit_user!(%RabbitUser{} = rabbit_user) do | |
| 119 | + deleted = Repo.delete!(rabbit_user) | |
| 120 | + stop_worker({:ok, deleted}) | |
| 121 | + deleted | |
| 100 | 122 | end |
| 101 | 123 | |
| 124 | + # The worker re-reads its row, and stops when there is none. A cast, so it | |
| 125 | + # is a no-op when no worker is running. | |
| 126 | + defp stop_worker({:ok, %RabbitUser{id: id}}), | |
| 127 | + do: RoomSanctum.Worker.Ankyra.refresh_db_cfg(to_string(id)) | |
| 128 | + | |
| 129 | + defp stop_worker(_result), do: :ok | |
| 130 | + | |
| 102 | 131 | @doc """ |
| 103 | 132 | Returns an `%Ecto.Changeset{}` for tracking rabbit_user changes. |
| 104 | 133 |
modified
apps/room_sanctum/lib/room_sanctum/workers/ankyra_worker.ex
+7
−1
@@ -1,6 +1,8 @@
| 1 | 1 | defmodule RoomSanctum.Worker.Ankyra do |
| 2 | 2 | @moduledoc false |
| 3 | − use GenServer | |
| 3 | + # Transient: a worker whose rabbit user has been deleted stops normally and | |
| 4 | + # stays stopped, rather than being restarted into the same missing row. | |
| 5 | + use GenServer, restart: :transient | |
| 4 | 6 | |
| 5 | 7 | require Logger |
| 6 | 8 |
@@ -180,6 +182,10 @@ defmodule RoomSanctum.Worker.Ankyra do
| 180 | 182 | def handle_cast(:refresh_db_cfg, state) do |
| 181 | 183 | p = Accounts.get_rabbit_user!(state[:id]) |
| 182 | 184 | {:noreply, state |> Map.put(:ankyra, p) |> listen_for_requests()} |
| 185 | + rescue | |
| 186 | + # Deleted. Raising here had the supervisor restart it straight back into | |
| 187 | + # the same missing row, until it gave up and took every Ankyra with it. | |
| 188 | + Ecto.NoResultsError -> {:stop, :normal, state} | |
| 183 | 189 | end |
| 184 | 190 | |
| 185 | 191 | def handle_cast({:publish, data}, state) do |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_ankyra/index.ex
+1
−1
@@ -42,7 +42,7 @@ defmodule RoomSanctumWeb.AnkyraLive.Index do
| 42 | 42 | @impl true |
| 43 | 43 | def handle_event("delete", %{"id" => id}, socket) do |
| 44 | 44 | ankyra = Accounts.get_rabbit_user!(owner(socket), id) |
| 45 | − {:ok, _} = Accounts.delete_rabbit_user!(ankyra) | |
| 45 | + Accounts.delete_rabbit_user!(ankyra) | |
| 46 | 46 | |
| 47 | 47 | {:noreply, stream_delete(socket, :ankyras, ankyra)} |
| 48 | 48 | end |
added
apps/room_sanctum/test/room_sanctum/delete_rabbit_user_test.exs
+48
−0
@@ -0,0 +1,48 @@
| 1 | +defmodule RoomSanctum.DeleteRabbitUserTest do | |
| 2 | + @moduledoc """ | |
| 3 | + Deleting an Ankyra -- a rabbit user -- removes its row, which is all the | |
| 4 | + broker knows it by: RabbitMQ asks room_hermes's HTTP auth backend, which | |
| 5 | + reads this table, and nothing is provisioned on the broker itself. Its | |
| 6 | + worker, re-reading a row that is gone, stops and stays stopped. | |
| 7 | + """ | |
| 8 | + use RoomSanctum.DataCase | |
| 9 | + | |
| 10 | + import RoomSanctum.OwnershipFixtures | |
| 11 | + | |
| 12 | + alias RoomSanctum.{Accounts, Repo} | |
| 13 | + alias RoomSanctum.Worker.Ankyra | |
| 14 | + | |
| 15 | + setup :users_rabbit! | |
| 16 | + | |
| 17 | + test "delete_rabbit_user!/1 removes the row and returns it" do | |
| 18 | + ankyra = ankyra_for(owner()) | |
| 19 | + | |
| 20 | + assert %Accounts.RabbitUser{id: id} = Accounts.delete_rabbit_user!(ankyra) | |
| 21 | + assert id == ankyra.id | |
| 22 | + refute Repo.reload(ankyra) | |
| 23 | + refute Accounts.find_rabbit_user(ankyra.username, ankyra.password) | |
| 24 | + end | |
| 25 | + | |
| 26 | + test "delete_rabbit_user!/1 raises when there is nothing left to delete" do | |
| 27 | + ankyra = ankyra_for(owner()) | |
| 28 | + {:ok, _} = Accounts.delete_rabbit_user(ankyra) | |
| 29 | + | |
| 30 | + assert_raise Ecto.StaleEntryError, fn -> Accounts.delete_rabbit_user!(ankyra) end | |
| 31 | + end | |
| 32 | + | |
| 33 | + test "delete_rabbit_user/1 removes the row" do | |
| 34 | + ankyra = ankyra_for(owner()) | |
| 35 | + | |
| 36 | + assert {:ok, _} = Accounts.delete_rabbit_user(ankyra) | |
| 37 | + refute Repo.reload(ankyra) | |
| 38 | + end | |
| 39 | + | |
| 40 | + test "a worker whose rabbit user is gone stops normally, and is not restarted" do | |
| 41 | + ankyra = ankyra_for(owner()) | |
| 42 | + {:ok, _} = Accounts.delete_rabbit_user(ankyra) | |
| 43 | + state = %{id: to_string(ankyra.id), ankyra: ankyra, requests: nil} | |
| 44 | + | |
| 45 | + assert {:stop, :normal, _state} = Ankyra.handle_cast(:refresh_db_cfg, state) | |
| 46 | + assert %{restart: :transient} = Ankyra.child_spec(id: "1") | |
| 47 | + end | |
| 48 | +end |
modified
apps/room_sanctum/test/room_sanctum_web/live/ownership_test.exs
+9
−0
@@ -495,6 +495,15 @@ defmodule RoomSanctumWeb.OwnershipTest do
| 495 | 495 | |
| 496 | 496 | assert Repo.reload(theirs) |
| 497 | 497 | end |
| 498 | + | |
| 499 | + test "deletes the user's own ankyra", ctx do | |
| 500 | + mine = ankyra_for(ctx.me) | |
| 501 | + {:ok, live, _html} = live(ctx.conn, Routes.ankyra_index_path(ctx.conn, :index)) | |
| 502 | + | |
| 503 | + render_click(live, "delete", %{"id" => to_string(mine.id)}) | |
| 504 | + | |
| 505 | + refute Repo.reload(mine) | |
| 506 | + end | |
| 498 | 507 | end |
| 499 | 508 | |
| 500 | 509 | defp scribus_for(user) do |
Parents: feea568