Registry 모듈

Registry 모듈

로컬·분산·확장 가능한(scalable) 키-값 프로세스 저장소예요. 개발자가 주어진 키로 하나 이상의 프로세스를 조회하게 해 줍니다. 레지스트리가 :unique 키를 가지면 키는 0개 또는 1개의 프로세스를 가리키고, :duplicate 키를 허용하면 한 키가 여러 프로세스를 가리킬 수 있어요. 두 경우 모두 서로 다른 키가 같은 프로세스를 식별할 수 있습니다.

출처: Registry

본문

레지스트리의 각 항목은 키를 등록한 프로세스와 연결됩니다. 프로세스가 죽으면 그 프로세스와 연결된 키는 자동으로 제거됩니다. 레지스트리의 모든 키 비교는 매치 연산(===/2)으로 이루어져요.

레지스트리는 이름 조회(:via 옵션 사용), 속성 저장, 커스텀 디스패치 규칙, pubsub 구현 등 다양한 목적으로 쓸 수 있어요. 아래에서 그중 몇 가지 사용 사례를 살펴볼게요.

레지스트리는 투명하게 파티션될 수도 있는데, 수천·수백만 항목이 있는 고동시성 환경에서 레지스트리를 실행할 때 더 확장 가능한 동작을 제공합니다.

:via에서 사용하기

Registry.start_link/1로 레지스트리를 주어진 이름으로 시작하면, {:via, Registry, {registry, key}} 튜플로 이름 있는 프로세스를 등록·접근하는 데 쓸 수 있어요.

{:ok, _} = Registry.start_link(keys: :unique, name: MyApp.Registry)
name = {:via, Registry, {MyApp.Registry, "agent"}}
{:ok, _} = Agent.start_link(fn -> 0 end, name: name)
Agent.get(name, & &1)
#=> 0
Agent.update(name, &(&1 + 1))
Agent.get(name, & &1)
#=> 1

이전 예시에서는 프로세스에 값을 연관시키는 데 관심이 없었어요.

Registry.lookup(MyApp.Registry, "agent")
#=> [{self(), nil}]

하지만 어떤 경우에는 대체 {:via, Registry, {registry, key, value}} 튜플로 프로세스에 값을 연관시키는 것이 바람직할 수 있어요.

{:ok, _} = Registry.start_link(keys: :unique, name: MyApp.Registry)
name = {:via, Registry, {MyApp.Registry, "agent", :hello}}
{:ok, agent_pid} = Agent.start_link(fn -> 0 end, name: name)

Registry.lookup(MyApp.Registry, "agent")
#=> [{agent_pid, :hello}]

name_without_meta = {:via, Registry, {MyApp.Registry, "agent"}}
Agent.update(name_without_meta, fn x -> x + 1 end)
Agent.get(name_without_meta, & &1)
#=> 1

메타데이터가 있는 경우와 없는 경우

메타데이터가 있는 :via 튜플 버전을 쓰더라도, 메타데이터 없이 프로세스를 조회하는 버전은 여전히 쓸 수 있어요.

지금까지 start_link/1Registry를 시작해 왔는데, 보통 레지스트리는 감독 트리의 일부로 시작됩니다.

{Registry, keys: :unique, name: MyApp.Registry}

:via에는 고유 키를 가진 레지스트리만 쓸 수 있어요. 이름이 이미 사용 중이면 사례별 start_link 함수(위 예시에서는 Agent.start_link/2)가 {:error, {:already_started, current_pid}}를 반환합니다.

디스패처(dispatcher)로 사용하기

Registry는 호출자에서 발동되는 커스텀 디스패치 로직을 구현할 수 있게 해 주는 디스패치 메커니즘을 가져요. 예를 들어 중복 레지스트리를 이렇게 시작했다고 해 보죠.

{:ok, _} = Registry.start_link(keys: :duplicate, name: Registry.DispatcherTest)

register/3를 호출하면 서로 다른 프로세스가 주어진 키 아래에 등록하고 그 키 아래에 어떤 값을 연관시킬 수 있어요. 이 경우 "hello" 키 아래에 현재 프로세스를 등록하고 {IO, :inspect} 튜플을 붙여 봅시다.

{:ok, _} = Registry.register(Registry.DispatcherTest, "hello", {IO, :inspect})

이제 주어진 키에 대한 이벤트를 디스패치하는 데 관심 있는 개체는 dispatch/3에 키와 콜백을 넘겨 호출할 수 있어요. 이 콜백은 요청된 키 아래에 등록된 모든 값의 리스트와 함께, 각 값을 등록한 프로세스의 PID가 {pid, value} 튜플 형태로 전달되어 호출됩니다. 우리 예시에서 value는 위 코드의 {module, function} 튜플이 될 거예요.

Registry.dispatch(Registry.DispatcherTest, "hello", fn entries ->
  for {pid, {module, function}} <- entries, do: apply(module, function, [pid])
end)
# Prints #PID<...> where the PID is for the process that called register/3 above
#=> :ok

