Skip to content

Inconsistent behavior when calling/casting a Registry-registered process with metadata in the via tuple #11740

@smaximov

Description

@smaximov

When registering a process in Registry using the :name option of GenServer.start_link/3, you can pass two kinds of tuples as the name:

  1. Tuple without metadata, e.g. {:via, Registry, {RegistryName, "key"}}.
  2. Tuple with associated metadata: {:via, Registry, {RegistryName, "key", %{meta: "data"}}}.

Let's assume we registered a process with metadata; if you later send a message to the process via GenServer.call/2 using the via-tuple, both form will work:

GenServer.call({:via, Registry, {RegistryName, "key"}}, :msg)
GenServer.call({:via, Registry, {RegistryName, "key", %{meta: "data"}}}, :msg)

But it isn't the case for GenServer.cast/2 — when using the via-tuple form with metadata to refer to the process, it doesn't receive any messages while tuple name without metadata still works.

Current behavior

Consider this snippet:

#!/usr/bin/env elixir

ExUnit.start()

defmodule Counter do
  use GenServer

  def start_link(opts) do
    initial_value = Keyword.get(opts, :initial_value, 0)
    GenServer.start_link(__MODULE__, initial_value, opts)
  end

  def init(initial_value) do
    {:ok, initial_value}
  end

  def value(counter) do
    GenServer.call(counter, :value)
  end

  def inc(counter, value \\ 1) do
    GenServer.call(counter, {:inc, value})
  end

  def inc_async(counter, value \\ 1) do
    GenServer.cast(counter, {:inc, value})
  end

  def handle_call(:value, _from, state) do
    {:reply, state, state}
  end

  def handle_call({:inc, value}, _from, state) do
    state = state + value
    {:reply, state, state}
  end

  def handle_cast({:inc, value}, state) do
    state = state + value

    {:noreply, state}
  end
end

defmodule RegistryTest do
  use ExUnit.Case

  setup %{test: test_name} do
    start_supervised!({Registry, name: test_name, keys: :unique})

    {:ok, registry: test_name}
  end

  test "call w/o metadata", %{registry: registry} do
    registered_name = {:via, Registry, {registry, "counter", %{meta: "data"}}}
    counter_name = {:via, Registry, {registry, "counter"}}

    start_supervised!({Counter, name: registered_name})

    assert Counter.inc(counter_name) == 1
  end

  test "cast w/o metadata", %{registry: registry} do
    registered_name = {:via, Registry, {registry, "counter", %{meta: "data"}}}
    counter_name = {:via, Registry, {registry, "counter"}}

    start_supervised!({Counter, name: registered_name})

    Counter.inc_async(counter_name)
    Process.sleep(100)

    assert Counter.value(counter_name) == 1
  end

  test "call w/ metadata", %{registry: registry} do
    counter_name = registered_name = {:via, Registry, {registry, "counter", %{meta: "data"}}}

    start_supervised!({Counter, name: registered_name})

    assert Counter.inc(counter_name) == 1
  end

  test "cast w/ metadata", %{registry: registry} do
    counter_name = registered_name = {:via, Registry, {registry, "counter", %{meta: "data"}}}

    start_supervised!({Counter, name: registered_name})

    Counter.inc_async(counter_name)
    Process.sleep(100)

    assert Counter.value(counter_name) == 1
  end
end

It has four tests, each test registers a process using a via-tuple with metadata. Two tests calls the process using GenServer.call/2 using the via-tuple name with and without metadata, another two — using GenServer.cast/2 (again, with and without metadata).

Running this snippet results in a failing test:

..

  1) test cast w/ metadata (RegistryTest)
     example.exs:83
     Assertion with == failed
     code:  assert Counter.value(counter_name) == 1
     left:  0
     right: 1
     stacktrace:
       example.exs:91: (test)

.

Finished in 0.2 seconds (0.06s on load, 0.00s async, 0.2s sync)
4 tests, 1 failure

Randomized with seed 188480

"test cast w/ metadata" fails because the process haven't received the message and haven't changed its state.

Expected behavior

Casting a process should behave similar to calling a process when using the via-tuple form with metadata: so either both "test cast w/ metadata" and "test cast w/ metadata" tests should pass, or both of them should fail.

Environment

  • Elixir & Erlang/OTP versions:

    Erlang/OTP 24 [erts-12.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
    
    Elixir 1.13.3 (compiled with Erlang/OTP 24)
    
  • Operating system: NixOS 21.09

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions