@@ -55,21 +55,18 @@ defmodule Sentry.Test.Registry do
5555 end
5656
5757 @ doc """
58- Removes every routing row whose owner is `owner_pid`. Direct ETS
59- match_delete — atomic, no GenServer round-trip.
60-
61- Kept for the case where a scope wants to drop its allowances
62- explicitly (e.g. `Sentry.Test.Scope.Registry.unregister/1`); the
63- `:DOWN` handler also prunes rows automatically when the owner pid
64- exits.
58+ Ensures `owner_pid` is monitored by the registry so that the
59+ `:DOWN` handler runs cleanup (routing-table prune + scope-state
60+ erase via `Sentry.Test.Scope.Registry.handle_owner_down/1`) when
61+ the owner exits. Idempotent.
62+
63+ Called from `Sentry.Test.Scope.Registry.update/1` on first scope
64+ creation so cleanup does not depend on `claim_allow` ever being
65+ invoked for this owner.
6566 """
66- @ spec drop_allows_for ( pid ( ) ) :: :ok
67- def drop_allows_for ( owner_pid ) when is_pid ( owner_pid ) do
68- if :ets . whereis ( @ routing_table ) != :undefined do
69- :ets . match_delete ( @ routing_table , { :_ , owner_pid , :_ } )
70- end
71-
72- :ok
67+ @ spec monitor_owner ( pid ( ) ) :: :ok
68+ def monitor_owner ( owner_pid ) when is_pid ( owner_pid ) do
69+ GenServer . call ( __MODULE__ , { :monitor_owner , owner_pid } )
7370 end
7471
7572 @ doc """
@@ -166,38 +163,63 @@ defmodule Sentry.Test.Registry do
166163 { :ok , % { owner_monitors: % { } } }
167164 end
168165
166+ # Serialization note: every claim funnels through this single named
167+ # GenServer and holds it across TWO blocking round-trips to the
168+ # ownership server — `ensure_scope_owner/1`'s
169+ # `NimbleOwnership.get_and_update/4` and `NimbleOwnership.allow/4`.
170+ # This is the deliberate price of atomicity (no two concurrent async
171+ # tests can both pass a check-then-write race for the same
172+ # `allowed_pid`). It is acceptable because claims happen at test
173+ # setup, not per event, and the hot config/buffer read paths
174+ # (`lookup_allow_owner/1`, `lookup_processor_for/1`) bypass this
175+ # GenServer with lock-free direct ETS reads.
169176 @ impl true
170177 def handle_call ( { :claim_allow , owner_pid , allowed_pid , mode } , _from , state ) do
171178 state = ensure_owner_monitored ( state , owner_pid )
172- ensure_scope_owner ( owner_pid )
173179
174180 reply =
175- case NimbleOwnership . allow ( @ ownership_server , owner_pid , allowed_pid , @ scope_key ) do
181+ case ensure_scope_owner ( owner_pid ) do
182+ { :error , { :taken , existing_owner } } ->
183+ if mode == :strict , do: { :error , { :taken , existing_owner } } , else: :skipped
184+
176185 :ok ->
177- upsert_owner ( allowed_pid , owner_pid )
178- :ok
186+ case NimbleOwnership . allow ( @ ownership_server , owner_pid , allowed_pid , @ scope_key ) do
187+ :ok ->
188+ upsert_owner ( allowed_pid , owner_pid )
189+ :ok
179190
180- { :error , % { reason: { :already_allowed , ^ owner_pid } } } ->
181- upsert_owner ( allowed_pid , owner_pid )
182- :ok
191+ { :error , % { reason: { :already_allowed , ^ owner_pid } } } ->
192+ upsert_owner ( allowed_pid , owner_pid )
193+ :ok
183194
184- { :error , % { reason: { :already_allowed , other } } } ->
185- if mode == :strict , do: { :error , { :taken , other } } , else: :skipped
195+ { :error , % { reason: { :already_allowed , other } } } ->
196+ if mode == :strict , do: { :error , { :taken , other } } , else: :skipped
186197
187- { :error , % { reason: :already_an_owner } } ->
188- # `allowed_pid` is itself a scope owner — treat as a conflict.
189- if mode == :strict , do: { :error , { :taken , allowed_pid } } , else: :skipped
198+ { :error , % { reason: :already_an_owner } } ->
199+ # `allowed_pid` is itself a scope owner — treat as a conflict.
200+ if mode == :strict , do: { :error , { :taken , allowed_pid } } , else: :skipped
201+
202+ { :error , % { reason: :not_allowed } } ->
203+ if mode == :strict , do: { :error , { :taken , allowed_pid } } , else: :skipped
204+ end
190205 end
191206
192207 { :reply , reply , state }
193208 end
194209
210+ def handle_call ( { :monitor_owner , owner_pid } , _from , state ) do
211+ state = ensure_owner_monitored ( state , owner_pid )
212+ { :reply , :ok , state }
213+ end
214+
195215 @ impl true
196216 def handle_info ( { :DOWN , _ref , :process , pid , _reason } , state ) do
197217 if :ets . whereis ( @ routing_table ) != :undefined do
198218 :ets . match_delete ( @ routing_table , { :_ , pid , :_ } )
199219 end
200220
221+ Sentry.Test.Scope.Registry . handle_owner_down ( pid )
222+
201223 { :noreply , % { state | owner_monitors: Map . delete ( state . owner_monitors , pid ) } }
202224 end
203225
@@ -220,21 +242,38 @@ defmodule Sentry.Test.Registry do
220242 # `Sentry.Test.setup_collector/1` (e.g. a test that uses
221243 # `Sentry.Test.Config.put/1` standalone). When the owner already
222244 # owns the key, the existing metadata is preserved.
245+ #
246+ # INVARIANT: the `:sentry_test_scope` key's metadata is overloaded —
247+ # `Sentry.Test.setup_collector/1` stores the per-test collector ETS
248+ # table name (an atom) under it, while this function stores a bare
249+ # `%{}` marker for collector-less scopes. `Sentry.Test`'s
250+ # `owner_collecting?/1` distinguishes the two purely by value type
251+ # (atom = collecting, map = not). Therefore the update fun below MUST
252+ # preserve an existing value (`current -> {:ok, current}`) and MUST
253+ # NOT overwrite it with `%{}`; doing so would silently turn a
254+ # collecting scope into a non-collecting one with no type error.
223255 defp ensure_scope_owner ( owner_pid ) do
224256 case NimbleOwnership . get_and_update (
225257 @ ownership_server ,
226258 owner_pid ,
227259 @ scope_key ,
228260 # Metadata MUST be non-nil so that NimbleOwnership treats
229261 # `owner_pid` as a key owner (its `cond` in `allow/4` checks
230- # truthiness of the metadata). Preserve any existing value.
262+ # truthiness of the metadata). Preserve any existing value
263+ # (see the INVARIANT above — never clobber a collector atom).
231264 fn
232265 nil -> { :ok , % { } }
233266 current -> { :ok , current }
234267 end
235268 ) do
236- { :ok , _ } -> :ok
237- { :error , _ } -> :ok
269+ { :ok , _ } ->
270+ :ok
271+
272+ { :error , % { reason: { :already_allowed , existing_owner } } } ->
273+ { :error , { :taken , existing_owner } }
274+
275+ { :error , _ } ->
276+ :ok
238277 end
239278 end
240279
0 commit comments