디스패치는 dispatch/3을 호출하는 프로세스에서 발생하며, 여러 파티션인 경우 (spawn된 태스크를 통해) 직렬 또는 동시에 일어나요. 등록된 프로세스들은 명시적으로 관련시키지 않는 한 디스패치에 관여하지 않습니다(예: 콜백에서 메시지를 보내는 경우).

게다가 디스패치 시 잘못된 등록으로 실패가 있으면 디스패치는 항상 실패하고 등록된 프로세스는 알림을 받지 못해요. 따라서 적어도 그 오류들을 감싸서 보고하도록 합시다.

require Logger

Registry.dispatch(Registry.DispatcherTest, "hello", fn entries ->
  for {pid, {module, function}} <- entries do
    try do
      apply(module, function, [pid])
    catch
      kind, reason ->
        formatted = Exception.format(kind, reason, __STACKTRACE__)
        Logger.error("Registry.dispatch/3 failed with #{formatted}")
    end
  end
end)
# Prints #PID<...>
#=> :ok

apply 시스템 전체를 메시지를 명시적으로 보내는 것으로 대체할 수도 있어요. 그게 다음에 볼 예시입니다.

PubSub로 사용하기

레지스트리는 dispatch/3 함수에 의존해서 로컬·비분산·확장 가능한 PubSub을 구현하는 데도 쓸 수 있어요. 이전 섹션과 비슷하지만, 이 경우에는 특정 모듈-함수를 호출하는 대신 각 연관 프로세스에 메시지를 보내요.

이 예시에서는 파티션 수를 온라인 스케줄러 수로 설정할 텐데, 이렇게 하면 고동시성 환경에서 레지스트리가 더 성능이 좋아집니다.

{:ok, _} =
  Registry.start_link(
    keys: :duplicate,
    name: Registry.PubSubTest,
    partitions: System.schedulers_online()
  )

{:ok, _} = Registry.register(Registry.PubSubTest, "hello", [])

Registry.dispatch(Registry.PubSubTest, "hello", fn entries ->
  for {pid, _} <- entries, do: send(pid, {:broadcast, "world"})
end)
#=> :ok

위 예시는 "hello"라는 "토픽"(지금까지 우리가 부른 "키") 아래에 등록된 모든 프로세스에 {:broadcast, "world"} 메시지를 브로드캐스트했습니다.

register/3에 주어지는 세 번째 인자는 현재 프로세스에 연관된 값이에요. 이전 섹션에서는 디스패치할 때 그걸 썼지만, 이 특정 예시에서는 그 값에 관심이 없으므로 빈 리스트로 설정했어요. 필요하다면 더 의미 있는 값을 저장할 수 있습니다.

등록(Registrations)

조회·디스패치·등록은 지연된 구독 해제(unsubscription)를 대가로 효율적이고 즉각적이에요. 예를 들어 프로세스가 죽으면 그 키는 레지스트리에서 자동으로 제거되지만 그 변화가 즉시 전파되지는 않을 수 있어요. 즉 특정 연산이 이미 죽은 프로세스를 반환할 수 있다는 뜻입니다. 그런 일이 일어날 수 있을 때는 함수 문서에 명시적으로 명시될 거예요.

다만 그런 경우들은 보통 문제가 되지 않는다는 점을 명심하세요. 어차피 PID가 참조하는 프로세스는 레지스트리에서 값을 얻고 메시지를 보내는 사이를 포함해 언제든 죽을 수 있어요. 표준 라이브러리의 많은 부분이 이를 대처하도록 설계되어 있어요. 예를 들어 Process.monitor/1은 모니터링되는 프로세스가 이미 죽어 있으면 :DOWN 메시지를 즉시 전달하고, send/2는 죽은 프로세스에 대해 no-op으로 동작합니다.

ETS

레지스트리는 하나의 ETS 테이블과 파티션당 두 개의 ETS 테이블을 사용한다는 점에 유의하세요.

타입(Types)

@type body() :: [term()]
@type dispatch_opts() :: [{:parallel, boolean()}]
@type guard() :: atom() | tuple()
@type guards() :: [guard()]
@type key() :: term()
@type keys() :: :unique | :duplicate | {:duplicate, :key} | {:duplicate, :pid}
@type listener_message() ::
  {:register, registry(), key(), registry_partition :: pid(), value()}
  | {:unregister, registry(), key(), registry_partition :: pid()}
@type match_pattern() :: atom() | term()
@type meta_key() :: atom() | tuple()
@type meta_value() :: term()
@type registry() :: atom()
@type spec() :: [{match_pattern(), guards(), body()}]
@type start_option() ::
  {:keys, keys()}
  | {:name, registry()}
  | {:partitions, pos_integer()}
  | {:listeners, [atom()]}
  | {:meta, [{meta_key(), meta_value()}]}
@type value() :: term()
  • keys() — 레지스트리 타입
  • listener_message() — 프로세스가 등록·해제될 때 레지스트리가 리스너에 보내는 메시지 (start_link/1:listeners 옵션 참고)
  • spec() — 레지스트리의 객체를 선택할 때 쓰는 전체 매치 스펙
  • start_option()child_spec/1start_link/1에 쓰는 옵션

