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/1로 Registry를 시작해 왔는데, 보통 레지스트리는 감독 트리의 일부로 시작됩니다.
{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/1과start_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을 쓰는 예시는 모듈 문서를 참고하세요.
옵션:
:parallel—true이면 모든 파티션에 걸쳐 병렬로 디스패치.{: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_key를 function이 실행되는 동안 대역 밖(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/2와count_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 테이블 모듈