함수(Functions)

child_spec(options) (1.5.0부터)

@spec child_spec([start_option()]) :: Supervisor.child_spec()

감독자 아래 레지스트리를 시작하기 위한 스펙을 반환합니다. Supervisor 참고.

count(registry) (1.7.0부터)

@spec count(registry()) :: non_neg_integer()

레지스트리의 등록된 키 수를 반환합니다. 상수 시간으로 실행돼요.

iex> Registry.start_link(keys: :unique, name: Registry.UniqueCountTest)
iex> Registry.count(Registry.UniqueCountTest)
0
iex> {:ok, _} = Registry.register(Registry.UniqueCountTest, "hello", :world)
iex> {:ok, _} = Registry.register(Registry.UniqueCountTest, "world", :world)
iex> Registry.count(Registry.UniqueCountTest)
2

중복 레지스트리에도 동일하게 적용됩니다.

iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateCountTest)
iex> Registry.count(Registry.DuplicateCountTest)
0
iex> {:ok, _} = Registry.register(Registry.DuplicateCountTest, "hello", :world)
iex> {:ok, _} = Registry.register(Registry.DuplicateCountTest, "hello", :world)
iex> Registry.count(Registry.DuplicateCountTest)
2

count_match(registry, key, pattern, guards \ []) (1.7.0부터)

@spec count_match(registry(), key(), match_pattern(), guards()) :: non_neg_integer()

registry에서 주어진 key 아래에 있고 pattern과 매치되는 {pid, value} 쌍의 수를 반환합니다. 예시 코드는 아래 match/4를 참고하세요.

패턴은 레지스트리에 저장된 값의 구조와 매치될 아톰 또는 튜플이어야 해요. :_ 아톰은 주어진 값이나 튜플 요소를 무시하는 데 쓰고, :"$1" 아톰은 패턴의 일부를 이후 비교를 위해 변수에 임시 할당하는 데 쓸 수 있습니다. 선택적으로 더 정밀한 매칭을 위해 가드 조건 리스트를 전달할 수 있어요. 각 가드는 할당된 패턴 부분이 통과해야 하는 검사를 설명하는 튜플입니다. 예를 들어 $1 > 1 가드 조건은 {:>, :"$1", 1} 튜플로 표현돼요. 가드 조건은 :"$1", :"$2" 같은 할당된 변수에서만 동작합니다. 특수 매치 변수 :"$_":"$$"는 예상대로 동작하지 않을 수 있으니 사용을 피하세요.

매치가 없으면 0이 반환됩니다. 고유 레지스트리에서는 단일 파티션 조회가 필요하고, 중복 레지스트리에서는 모든 파티션을 조회해야 해요.

예시:

iex> Registry.start_link(keys: :duplicate, name: Registry.CountMatchTest)
iex> {:ok, _} = Registry.register(Registry.CountMatchTest, "hello", {1, :atom, 1})
iex> {:ok, _} = Registry.register(Registry.CountMatchTest, "hello", {2, :atom, 2})
iex> Registry.count_match(Registry.CountMatchTest, "hello", {1, :_, :_})
1
iex> Registry.count_match(Registry.CountMatchTest, "hello", {2, :_, :_})
1
iex> Registry.count_match(Registry.CountMatchTest, "hello", {:_, :atom, :_})
2
iex> Registry.count_match(Registry.CountMatchTest, "hello", {:"$1", :_, :"$1"})
2
iex> Registry.count_match(Registry.CountMatchTest, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 1}])
1
iex> Registry.count_match(Registry.CountMatchTest, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}])
2

count_select(registry, spec) (1.14.0부터)

@spec count_select(registry(), spec()) :: non_neg_integer()

select/2처럼 동작하지만 매치되는 레코드 수만 반환합니다.

iex> Registry.start_link(keys: :unique, name: Registry.CountSelectTest)
iex> {:ok, _} = Registry.register(Registry.CountSelectTest, "hello", :value)
iex> {:ok, _} = Registry.register(Registry.CountSelectTest, "world", :value)
iex> Registry.count_select(Registry.CountSelectTest, [{{:_, :_, :value}, [], [true]}])
2

delete_meta(registry, key) (1.11.0부터)

@spec delete_meta(registry(), meta_key()) :: :ok

registry에서 주어진 key에 대한 레지스트리 메타데이터를 삭제합니다.

iex> Registry.start_link(keys: :unique, name: Registry.DeleteMetaTest)
iex> Registry.put_meta(Registry.DeleteMetaTest, :custom_key, "custom_value")
:ok
iex> Registry.meta(Registry.DeleteMetaTest, :custom_key)
{:ok, "custom_value"}
iex> Registry.delete_meta(Registry.DeleteMetaTest, :custom_key)
:ok
iex> Registry.meta(Registry.DeleteMetaTest, :custom_key)
:error

dispatch(registry, key, mfa_or_fun, opts \ []) (1.4.0부터)

@spec dispatch(registry(), key(), dispatcher, dispatch_opts()) :: :ok
when dispatcher:
       (entries :: [{pid(), value()}] -> term()) | {module(), atom(), [term()]}

주어진 registry의 각 파티션에서 key 아래의 모든 항목으로 콜백을 호출합니다. entries 리스트는 두 요소 튜플(첫 요소가 PID, 둘째가 PID에 연관된 값)의 비어 있지 않은 리스트입니다. 주어진 키에 항목이 없으면 콜백은 절대 호출되지 않아요. 레지스트리가 파티션되어 있으면 콜백은 파티션당 여러 번 호출됩니다. 레지스트리가 파티션되어 있고 옵션으로 parallel: true가 주어지면 디스패치가 병렬로 일어나요. 두 경우 모두 콜백은 그 파티션에 항목이 있을 때만 호출됩니다.

{:duplicate, :key} 레지스트리의 경우 주어진 키의 모든 항목이 단일 파티션에 있으므로 :parallel 옵션은 효과가 없어요.

커스텀 디스패칭이나 pubsub 시스템을 만드는 데 dispatch/3을 쓰는 예시는 모듈 문서를 참고하세요.

옵션:

  • :paralleltrue이면 모든 파티션에 걸쳐 병렬로 디스패치. {:duplicate, :pid} 레지스트리에서만 의미 있음. 기본값 false.

keys(registry, pid) (1.4.0부터)

@spec keys(registry(), pid()) :: [key()]

registry에서 주어진 pid의 알려진 키를 특정 순서 없이 반환합니다. 레지스트리가 고유하면 키가 고유합니다. 그렇지 않으면 프로세스가 같은 키로 여러 번 등록되었다면 중복을 포함할 수 있어요. 프로세스가 죽었거나 이 레지스트리에 키가 없으면 리스트는 비어 있습니다.

iex> Registry.start_link(keys: :unique, name: Registry.UniqueKeysTest)
iex> Registry.keys(Registry.UniqueKeysTest, self())
[]
iex> {:ok, _} = Registry.register(Registry.UniqueKeysTest, "hello", :world)
iex> Registry.register(Registry.UniqueKeysTest, "hello", :later) # registry is :unique
{:error, {:already_registered, self()}}
iex> Registry.keys(Registry.UniqueKeysTest, self())
["hello"]

중복 레지스트리에서는 그게 가능합니다.

iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateKeysTest)
iex> Registry.keys(Registry.DuplicateKeysTest, self())
[]
iex> {:ok, _} = Registry.register(Registry.DuplicateKeysTest, "hello", :world)
iex> {:ok, _} = Registry.register(Registry.DuplicateKeysTest, "hello", :world)
iex> Registry.keys(Registry.DuplicateKeysTest, self())
["hello", "hello"]

lock(registry, lock_key, function) (1.18.0부터)

주어진 lock_keyfunction이 실행되는 동안 대역 밖(out-of-band)으로 잠급니다. 같은 lock_key 아래에서는 한 번에 하나의 함수만 실행될 수 있어요. 주어진 함수는 항상 호출자 프로세스에서 실행됩니다. lock_key는 자체 네임스페이스를 가지므로 일반 레지스트리 키와 충돌하거나 겹치지 않아요. 즉 잠금은 일반 Registry 연산과 대역 밖에서 동작합니다. 아래 "Use cases" 섹션 참고. 잠금은 레지스트리 타입과 관계없이 동일하게 동작합니다.

사용 사례(Use cases)

Registry는 기본적으로 안전하고 동시적입니다. Registry와 상호작용할 때 이 함수를 쓸 필요는 없어요. 게다가 :unique 키를 가진 Registry는 이미 어떤 키에 대한 프로세스 잠금으로 동작할 수 있습니다. 예를 들어 주어진 :key에 대해 한 번에 하나의 프로세스만 실행되게 하려면 이렇게 할 수 있어요.

name = {:via, Registry, {MyApp.Registry, :key, :value}}

# Do not attempt to start if we are already running
if pid = GenServer.whereis(name) do
  pid
else
  case GenServer.start_link(__MODULE__, :ok, name: name) do
    {:ok, pid} -> pid
    {:error, {:already_started, pid}} -> pid
  end
end

프로세스 잠금은 충분한 유연성과 결함 격리를 주며 대부분의 경우에 충분해요.

이 함수는 프로세스를 spawn하는 것이 선택지가 아닐 때만 유용한데, 예를 들어 다른 프로세스로 데이터를 복사하는 것이 너무 비쌀 때나, 다른 이유로 작업이 현재 프로세스 안에서 수행되어야 할 때 그렇습니다. 그런 경우 이 함수는 레지스트리의 인프라 위에서 잠금을 관리하는 확장 가능한 메커니즘을 제공합니다.

iex> Registry.start_link(keys: :unique, name: Registry.LockTest)
iex> Registry.lock(Registry.LockTest, :hello, fn -> :ok end)
:ok
iex> Registry.lock(Registry.LockTest, :world, fn -> self() end)
self()

lookup(registry, key) (1.4.0부터)

@spec lookup(registry(), key()) :: [{pid(), value()}]

registry에서 주어진 key{pid, value} 쌍을 특정 순서 없이 찾습니다. 매치가 없으면 빈 리스트예요. 고유 레지스트리에서는 단일 파티션 조회가 필요하고, 중복 레지스트리에서는 모든 파티션을 조회해야 해요.

iex> Registry.start_link(keys: :unique, name: Registry.UniqueLookupTest)
iex> Registry.lookup(Registry.UniqueLookupTest, "hello")
[]
iex> {:ok, _} = Registry.register(Registry.UniqueLookupTest, "hello", :world)
iex> Registry.lookup(Registry.UniqueLookupTest, "hello")
[{self(), :world}]
iex> Task.async(fn -> Registry.lookup(Registry.UniqueLookupTest, "hello") end) |> Task.await()
[{self(), :world}]

중복 레지스트리에도 동일하게 적용됩니다.

iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateLookupTest)
iex> Registry.lookup(Registry.DuplicateLookupTest, "hello")
[]
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :world)
iex> Registry.lookup(Registry.DuplicateLookupTest, "hello")
[{self(), :world}]
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :another)
iex> Enum.sort(Registry.lookup(Registry.DuplicateLookupTest, "hello"))
[{self(), :another}, {self(), :world}]

match(registry, key, pattern, guards \ []) (1.4.0부터)

@spec match(registry(), key(), match_pattern(), guards()) :: [{pid(), term()}]

registry에서 주어진 key 아래에 있고 pattern과 매치되는 {pid, value} 쌍을 반환합니다.

패턴은 레지스트리에 저장된 값의 구조와 매치될 아톰 또는 튜플이어야 해요. :_ 아톰은 주어진 값이나 튜플 요소를 무시하는 데 쓰고, :"$1" 아톰은 패턴의 일부를 이후 비교를 위해 변수에 임시 할당하는 데 쓸 수 있습니다. 선택적으로 더 정밀한 매칭을 위해 가드 조건 리스트를 전달할 수 있어요. 각 가드는 할당된 패턴 부분이 통과해야 하는 검사를 설명하는 튜플입니다. 예를 들어 $1 > 1 가드 조건은 {:>, :"$1", 1} 튜플로 표현돼요. 가드 조건은 :"$1", :"$2" 같은 할당된 변수에서만 동작합니다. 특수 매치 변수 :"$_":"$$"는 예상대로 동작하지 않을 수 있으니 사용을 피하세요.

매치가 없으면 빈 리스트가 반환됩니다. 고유 레지스트리에서는 단일 파티션 조회가 필요하고, 중복 레지스트리에서는 모든 파티션을 조회해야 해요.

iex> Registry.start_link(keys: :duplicate, name: Registry.MatchTest)
iex> {:ok, _} = Registry.register(Registry.MatchTest, "hello", {1, :atom, 1})
iex> {:ok, _} = Registry.register(Registry.MatchTest, "hello", {2, :atom, 2})
iex> Registry.match(Registry.MatchTest, "hello", {1, :_, :_})
[{self(), {1, :atom, 1}}]
iex> Registry.match(Registry.MatchTest, "hello", {2, :_, :_})
[{self(), {2, :atom, 2}}]
iex> Registry.match(Registry.MatchTest, "hello", {:_, :atom, :_}) |> Enum.sort()
[{self(), {1, :atom, 1}}, {self(), {2, :atom, 2}}]
iex> Registry.match(Registry.MatchTest, "hello", {:"$1", :_, :"$1"}) |> Enum.sort()
[{self(), {1, :atom, 1}}, {self(), {2, :atom, 2}}]
iex> guards = [{:>, :"$1", 1}]
iex> Registry.match(Registry.MatchTest, "hello", {:_, :_, :"$1"}, guards)
[{self(), {2, :atom, 2}}]
iex> guards = [{:is_atom, :"$1"}]
iex> Registry.match(Registry.MatchTest, "hello", {:_, :"$1", :_}, guards) |> Enum.sort()
[{self(), {1, :atom, 1}}, {self(), {2, :atom, 2}}]

meta(registry, key) (1.4.0부터)

@spec meta(registry(), meta_key()) :: {:ok, meta_value()} | :error

start_link/1에 주어진 레지스트리 메타데이터를 읽습니다. 아톰과 튜플이 키로 허용돼요.

iex> Registry.start_link(keys: :unique, name: Registry.MetaTest, meta: [custom_key: "custom_value"])
iex> Registry.meta(Registry.MetaTest, :custom_key)
{:ok, "custom_value"}
iex> Registry.meta(Registry.MetaTest, :unknown_key)
:error

put_meta(registry, key, value) (1.4.0부터)

@spec put_meta(registry(), meta_key(), meta_value()) :: :ok

레지스트리 메타데이터를 저장합니다. 아톰과 튜플이 키로 허용돼요.

iex> Registry.start_link(keys: :unique, name: Registry.PutMetaTest)
iex> Registry.put_meta(Registry.PutMetaTest, :custom_key, "custom_value")
:ok
iex> Registry.meta(Registry.PutMetaTest, :custom_key)
{:ok, "custom_value"}
iex> Registry.put_meta(Registry.PutMetaTest, {:tuple, :key}, "tuple_value")
:ok
iex> Registry.meta(Registry.PutMetaTest, {:tuple, :key})
{:ok, "tuple_value"}

register(registry, key, value) (1.4.0부터)

@spec register(registry(), key(), value()) ::
  {:ok, pid()} | {:error, {:already_registered, pid()}}

registry에서 주어진 key 아래에 현재 프로세스를 등록합니다. 이 등록에 연관될 값도 주어져야 해요. 이 값은 디스패치하거나 키 조회할 때마다 가져와집니다.

이 함수는 {:ok, owner} 또는 {:error, reason}을 반환합니다. owner는 PID를 담당하는 레지스트리 파티션의 PID이며, 호출자에 자동으로 링크됩니다. 레지스트리가 고유 키를 가지면 {:ok, owner}를 반환하되, 키가 이미 PID에 연관되어 있다면 {:error, {:already_registered, pid}}를 반환합니다. 레지스트리가 중복 키를 가지면 현재 프로세스가 같은 키 아래에서 여러 번 등록하는 것이 허용됩니다.

start_link/1:listeners 옵션으로 리스너가 지정되면, 그 리스너들은 등록을 통보받고 listener_message/0 타입의 메시지를 받습니다.

iex> Registry.start_link(keys: :unique, name: Registry.UniqueRegisterTest)
iex> {:ok, _} = Registry.register(Registry.UniqueRegisterTest, "hello", :world)
iex> Registry.register(Registry.UniqueRegisterTest, "hello", :later)
{:error, {:already_registered, self()}}
iex> Registry.keys(Registry.UniqueRegisterTest, self())
["hello"]

중복 레지스트리에서는 그게 가능합니다.

iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateRegisterTest)
iex> {:ok, _} = Registry.register(Registry.DuplicateRegisterTest, "hello", :world)
iex> {:ok, _} = Registry.register(Registry.DuplicateRegisterTest, "hello", :world)
iex> Registry.keys(Registry.DuplicateRegisterTest, self())
["hello", "hello"]

select(registry, spec) (1.9.0부터)

@spec select(registry(), spec()) :: [term()]

전체 매치 스펙으로 등록된 키, PID, 값들을 선택합니다.

spec[{match_pattern, guards, body}] 형태의 세 부분 튜플 리스트로 구성됩니다. 첫 부분인 매치 패턴은 레지스트리에 저장된 데이터({key, pid, value})의 구조와 매치될 튜플이어야 해요. :_ 아톰은 주어진 값이나 튜플 요소를 무시하는 데, :"$1" 아톰은 패턴의 일부를 이후 비교를 위해 변수에 임시 할당하는 데 쓸 수 있습니다. 이는 {:"$1", :_, :_}처럼 결합할 수 있어요.

두 번째 부분인 가드는 결과를 필터링하는 조건들의 리스트입니다. 각 가드는 할당된 패턴 부분이 통과해야 하는 검사를 설명하는 튜플이에요. 예를 들어 $1 > 1 가드 조건은 {:>, :"$1", 1} 튜플로 표현됩니다. 가드 조건은 :"$1", :"$2" 같은 할당된 변수에서만 동작합니다.

세 번째 부분인 body는 반환되는 항목들의 모양 리스트입니다. 가드처럼 :"$1" 같은 할당된 변수에 접근할 수 있고, 하드코딩된 값과 결합해서 항목을 자유롭게 모양 짓을 수 있어요. 튜플은 추가 튜플로 감싸야 한다는 점에 유의하세요. %{key: key, pid: pid, value: value} 같은 결과 형식을 얻으려면(매치 부분에서 그 변수들을 순서대로 바인딩했다고 가정), [%{key: :"$1", pid: :"$2", value: :"$3"}] 같은 body를 제공하면 됩니다. 가드처럼 :element 같은 일부 연산으로 출력 형식을 수정할 수 있어요.

특수 매치 변수 :"$_":"$$"는 예상대로 동작하지 않을 수 있으니 쓰지 마세요. 특히 {:duplicate, :key} 레지스트리는 내부 ETS 배치가 다르므로, :"$_"로 기저 항목 구조를 참조하는 매치 스펙은 다른 결과를 반환합니다. 대신 :"$1", :"$2", :"$3" 같은 이름 있는 변수를 쓰세요.

파티션이 많은 큰 레지스트리에서는 모든 파티션을 이어 붙여 결과를 만들기 때문에 비용이 들 수 있다는 점에 유의하세요.

iex> Registry.start_link(keys: :unique, name: Registry.SelectAllTest)
iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "hello", :value)
iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "world", :value)
iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}]) |> Enum.sort()
[{"hello", self(), :value}, {"world", self(), :value}]

키만 얻고 싶다면 별도의 셀렉터를 넘길 수 있어요.

iex> Registry.start_link(keys: :unique, name: Registry.SelectKeysTest)
iex> {:ok, _} = Registry.register(Registry.SelectKeysTest, "hello", :value)
iex> {:ok, _} = Registry.register(Registry.SelectKeysTest, "world", :value)
iex> Registry.select(Registry.SelectKeysTest, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort()
["hello", "world"]

start_link(options) (1.5.0부터)

@spec start_link([start_option()]) :: {:ok, pid()} | {:error, term()}

레지스트리를 감독자 프로세스로 시작합니다. 수동으로는 이렇게 시작할 수 있어요.

Registry.start_link(keys: :unique, name: MyApp.Registry)

감독 트리에서는 이렇게 쓸 것입니다.

Supervisor.start_link([
  {Registry, keys: :unique, name: MyApp.Registry}
], strategy: :one_for_one)

집약적인 워크로드의 경우 레지스트리는(:partitions 옵션을 지정해서) 파티션될 수도 있어요. 파티션닝이 필요하다면 좋은 기본값은 파티션 수를 사용 가능한 스케줄러 수로 설정하는 것입니다.

Registry.start_link(
  keys: :unique,
  name: MyApp.Registry,
  partitions: System.schedulers_online()
)

또는:

Supervisor.start_link([
  {Registry, keys: :unique, name: MyApp.Registry, partitions: System.schedulers_online()}
], strategy: :one_for_one)

서로 다른 키가 많은 :duplicate 레지스트리(예: 구독자가 각각 적은 많은 토픽)의 경우 키별 파티셔닝으로 키 기반 조회를 최적화할 수 있어요.

Registry.start_link(
  keys: {:duplicate, :key},
  name: MyApp.TopicRegistry,
  partitions: System.schedulers_online()
)

이렇게 하면 키 기반 조회가 모든 파티션을 검색하는 대신 단일 파티션만 확인하게 됩니다. 항목당 키가 적고 항목이 많은 경우(예: 구독자가 많은 하나의 토픽)에는 기본 :pid 파티셔닝을 쓰세요.

옵션(Options)

레지스트리는 다음 키를 요구합니다.

  • :keys — 키가 :unique, :duplicate, {:duplicate, :key} 또는 {:duplicate, :pid} 중 무엇인지 선택
  • :name — 레지스트리와 그 테이블들의 이름

다음 키는 선택적입니다.

  • :partitions — 레지스트리의 파티션 수. 기본값 1.
  • :listeners — register·unregister 이벤트를 통보받는 이름 있는 프로세스 리스트. 등록된 프로세스는 리스너가 등록된 프로세스가 죽을 때 통보받기를 원하면 리스너가 모니터링해야 합니다. 리스너에게 보내는 메시지는 listener_message/0 타입입니다.
  • :meta — 레지스트리에 첨부할 메타데이터 키워드 리스트.

:duplicate 레지스트리의 경우 파티셔닝 전략을 :keys 옵션에 직접 지정할 수 있어요.

  • :duplicate 또는 {:duplicate, :pid} — 항목이 많은 키(예: 구독자가 많은 하나의 토픽)일 때 :pid 파티셔닝(기본값) 사용. 이것은 전통적인 동작이며 같은 프로세스의 모든 항목을 함께 그룹화합니다.
  • {:duplicate, :key} — 항목이 서로 다른 많은 키에 퍼져 있을 때(예: 각각 구독자가 적은 많은 토픽) :key 파티셔닝 사용. 이렇게 하면 키 기반 조회가 모든 파티션 대신 단일 파티션만 확인하면 되므로 더 효율적입니다. 이 옵션은 서로 다른 내부 ETS 테이블 타입(duplicate_bag 대신 ordered_set)을 사용하므로, select/2count_select/2에 넘겨지는 :"$_"를 참조하는 매치 스펙이 다르게 동작할 수 있습니다. 대신 :"$1", :"$2" 같은 이름 있는 매치 변수를 쓰세요.

unregister(registry, key) (1.4.0부터)

@spec unregister(registry(), key()) :: :ok

registry에서 현재 프로세스와 연관된 주어진 key의 모든 항목을 등록 해제합니다. 항상 :ok을 반환하며, 현재 프로세스에 더 이상 연관된 키가 없으면 현재 프로세스를 owner에서 자동으로 unlink합니다. "owner"에 대한 자세한 내용은 register/3을 참고하세요. start_link/1:listeners 옵션으로 리스너가 지정되면, 그 리스너들은 등록 해제를 통보받고 listener_message/0 타입의 메시지를 받습니다.

iex> Registry.start_link(keys: :unique, name: Registry.UniqueUnregisterTest)
iex> Registry.register(Registry.UniqueUnregisterTest, "hello", :world)
iex> Registry.keys(Registry.UniqueUnregisterTest, self())
["hello"]
iex> Registry.unregister(Registry.UniqueUnregisterTest, "hello")
:ok
iex> Registry.keys(Registry.UniqueUnregisterTest, self())
[]

중복 레지스트리의 경우:

iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateUnregisterTest)
iex> Registry.register(Registry.DuplicateUnregisterTest, "hello", :world)
iex> Registry.register(Registry.DuplicateUnregisterTest, "hello", :world)
iex> Registry.keys(Registry.DuplicateUnregisterTest, self())
["hello", "hello"]
iex> Registry.unregister(Registry.DuplicateUnregisterTest, "hello")
:ok
iex> Registry.keys(Registry.DuplicateUnregisterTest, self())
[]

unregister_match(registry, key, pattern, guards \ []) (1.5.0부터)

@spec unregister_match(registry(), key(), match_pattern(), guards()) :: :ok

registry에서 현재 프로세스와 연관된 키 중 pattern과 매치되는 키의 항목을 등록 해제합니다.

고유 레지스트리의 경우 특정 값과 매치되는지 여부를 기준으로 키를 조건부로 등록 해제하는 데 쓸 수 있어요.

iex> Registry.start_link(keys: :unique, name: Registry.UniqueUnregisterMatchTest)
iex> Registry.register(Registry.UniqueUnregisterMatchTest, "hello", :world)
iex> Registry.keys(Registry.UniqueUnregisterMatchTest, self())
["hello"]
iex> Registry.unregister_match(Registry.UniqueUnregisterMatchTest, "hello", :foo)
:ok
iex> Registry.keys(Registry.UniqueUnregisterMatchTest, self())
["hello"]
iex> Registry.unregister_match(Registry.UniqueUnregisterMatchTest, "hello", :world)
:ok
iex> Registry.keys(Registry.UniqueUnregisterMatchTest, self())
[]

중복 레지스트리의 경우:

iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateUnregisterMatchTest)
iex> Registry.register(Registry.DuplicateUnregisterMatchTest, "hello", :world_a)
iex> Registry.register(Registry.DuplicateUnregisterMatchTest, "hello", :world_b)
iex> Registry.register(Registry.DuplicateUnregisterMatchTest, "hello", :world_c)
iex> Registry.keys(Registry.DuplicateUnregisterMatchTest, self())
["hello", "hello", "hello"]
iex> Registry.unregister_match(Registry.DuplicateUnregisterMatchTest, "hello", :world_a)
:ok
iex> Registry.keys(Registry.DuplicateUnregisterMatchTest, self())
["hello", "hello"]
iex> Registry.lookup(Registry.DuplicateUnregisterMatchTest, "hello")
[{self(), :world_b}, {self(), :world_c}]

update_value(registry, key, callback) (1.4.0부터)

@spec update_value(registry(), key(), (value() -> value())) ::
  {new_value :: term(), old_value :: term()} | :error

고유 registry에서 현재 프로세스의 key에 대한 값을 갱신합니다. {new_value, old_value} 튜플을 반환하거나, 현재 프로세스에 할당된 그런 키가 없으면 :error를 반환합니다. 고유하지 않은 레지스트리가 주어지면 오류가 발생합니다.

iex> Registry.start_link(keys: :unique, name: Registry.UpdateTest)
iex> {:ok, _} = Registry.register(Registry.UpdateTest, "hello", 1)
iex> Registry.lookup(Registry.UpdateTest, "hello")
[{self(), 1}]
iex> Registry.update_value(Registry.UpdateTest, "hello", &(&1 + 1))
{2, 1}
iex> Registry.lookup(Registry.UpdateTest, "hello")
[{self(), 2}]

values(registry, key, pid) (1.12.0부터)

@spec values(registry(), key(), pid()) :: [value()]

registry에서 pid의 주어진 key에 대한 값들을 읽습니다. 고유 레지스트리에서는 빈 리스트이거나 단일 요소 리스트입니다. 중복 레지스트리에서는 0, 1 또는 여러 요소의 리스트입니다.

iex> Registry.start_link(keys: :unique, name: Registry.UniqueValuesTest)
iex> Registry.values(Registry.UniqueValuesTest, "hello", self())
[]
iex> {:ok, _} = Registry.register(Registry.UniqueValuesTest, "hello", :world)
iex> Registry.values(Registry.UniqueValuesTest, "hello", self())
[:world]
iex> Task.async(fn -> Registry.values(Registry.UniqueValuesTest, "hello", self()) end) |> Task.await()
[]
iex> parent = self()
iex> Task.async(fn -> Registry.values(Registry.UniqueValuesTest, "hello", parent) end) |> Task.await()
[:world]

중복 레지스트리에도 동일하게 적용됩니다.

iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateValuesTest)
iex> Registry.values(Registry.DuplicateValuesTest, "hello", self())
[]
iex> {:ok, _} = Registry.register(Registry.DuplicateValuesTest, "hello", :world)
iex> Registry.values(Registry.DuplicateValuesTest, "hello", self())
[:world]
iex> {:ok, _} = Registry.register(Registry.DuplicateValuesTest, "hello", :another)
iex> Enum.sort(Registry.values(Registry.DuplicateValuesTest, "hello", self()))
[:another, :world]

더 알아보기

  • GenServer{:via, ...} 이름으로 Registry를 쓰는 서버
  • Agent{:via, ...} 이름으로 Registry를 쓰는 에이전트
  • Supervisor — 레지스트리를 감독 트리에 넣기
  • :ets — 레지스트리가 내부에서 쓰는 ETS 테이블 모듈