update
This commit is contained in:
BIN
phoenix/deps/phoenix_pubsub/.hex
Normal file
BIN
phoenix/deps/phoenix_pubsub/.hex
Normal file
Binary file not shown.
45
phoenix/deps/phoenix_pubsub/CHANGELOG.md
Normal file
45
phoenix/deps/phoenix_pubsub/CHANGELOG.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Changelog
|
||||
|
||||
This new version of Phoenix.PubSub provides a simpler, more extensible, and more performant Phoenix.PubSub API. For users of Phoenix.PubSub, the API is the same, although frameworks and other adapters will have to migrate accordingly (which often means less code).
|
||||
|
||||
## 2.2.0 (2025-10-22)
|
||||
|
||||
### Enhancements
|
||||
- Allow the registry size to be set separate from pool size
|
||||
- Introduce `:broadcast_pool_size` option to allow safe pool size migration
|
||||
|
||||
### Bug fixes
|
||||
- Only restart shards if they terminate unexpectedly
|
||||
|
||||
## 2.1.4 (2024-09-27)
|
||||
|
||||
### Enhancements
|
||||
- Add `:permdown_on_shutdown` option
|
||||
|
||||
## 2.1.3 (2023-06-14)
|
||||
|
||||
### Bug fixes
|
||||
- Fix memory leak introduced in 2.1.2
|
||||
|
||||
## 2.1.2 (2023-05-24)
|
||||
|
||||
### Bug fixes
|
||||
- Fix race condition on tracker update allowing state to become out of sync
|
||||
|
||||
## 2.1.1 (2022-04-05)
|
||||
|
||||
### Enhancements
|
||||
- Support compatibility with 2.0 nodes when pool_size is 1
|
||||
|
||||
## 2.1.0 (2022-04-01)
|
||||
|
||||
### Enhancements
|
||||
- Support `handle_info` callback on `Phoenix.Tracker`
|
||||
|
||||
## 2.0.0 (2020-04-14)
|
||||
|
||||
### Enhancements
|
||||
- Use erlang's new `:pg` module if available instead of `:pg2`
|
||||
|
||||
### Backwards incompatible changes
|
||||
- Frameworks and other adapters will require the use of the new child_spec API
|
||||
22
phoenix/deps/phoenix_pubsub/LICENSE.md
Normal file
22
phoenix/deps/phoenix_pubsub/LICENSE.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# MIT License
|
||||
|
||||
Copyright (c) 2014 Chris McCord
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
56
phoenix/deps/phoenix_pubsub/README.md
Normal file
56
phoenix/deps/phoenix_pubsub/README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Phoenix.PubSub
|
||||
|
||||
> Distributed PubSub and Presence platform for the Phoenix Framework
|
||||
|
||||
[](https://github.com/phoenixframework/phoenix_pubsub/actions/workflows/ci.yml)
|
||||
|
||||
## Usage
|
||||
|
||||
Add `phoenix_pubsub` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[{:phoenix_pubsub, "~> 2.0"}]
|
||||
end
|
||||
```
|
||||
|
||||
Then start your PubSub instance:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp do
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
{Phoenix.PubSub, name: MyApp.PubSub}
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Now broadcast and subscribe:
|
||||
|
||||
```elixir
|
||||
Phoenix.PubSub.subscribe(MyApp.PubSub, "user:123")
|
||||
Phoenix.PubSub.broadcast(MyApp.PubSub, "user:123", :hello_world)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Testing by default spawns nodes internally for distributed tests.
|
||||
To run tests that do not require clustering, exclude the `clustered` tag:
|
||||
|
||||
```shell
|
||||
$ mix test --exclude clustered
|
||||
```
|
||||
|
||||
If you have issues running the clustered tests try running:
|
||||
|
||||
```shell
|
||||
$ epmd -daemon
|
||||
```
|
||||
|
||||
before running the tests.
|
||||
22
phoenix/deps/phoenix_pubsub/hex_metadata.config
Normal file
22
phoenix/deps/phoenix_pubsub/hex_metadata.config
Normal file
@@ -0,0 +1,22 @@
|
||||
{<<"links">>,
|
||||
[{<<"github">>,<<"https://github.com/phoenixframework/phoenix_pubsub">>}]}.
|
||||
{<<"name">>,<<"phoenix_pubsub">>}.
|
||||
{<<"version">>,<<"2.2.0">>}.
|
||||
{<<"description">>,<<"Distributed PubSub and Presence platform">>}.
|
||||
{<<"elixir">>,<<"~> 1.6">>}.
|
||||
{<<"files">>,
|
||||
[<<"lib">>,<<"lib/phoenix">>,<<"lib/phoenix/tracker">>,
|
||||
<<"lib/phoenix/tracker/clock.ex">>,<<"lib/phoenix/tracker/replica.ex">>,
|
||||
<<"lib/phoenix/tracker/shutdown_handler.ex">>,
|
||||
<<"lib/phoenix/tracker/delta_generation.ex">>,
|
||||
<<"lib/phoenix/tracker/state.ex">>,<<"lib/phoenix/tracker/shard.ex">>,
|
||||
<<"lib/phoenix/pubsub">>,<<"lib/phoenix/pubsub/adapter.ex">>,
|
||||
<<"lib/phoenix/pubsub/supervisor.ex">>,
|
||||
<<"lib/phoenix/pubsub/application.ex">>,<<"lib/phoenix/pubsub/pg2.ex">>,
|
||||
<<"lib/phoenix/tracker.ex">>,<<"lib/phoenix/pubsub.ex">>,<<"test/shared">>,
|
||||
<<"test/shared/pubsub_test.exs">>,<<"CHANGELOG.md">>,<<"LICENSE.md">>,
|
||||
<<"mix.exs">>,<<"README.md">>]}.
|
||||
{<<"app">>,<<"phoenix_pubsub">>}.
|
||||
{<<"licenses">>,[<<"MIT">>]}.
|
||||
{<<"requirements">>,[]}.
|
||||
{<<"build_tools">>,[<<"mix">>]}.
|
||||
385
phoenix/deps/phoenix_pubsub/lib/phoenix/pubsub.ex
Normal file
385
phoenix/deps/phoenix_pubsub/lib/phoenix/pubsub.ex
Normal file
@@ -0,0 +1,385 @@
|
||||
defmodule Phoenix.PubSub do
|
||||
@moduledoc """
|
||||
Realtime Publisher/Subscriber service.
|
||||
|
||||
## Getting started
|
||||
|
||||
You start Phoenix.PubSub directly in your supervision
|
||||
tree:
|
||||
|
||||
{Phoenix.PubSub, name: :my_pubsub}
|
||||
|
||||
You can now use the functions in this module to subscribe
|
||||
and broadcast messages:
|
||||
|
||||
iex> alias Phoenix.PubSub
|
||||
iex> PubSub.subscribe(:my_pubsub, "user:123")
|
||||
:ok
|
||||
iex> Process.info(self(), :messages)
|
||||
{:messages, []}
|
||||
iex> PubSub.broadcast(:my_pubsub, "user:123", {:user_update, %{id: 123, name: "Shane"}})
|
||||
:ok
|
||||
iex> Process.info(self(), :messages)
|
||||
{:messages, [{:user_update, %{id: 123, name: "Shane"}}]}
|
||||
|
||||
## Adapters
|
||||
|
||||
Phoenix PubSub was designed to be flexible and support
|
||||
multiple backends. There are two officially supported
|
||||
backends:
|
||||
|
||||
* `Phoenix.PubSub.PG2` - the default adapter that ships
|
||||
as part of Phoenix.PubSub. It uses Distributed Elixir,
|
||||
directly exchanging notifications between servers.
|
||||
It supports a `:pool_size` option to be given alongside
|
||||
the name, defaults to `1`. Note the `:pool_size` must
|
||||
be the same throughout the cluster, therefore don't
|
||||
configure the pool size based on `System.schedulers_online/0`,
|
||||
especially if you are using machines with different specs.
|
||||
|
||||
* `Phoenix.PubSub.Redis` - uses Redis to exchange data between
|
||||
servers. It requires the `:phoenix_pubsub_redis` dependency.
|
||||
|
||||
See `Phoenix.PubSub.Adapter` to implement a custom adapter.
|
||||
|
||||
## Custom dispatching
|
||||
|
||||
Phoenix.PubSub allows developers to perform custom dispatching
|
||||
by passing a `dispatcher` module which is responsible for local
|
||||
message deliveries.
|
||||
|
||||
The dispatcher must be available on all nodes running the PubSub
|
||||
system. The `dispatch/3` function of the given module will be
|
||||
invoked with the subscriptions entries, the broadcaster identifier
|
||||
(either a pid or `:none`), and the message to broadcast.
|
||||
|
||||
You may want to use the dispatcher to perform special delivery for
|
||||
certain subscriptions. This can be done by passing the :metadata
|
||||
option during subscriptions. For instance, Phoenix Channels use a
|
||||
custom `value` to provide "fastlaning", allowing messages broadcast
|
||||
to thousands or even millions of users to be encoded once and written
|
||||
directly to sockets instead of being encoded per channel.
|
||||
|
||||
## Safe pool size migration (when using `Phoenix.PubSub.PG2` adapter)
|
||||
|
||||
When you need to change the pool size in a running cluster,
|
||||
you can use the `broadcast_pool_size` option to ensure no
|
||||
messages are lost during deployment. This is particularly
|
||||
important when increasing the pool size.
|
||||
|
||||
Here's how to safely increase the pool size from 1 to 2:
|
||||
|
||||
1. Initial state - Current configuration with `pool_size: 1`:
|
||||
```
|
||||
{Phoenix.PubSub, name: :my_pubsub, pool_size: 1}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Initial State"
|
||||
subgraph "Node 1"
|
||||
A1[Shard 1<br/>Broadcast & Receive]
|
||||
end
|
||||
subgraph "Node 2"
|
||||
B1[Shard 1<br/>Broadcast & Receive]
|
||||
end
|
||||
A1 <--> B1
|
||||
end
|
||||
```
|
||||
|
||||
2. First deployment - Set the new pool size but keep broadcasting on the old size:
|
||||
```
|
||||
{Phoenix.PubSub, name: :my_pubsub, pool_size: 2, broadcast_pool_size: 1}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "First Deployment"
|
||||
subgraph "Node 1"
|
||||
A1[Shard 1<br/>Broadcast & Receive]
|
||||
A2[Shard 2<br/>Broadcast & Receive]
|
||||
end
|
||||
subgraph "Node 2"
|
||||
B1[Shard 1<br/>Broadcast & Receive]
|
||||
B2[Shard 2<br/>Receive Only]
|
||||
end
|
||||
A1 <--> B1
|
||||
A2 --> B2
|
||||
end
|
||||
```
|
||||
|
||||
3. Final deployment - All nodes running with new pool size:
|
||||
```
|
||||
{Phoenix.PubSub, name: :my_pubsub, pool_size: 2}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Final State"
|
||||
subgraph "Node 1"
|
||||
A1[Shard 1<br/>Broadcast & Receive]
|
||||
A2[Shard 2<br/>Broadcast & Receive]
|
||||
end
|
||||
subgraph "Node 2"
|
||||
B1[Shard 1<br/>Broadcast & Receive]
|
||||
B2[Shard 2<br/>Broadcast & Receive]
|
||||
end
|
||||
A1 <--> B1
|
||||
A2 <--> B2
|
||||
end
|
||||
```
|
||||
|
||||
This two-step process ensures that:
|
||||
- All nodes can receive messages from both old and new pool sizes
|
||||
- No messages are lost during the transition
|
||||
- The cluster remains fully functional throughout the deployment
|
||||
|
||||
To decrease the pool size, follow the same process in reverse order.
|
||||
|
||||
"""
|
||||
|
||||
@type node_name :: atom | binary
|
||||
@type t :: atom
|
||||
@type topic :: binary
|
||||
@type message :: term
|
||||
@type dispatcher :: module
|
||||
|
||||
defmodule BroadcastError do
|
||||
defexception [:message]
|
||||
|
||||
def exception(msg) do
|
||||
%BroadcastError{message: "broadcast failed with #{inspect(msg)}"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a child specification for pubsub with the given `options`.
|
||||
|
||||
The `:name` is required as part of `options`. The remaining options
|
||||
are described below.
|
||||
|
||||
## Options
|
||||
|
||||
* `:name` - the name of the pubsub to be started
|
||||
* `:adapter` - the adapter to use (defaults to `Phoenix.PubSub.PG2`)
|
||||
* `:pool_size` - number of pubsub partitions to launch
|
||||
(defaults to one partition for every 4 cores)
|
||||
* `:registry_size` - number of `Registry` partitions to launch
|
||||
(defaults to `:pool_size`). This controls the number of Registry partitions
|
||||
used for storing subscriptions and can be tuned independently from `:pool_size`
|
||||
for better performance characteristics.
|
||||
* `:broadcast_pool_size` - number of pubsub partitions used for broadcasting messages
|
||||
(defaults to `:pool_size`). This option is used during pool size migrations to ensure
|
||||
no messages are lost. See the "Safe Pool Size Migration" section in the module documentation.
|
||||
|
||||
"""
|
||||
@spec child_spec(keyword) :: Supervisor.child_spec()
|
||||
defdelegate child_spec(options), to: Phoenix.PubSub.Supervisor
|
||||
|
||||
@doc """
|
||||
Subscribes the caller to the PubSub adapter's topic.
|
||||
|
||||
* `pubsub` - The name of the pubsub system
|
||||
* `topic` - The topic to subscribe to, for example: `"users:123"`
|
||||
* `opts` - The optional list of options. See below.
|
||||
|
||||
## Duplicate Subscriptions
|
||||
|
||||
Callers should only subscribe to a given topic a single time.
|
||||
Duplicate subscriptions for a Pid/topic pair are allowed and
|
||||
will cause duplicate events to be sent; however, when using
|
||||
`Phoenix.PubSub.unsubscribe/2`, all duplicate subscriptions
|
||||
will be dropped.
|
||||
|
||||
## Options
|
||||
|
||||
* `:metadata` - provides metadata to be attached to this
|
||||
subscription. The metadata can be used by custom
|
||||
dispatching mechanisms. See the "Custom dispatching"
|
||||
section in the module documentation
|
||||
|
||||
"""
|
||||
@spec subscribe(t, topic, keyword) :: :ok | {:error, term}
|
||||
def subscribe(pubsub, topic, opts \\ [])
|
||||
when is_atom(pubsub) and is_binary(topic) and is_list(opts) do
|
||||
case Registry.register(pubsub, topic, opts[:metadata]) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, _} = error -> error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unsubscribes the caller from the PubSub adapter's topic.
|
||||
"""
|
||||
@spec unsubscribe(t, topic) :: :ok
|
||||
def unsubscribe(pubsub, topic) when is_atom(pubsub) and is_binary(topic) do
|
||||
Registry.unregister(pubsub, topic)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts message on given topic across the whole cluster.
|
||||
|
||||
* `pubsub` - The name of the pubsub system
|
||||
* `topic` - The topic to broadcast to, ie: `"users:123"`
|
||||
* `message` - The payload of the broadcast
|
||||
|
||||
A custom dispatcher may also be given as a fourth, optional argument.
|
||||
See the "Custom dispatching" section in the module documentation.
|
||||
"""
|
||||
@spec broadcast(t, topic, message, dispatcher) :: :ok | {:error, term}
|
||||
def broadcast(pubsub, topic, message, dispatcher \\ __MODULE__)
|
||||
when is_atom(pubsub) and is_binary(topic) and is_atom(dispatcher) do
|
||||
{:ok, {adapter, name}} = Registry.meta(pubsub, :pubsub)
|
||||
|
||||
with :ok <- adapter.broadcast(name, topic, message, dispatcher) do
|
||||
dispatch(pubsub, :none, topic, message, dispatcher)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts message on given topic from the given process across the whole cluster.
|
||||
|
||||
* `pubsub` - The name of the pubsub system
|
||||
* `from` - The pid that will send the message
|
||||
* `topic` - The topic to broadcast to, ie: `"users:123"`
|
||||
* `message` - The payload of the broadcast
|
||||
|
||||
The default dispatcher will broadcast the message to all subscribers except for the
|
||||
process that initiated the broadcast.
|
||||
|
||||
A custom dispatcher may also be given as a fifth, optional argument.
|
||||
See the "Custom dispatching" section in the module documentation.
|
||||
"""
|
||||
@spec broadcast_from(t, pid, topic, message, dispatcher) :: :ok | {:error, term}
|
||||
def broadcast_from(pubsub, from, topic, message, dispatcher \\ __MODULE__)
|
||||
when is_atom(pubsub) and is_pid(from) and is_binary(topic) and is_atom(dispatcher) do
|
||||
{:ok, {adapter, name}} = Registry.meta(pubsub, :pubsub)
|
||||
|
||||
with :ok <- adapter.broadcast(name, topic, message, dispatcher) do
|
||||
dispatch(pubsub, from, topic, message, dispatcher)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts message on given topic only for the current node.
|
||||
|
||||
* `pubsub` - The name of the pubsub system
|
||||
* `topic` - The topic to broadcast to, ie: `"users:123"`
|
||||
* `message` - The payload of the broadcast
|
||||
|
||||
A custom dispatcher may also be given as a fourth, optional argument.
|
||||
See the "Custom dispatching" section in the module documentation.
|
||||
"""
|
||||
@spec local_broadcast(t, topic, message, dispatcher) :: :ok
|
||||
def local_broadcast(pubsub, topic, message, dispatcher \\ __MODULE__)
|
||||
when is_atom(pubsub) and is_binary(topic) and is_atom(dispatcher) do
|
||||
dispatch(pubsub, :none, topic, message, dispatcher)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts message on given topic from a given process only for the current node.
|
||||
|
||||
* `pubsub` - The name of the pubsub system
|
||||
* `from` - The pid that will send the message
|
||||
* `topic` - The topic to broadcast to, ie: `"users:123"`
|
||||
* `message` - The payload of the broadcast
|
||||
|
||||
The default dispatcher will broadcast the message to all subscribers except for the
|
||||
process that initiated the broadcast.
|
||||
|
||||
A custom dispatcher may also be given as a fifth, optional argument.
|
||||
See the "Custom dispatching" section in the module documentation.
|
||||
"""
|
||||
@spec local_broadcast_from(t, pid, topic, message, dispatcher) :: :ok
|
||||
def local_broadcast_from(pubsub, from, topic, message, dispatcher \\ __MODULE__)
|
||||
when is_atom(pubsub) and is_pid(from) and is_binary(topic) and is_atom(dispatcher) do
|
||||
dispatch(pubsub, from, topic, message, dispatcher)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts message on given topic to a given node.
|
||||
|
||||
* `node_name` - The target node name
|
||||
* `pubsub` - The name of the pubsub system
|
||||
* `topic` - The topic to broadcast to, ie: `"users:123"`
|
||||
* `message` - The payload of the broadcast
|
||||
|
||||
**DO NOT** use this function if you wish to broadcast to the current
|
||||
node, as it is always serialized, use `local_broadcast/4` instead.
|
||||
|
||||
A custom dispatcher may also be given as a fifth, optional argument.
|
||||
See the "Custom dispatching" section in the module documentation.
|
||||
"""
|
||||
@spec direct_broadcast(node_name, t, topic, message, dispatcher) :: :ok | {:error, term}
|
||||
def direct_broadcast(node_name, pubsub, topic, message, dispatcher \\ __MODULE__)
|
||||
when is_atom(pubsub) and is_binary(topic) and is_atom(dispatcher) do
|
||||
{:ok, {adapter, name}} = Registry.meta(pubsub, :pubsub)
|
||||
adapter.direct_broadcast(name, node_name, topic, message, dispatcher)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Raising version of `broadcast/4`.
|
||||
"""
|
||||
@spec broadcast!(t, topic, message, dispatcher) :: :ok
|
||||
def broadcast!(pubsub, topic, message, dispatcher \\ __MODULE__) do
|
||||
case broadcast(pubsub, topic, message, dispatcher) do
|
||||
:ok -> :ok
|
||||
{:error, error} -> raise BroadcastError, "broadcast failed: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Raising version of `broadcast_from/5`.
|
||||
"""
|
||||
@spec broadcast_from!(t, pid, topic, message, dispatcher) :: :ok
|
||||
def broadcast_from!(pubsub, from, topic, message, dispatcher \\ __MODULE__) do
|
||||
case broadcast_from(pubsub, from, topic, message, dispatcher) do
|
||||
:ok -> :ok
|
||||
{:error, error} -> raise BroadcastError, "broadcast failed: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Raising version of `direct_broadcast/5`.
|
||||
"""
|
||||
@spec direct_broadcast!(node_name, t, topic, message, dispatcher) :: :ok
|
||||
def direct_broadcast!(node_name, pubsub, topic, message, dispatcher \\ __MODULE__) do
|
||||
case direct_broadcast(node_name, pubsub, topic, message, dispatcher) do
|
||||
:ok -> :ok
|
||||
{:error, error} -> raise BroadcastError, "broadcast failed: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the node name of the PubSub server.
|
||||
"""
|
||||
@spec node_name(t) :: node_name
|
||||
def node_name(pubsub) do
|
||||
{:ok, {adapter, name}} = Registry.meta(pubsub, :pubsub)
|
||||
adapter.node_name(name)
|
||||
end
|
||||
|
||||
## Dispatch callback
|
||||
|
||||
@doc false
|
||||
def dispatch(entries, :none, message) do
|
||||
for {pid, _} <- entries do
|
||||
send(pid, message)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def dispatch(entries, from, message) do
|
||||
for {pid, _} <- entries, pid != from do
|
||||
send(pid, message)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp dispatch(pubsub, from, topic, message, dispatcher) do
|
||||
Registry.dispatch(pubsub, topic, {dispatcher, :dispatch, [from, message]})
|
||||
:ok
|
||||
end
|
||||
end
|
||||
46
phoenix/deps/phoenix_pubsub/lib/phoenix/pubsub/adapter.ex
Normal file
46
phoenix/deps/phoenix_pubsub/lib/phoenix/pubsub/adapter.ex
Normal file
@@ -0,0 +1,46 @@
|
||||
defmodule Phoenix.PubSub.Adapter do
|
||||
@moduledoc """
|
||||
Specification to implement a custom PubSub adapter.
|
||||
"""
|
||||
|
||||
@type adapter_name :: atom
|
||||
|
||||
@doc """
|
||||
Returns the node name as an atom or a binary.
|
||||
"""
|
||||
@callback node_name(adapter_name) :: Phoenix.PubSub.node_name()
|
||||
|
||||
@doc """
|
||||
Returns a child specification that mounts the processes
|
||||
required for the adapter.
|
||||
|
||||
`child_spec` will receive all options given `Phoenix.PubSub`.
|
||||
Note, however, that the `:name` under options is the name
|
||||
of the complete PubSub system. The reserved key space to
|
||||
be used by the adapter is under the `:adapter_name` key.
|
||||
"""
|
||||
@callback child_spec(keyword) :: Supervisor.child_spec()
|
||||
|
||||
@doc """
|
||||
Broadcasts the given topic, message, and dispatcher to
|
||||
all nodes in the cluster (except the current node itself).
|
||||
"""
|
||||
@callback broadcast(
|
||||
adapter_name,
|
||||
topic :: Phoenix.PubSub.topic(),
|
||||
message :: Phoenix.PubSub.message(),
|
||||
dispatcher :: Phoenix.PubSub.dispatcher()
|
||||
) :: :ok | {:error, term}
|
||||
|
||||
@doc """
|
||||
Broadcasts the given topic, message, and dispatcher to
|
||||
given node in the cluster (it may point to itself).
|
||||
"""
|
||||
@callback direct_broadcast(
|
||||
adapter_name,
|
||||
node_name :: Phoenix.PubSub.node_name(),
|
||||
topic :: Phoenix.PubSub.topic(),
|
||||
message :: Phoenix.PubSub.message(),
|
||||
dispatcher :: Phoenix.PubSub.dispatcher()
|
||||
) :: :ok | {:error, term}
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
defmodule Phoenix.PubSub.Application do
|
||||
@moduledoc false
|
||||
use Application
|
||||
|
||||
def start(_, _) do
|
||||
children = pg_children()
|
||||
Supervisor.start_link(children, strategy: :one_for_one)
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(:pg) do
|
||||
defp pg_children() do
|
||||
[%{id: :pg, start: {:pg, :start_link, [Phoenix.PubSub]}}]
|
||||
end
|
||||
else
|
||||
defp pg_children() do
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
140
phoenix/deps/phoenix_pubsub/lib/phoenix/pubsub/pg2.ex
Normal file
140
phoenix/deps/phoenix_pubsub/lib/phoenix/pubsub/pg2.ex
Normal file
@@ -0,0 +1,140 @@
|
||||
defmodule Phoenix.PubSub.PG2 do
|
||||
@moduledoc """
|
||||
Phoenix PubSub adapter based on `:pg`/`:pg2`.
|
||||
|
||||
It runs on Distributed Erlang and is the default adapter.
|
||||
"""
|
||||
|
||||
@behaviour Phoenix.PubSub.Adapter
|
||||
use Supervisor
|
||||
|
||||
## Adapter callbacks
|
||||
|
||||
@impl true
|
||||
def node_name(_), do: node()
|
||||
|
||||
@impl true
|
||||
def broadcast(adapter_name, topic, message, dispatcher) do
|
||||
case pg_members(group(adapter_name)) do
|
||||
{:error, {:no_such_group, _}} ->
|
||||
{:error, :no_such_group}
|
||||
|
||||
pids ->
|
||||
message = forward_to_local(topic, message, dispatcher)
|
||||
|
||||
for pid <- pids, node(pid) != node() do
|
||||
send(pid, message)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def direct_broadcast(adapter_name, node_name, topic, message, dispatcher) do
|
||||
send({group(adapter_name), node_name}, {:forward_to_local, topic, message, dispatcher})
|
||||
:ok
|
||||
end
|
||||
|
||||
defp forward_to_local(topic, message, dispatcher) do
|
||||
{:forward_to_local, topic, message, dispatcher}
|
||||
end
|
||||
|
||||
defp group(adapter_name) do
|
||||
groups = :persistent_term.get(adapter_name)
|
||||
elem(groups, :erlang.phash2(self(), tuple_size(groups)))
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(:pg) do
|
||||
defp pg_members(group) do
|
||||
:pg.get_members(Phoenix.PubSub, group)
|
||||
end
|
||||
else
|
||||
defp pg_members(group) do
|
||||
:pg2.get_members({:phx, group})
|
||||
end
|
||||
end
|
||||
|
||||
## Supervisor callbacks
|
||||
|
||||
@doc false
|
||||
def start_link(opts) do
|
||||
name = Keyword.fetch!(opts, :name)
|
||||
pool_size = Keyword.get(opts, :pool_size, 1)
|
||||
broadcast_pool_size = Keyword.get(opts, :broadcast_pool_size, pool_size)
|
||||
|
||||
if pool_size < broadcast_pool_size do
|
||||
{:error, "the :pool_size option must be greater than or equal to the :broadcast_pool_size option"}
|
||||
else
|
||||
adapter_name = Keyword.fetch!(opts, :adapter_name)
|
||||
Supervisor.start_link(__MODULE__, {name, adapter_name, pool_size, broadcast_pool_size}, name: :"#{adapter_name}_supervisor")
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init({name, adapter_name, pool_size, broadcast_pool_size}) do
|
||||
|
||||
listener_groups = groups(adapter_name, pool_size)
|
||||
broadcast_groups = groups(adapter_name, broadcast_pool_size)
|
||||
|
||||
:persistent_term.put(adapter_name, List.to_tuple(broadcast_groups))
|
||||
|
||||
children =
|
||||
for group <- listener_groups do
|
||||
Supervisor.child_spec({Phoenix.PubSub.PG2Worker, {name, group}}, id: group)
|
||||
end
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
|
||||
defp groups(adapter_name, pool_size) do
|
||||
[_ | groups] =
|
||||
for number <- 1..pool_size do
|
||||
:"#{adapter_name}_#{number}"
|
||||
end
|
||||
|
||||
# Use `adapter_name` for the first in the pool for backwards compatibility
|
||||
# with v2.0 when the pool_size is 1.
|
||||
[adapter_name | groups]
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Phoenix.PubSub.PG2Worker do
|
||||
@moduledoc false
|
||||
use GenServer
|
||||
|
||||
@doc false
|
||||
def start_link({name, group}) do
|
||||
GenServer.start_link(__MODULE__, {name, group}, name: group)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init({name, group}) do
|
||||
:ok = pg_join(group)
|
||||
{:ok, name}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:forward_to_local, topic, message, dispatcher}, pubsub) do
|
||||
Phoenix.PubSub.local_broadcast(pubsub, topic, message, dispatcher)
|
||||
{:noreply, pubsub}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(_, pubsub) do
|
||||
{:noreply, pubsub}
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(:pg) do
|
||||
defp pg_join(group) do
|
||||
:ok = :pg.join(Phoenix.PubSub, group, self())
|
||||
end
|
||||
else
|
||||
defp pg_join(group) do
|
||||
namespace = {:phx, group}
|
||||
:ok = :pg2.create(namespace)
|
||||
:ok = :pg2.join(namespace, self())
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
42
phoenix/deps/phoenix_pubsub/lib/phoenix/pubsub/supervisor.ex
Normal file
42
phoenix/deps/phoenix_pubsub/lib/phoenix/pubsub/supervisor.ex
Normal file
@@ -0,0 +1,42 @@
|
||||
defmodule Phoenix.PubSub.Supervisor do
|
||||
@moduledoc false
|
||||
use Supervisor
|
||||
|
||||
def start_link(opts) do
|
||||
unless Code.ensure_loaded?(:persistent_term) do
|
||||
raise "Phoenix.PubSub v2.1 requires at least Erlang/OTP 21.3"
|
||||
end
|
||||
|
||||
name =
|
||||
opts[:name] ||
|
||||
raise ArgumentError, "the :name option is required when starting Phoenix.PubSub"
|
||||
|
||||
sup_name = Module.concat(name, "Supervisor")
|
||||
Supervisor.start_link(__MODULE__, opts, name: sup_name)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
name = opts[:name]
|
||||
adapter = opts[:adapter] || Phoenix.PubSub.PG2
|
||||
adapter_name = Module.concat(name, "Adapter")
|
||||
|
||||
partitions =
|
||||
opts[:registry_size] || opts[:pool_size] ||
|
||||
System.schedulers_online() |> Kernel./(4) |> Float.ceil() |> trunc()
|
||||
|
||||
registry = [
|
||||
meta: [pubsub: {adapter, adapter_name}],
|
||||
partitions: partitions,
|
||||
keys: :duplicate,
|
||||
name: name
|
||||
]
|
||||
|
||||
children = [
|
||||
{Registry, registry},
|
||||
{adapter, Keyword.put(opts, :adapter_name, adapter_name)}
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :rest_for_one)
|
||||
end
|
||||
end
|
||||
372
phoenix/deps/phoenix_pubsub/lib/phoenix/tracker.ex
Normal file
372
phoenix/deps/phoenix_pubsub/lib/phoenix/tracker.ex
Normal file
@@ -0,0 +1,372 @@
|
||||
defmodule Phoenix.Tracker do
|
||||
@moduledoc ~S"""
|
||||
Provides distributed presence tracking to processes.
|
||||
|
||||
Tracker shards use a heartbeat protocol and CRDT to replicate presence
|
||||
information across a cluster in an eventually consistent, conflict-free
|
||||
manner. Under this design, there is no single source of truth or global
|
||||
process. Each node runs a pool of trackers and node-local changes are
|
||||
replicated across the cluster and handled locally as a diff of changes.
|
||||
|
||||
## Implementing a Tracker
|
||||
|
||||
To start a tracker, first add the tracker to your supervision tree:
|
||||
|
||||
children = [
|
||||
# ...
|
||||
{MyTracker, [name: MyTracker, pubsub_server: MyApp.PubSub]}
|
||||
]
|
||||
|
||||
Next, implement `MyTracker` with support for the `Phoenix.Tracker`
|
||||
behaviour callbacks. An example of a minimal tracker could include:
|
||||
|
||||
defmodule MyTracker do
|
||||
use Phoenix.Tracker
|
||||
|
||||
def start_link(opts) do
|
||||
opts = Keyword.merge([name: __MODULE__], opts)
|
||||
Phoenix.Tracker.start_link(__MODULE__, opts, opts)
|
||||
end
|
||||
|
||||
def init(opts) do
|
||||
server = Keyword.fetch!(opts, :pubsub_server)
|
||||
{:ok, %{pubsub_server: server, node_name: Phoenix.PubSub.node_name(server)}}
|
||||
end
|
||||
|
||||
def handle_diff(diff, state) do
|
||||
for {topic, {joins, leaves}} <- diff do
|
||||
for {key, meta} <- joins do
|
||||
IO.puts "presence join: key \"#{key}\" with meta #{inspect meta}"
|
||||
msg = {:join, key, meta}
|
||||
Phoenix.PubSub.direct_broadcast!(state.node_name, state.pubsub_server, topic, msg)
|
||||
end
|
||||
for {key, meta} <- leaves do
|
||||
IO.puts "presence leave: key \"#{key}\" with meta #{inspect meta}"
|
||||
msg = {:leave, key, meta}
|
||||
Phoenix.PubSub.direct_broadcast!(state.node_name, state.pubsub_server, topic, msg)
|
||||
end
|
||||
end
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
|
||||
Trackers must implement `start_link/1`, `c:init/1`, and `c:handle_diff/2`.
|
||||
The `c:init/1` callback allows the tracker to manage its own state when
|
||||
running within the `Phoenix.Tracker` server. The `handle_diff` callback
|
||||
is invoked with a diff of presence join and leave events, grouped by
|
||||
topic. As replicas heartbeat and replicate data, the local tracker state is
|
||||
merged with the remote data, and the diff is sent to the callback. The
|
||||
handler can use this information to notify subscribers of events, as
|
||||
done above.
|
||||
|
||||
An optional `handle_info/2` callback may also be invoked to handle
|
||||
application specific messages within your tracker.
|
||||
|
||||
## Stability and Performance Considerations
|
||||
|
||||
Operations within `handle_diff/2` happen *in the tracker server's context*.
|
||||
Therefore, blocking operations should be avoided when possible, and offloaded
|
||||
to a supervised task when required. Also, a crash in the `handle_diff/2` will
|
||||
crash the tracker server, so operations that may crash the server should be
|
||||
offloaded with a `Task.Supervisor` spawned process.
|
||||
|
||||
## Application Shutdown
|
||||
|
||||
When a tracker shuts down, the other nodes do not assume it is gone
|
||||
for good. After all, in a distributed system, it is impossible to know if something
|
||||
is just temporarily unavailable or if it has crashed.
|
||||
|
||||
For this reason, when you call `System.stop()` or the Erlang VM receives a
|
||||
`SIGTERM`, any presences that the local tracker instance has will continue to
|
||||
be seen as present by other trackers in the cluster until the `:down_period`
|
||||
for the instance has passed.
|
||||
|
||||
If you want a normal shutdown to immediately cause other nodes to see that
|
||||
tracker's presences as leaving, pass `permdown_on_shutdown: true`. On the
|
||||
other hand, if you are using `Phoenix.Presence` for clients which will
|
||||
immediately attempt to connect to a new node, it may be preferable to use
|
||||
`permdown_on_shutdown: false`, allowing the disconnected clients time to
|
||||
reconnect before removing their old presences, to avoid overwhelming clients
|
||||
with notifications that many users left and immediately rejoined.
|
||||
|
||||
If the application crashes or is halted non-gracefully (for instance, with a
|
||||
`SIGKILL` or a `Ctrl+C` in `iex`), other nodes will still have to wait the
|
||||
`:down_period` to notice that the tracker's presences are gone.
|
||||
"""
|
||||
|
||||
use Supervisor
|
||||
alias Phoenix.Tracker.Shard
|
||||
|
||||
@type presence :: {key :: String.t(), meta :: map}
|
||||
@type topic :: String.t()
|
||||
|
||||
@callback init(Keyword.t()) :: {:ok, state :: term} | {:error, reason :: term}
|
||||
@callback handle_diff(%{topic => {joins :: [presence], leaves :: [presence]}}, state :: term) ::
|
||||
{:ok, state :: term}
|
||||
@callback handle_info(message :: term, state :: term) :: {:noreply, state :: term}
|
||||
@optional_callbacks handle_info: 2
|
||||
|
||||
defmacro __using__(_opts) do
|
||||
quote location: :keep do
|
||||
@behaviour Phoenix.Tracker
|
||||
|
||||
if Module.get_attribute(__MODULE__, :doc) == nil do
|
||||
@doc """
|
||||
Returns a specification to start this module under a supervisor.
|
||||
|
||||
See `Supervisor`.
|
||||
"""
|
||||
end
|
||||
|
||||
def child_spec(init_arg) do
|
||||
default = %{
|
||||
id: __MODULE__,
|
||||
start: {__MODULE__, :start_link, [init_arg]},
|
||||
type: :supervisor
|
||||
}
|
||||
end
|
||||
|
||||
defoverridable child_spec: 1
|
||||
end
|
||||
end
|
||||
|
||||
## Client
|
||||
|
||||
@doc """
|
||||
Tracks a presence.
|
||||
|
||||
* `tracker_name` - The registered name of the tracker server
|
||||
* `pid` - The Pid to track
|
||||
* `topic` - The `Phoenix.PubSub` topic for this presence
|
||||
* `key` - The key identifying this presence
|
||||
* `meta` - The map of metadata to attach to this presence
|
||||
|
||||
A process may be tracked multiple times, provided the topic and key pair
|
||||
are unique for any prior calls for the given process.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Phoenix.Tracker.track(MyTracker, self(), "lobby", u.id, %{stat: "away"})
|
||||
{:ok, "1WpAofWYIAA="}
|
||||
|
||||
iex> Phoenix.Tracker.track(MyTracker, self(), "lobby", u.id, %{stat: "away"})
|
||||
{:error, {:already_tracked, #PID<0.56.0>, "lobby", "123"}}
|
||||
"""
|
||||
@spec track(atom, pid, topic, term, map) :: {:ok, ref :: binary} | {:error, reason :: term}
|
||||
def track(tracker_name, pid, topic, key, meta) when is_pid(pid) and is_map(meta) do
|
||||
tracker_name
|
||||
|> Shard.name_for_topic(topic, pool_size(tracker_name))
|
||||
|> GenServer.call({:track, pid, topic, key, meta})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Untracks a presence.
|
||||
|
||||
* `tracker_name` - The registered name of the tracker server
|
||||
* `pid` - The Pid to untrack
|
||||
* `topic` - The `Phoenix.PubSub` topic to untrack for this presence
|
||||
* `key` - The key identifying this presence
|
||||
|
||||
All presences for a given Pid can be untracked by calling the
|
||||
`Phoenix.Tracker.untrack/2` signature of this function.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Phoenix.Tracker.untrack(MyTracker, self(), "lobby", u.id)
|
||||
:ok
|
||||
iex> Phoenix.Tracker.untrack(MyTracker, self())
|
||||
:ok
|
||||
"""
|
||||
@spec untrack(atom, pid, topic, term) :: :ok
|
||||
def untrack(tracker_name, pid, topic, key) when is_pid(pid) do
|
||||
tracker_name
|
||||
|> Shard.name_for_topic(topic, pool_size(tracker_name))
|
||||
|> GenServer.call({:untrack, pid, topic, key})
|
||||
end
|
||||
|
||||
def untrack(tracker_name, pid) when is_pid(pid) do
|
||||
shard_multicall(tracker_name, {:untrack, pid})
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a presence's metadata.
|
||||
|
||||
* `tracker_name` - The registered name of the tracker server
|
||||
* `pid` - The Pid being tracked
|
||||
* `topic` - The `Phoenix.PubSub` topic to update for this presence
|
||||
* `key` - The key identifying this presence
|
||||
* `meta` - Either a new map of metadata to attach to this presence,
|
||||
or a function. The function will receive the current metadata as
|
||||
input and the return value will be used as the new metadata
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Phoenix.Tracker.update(MyTracker, self(), "lobby", u.id, %{stat: "zzz"})
|
||||
{:ok, "1WpAofWYIAA="}
|
||||
|
||||
iex> Phoenix.Tracker.update(MyTracker, self(), "lobby", u.id, fn meta -> Map.put(meta, :away, true) end)
|
||||
{:ok, "1WpAofWYIAA="}
|
||||
"""
|
||||
@spec update(atom, pid, topic, term, map | (map -> map)) ::
|
||||
{:ok, ref :: binary} | {:error, reason :: term}
|
||||
def update(tracker_name, pid, topic, key, meta)
|
||||
when is_pid(pid) and (is_map(meta) or is_function(meta)) do
|
||||
tracker_name
|
||||
|> Shard.name_for_topic(topic, pool_size(tracker_name))
|
||||
|> GenServer.call({:update, pid, topic, key, meta})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lists all presences tracked under a given topic.
|
||||
|
||||
* `tracker_name` - The registered name of the tracker server
|
||||
* `topic` - The `Phoenix.PubSub` topic
|
||||
|
||||
Returns a list of presences in key/metadata tuple pairs.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Phoenix.Tracker.list(MyTracker, "lobby")
|
||||
[{123, %{name: "user 123"}}, {456, %{name: "user 456"}}]
|
||||
"""
|
||||
@spec list(atom, topic) :: [presence]
|
||||
def list(tracker_name, topic) do
|
||||
tracker_name
|
||||
|> Shard.name_for_topic(topic, pool_size(tracker_name))
|
||||
|> Phoenix.Tracker.Shard.list(topic)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets presences tracked under a given topic and key pair.
|
||||
|
||||
* `tracker_name` - The registered name of the tracker server
|
||||
* `topic` - The `Phoenix.PubSub` topic
|
||||
* `key` - The key of the presence
|
||||
|
||||
Returns a list of presence metadata.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Phoenix.Tracker.get_by_key(MyTracker, "lobby", "user1")
|
||||
[{#PID<0.88.0>, %{name: "User 1"}}, {#PID<0.89.0>, %{name: "User 1"}}]
|
||||
"""
|
||||
@spec get_by_key(atom, topic, term) :: [{pid, map}]
|
||||
def get_by_key(tracker_name, topic, key) do
|
||||
tracker_name
|
||||
|> Shard.name_for_topic(topic, pool_size(tracker_name))
|
||||
|> Phoenix.Tracker.Shard.get_by_key(topic, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gracefully shuts down by broadcasting permdown to all replicas.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Phoenix.Tracker.graceful_permdown(MyTracker)
|
||||
:ok
|
||||
"""
|
||||
@spec graceful_permdown(atom) :: :ok
|
||||
def graceful_permdown(tracker_name) do
|
||||
shard_multicall(tracker_name, :graceful_permdown)
|
||||
Supervisor.stop(tracker_name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts a tracker pool.
|
||||
|
||||
* `tracker` - The tracker module implementing the `Phoenix.Tracker` behaviour
|
||||
* `tracker_arg` - The argument to pass to the tracker handler `c:init/1`
|
||||
* `pool_opts` - The list of options used to construct the shard pool
|
||||
|
||||
## Required `pool_opts`:
|
||||
|
||||
* `:name` - The name of the server, such as: `MyApp.Tracker`
|
||||
This will also form the common prefix for all shard names
|
||||
* `:pubsub_server` - The name of the PubSub server, such as: `MyApp.PubSub`
|
||||
|
||||
## Optional `pool_opts`:
|
||||
|
||||
* `:broadcast_period` - The interval in milliseconds to send delta broadcasts
|
||||
across the cluster. Default `1500`
|
||||
* `:max_silent_periods` - The max integer of broadcast periods for which no
|
||||
delta broadcasts have been sent. Default `10` (15s heartbeat)
|
||||
* `:down_period` - The interval in milliseconds to flag a replica
|
||||
as temporarily down. Default `broadcast_period * max_silent_periods * 2`
|
||||
(30s down detection). Note: This must be at least 2x the `broadcast_period`.
|
||||
* `permdown_on_shutdown` - boolean; whether to immediately call
|
||||
`graceful_permdown/1` on the tracker during a graceful shutdown. See
|
||||
'Application Shutdown' section. You can only safely set this if `Phoenix.Tracker`
|
||||
is mounted at the root of your supervision tree and the strategy is `:one_for_one`.
|
||||
Default `false`.
|
||||
* `:permdown_period` - The interval in milliseconds to flag a replica
|
||||
as permanently down, and discard its state.
|
||||
Note: This must be at least greater than the `down_period`.
|
||||
Default `1_200_000` (20 minutes)
|
||||
* `:clock_sample_periods` - The numbers of heartbeat windows to sample
|
||||
remote clocks before collapsing and requesting transfer. Default `2`
|
||||
* `:max_delta_sizes` - The list of delta generation sizes to keep before
|
||||
falling back to sending entire state. Defaults `[100, 1000, 10_000]`.
|
||||
* `:log_level` - The log level to log events, defaults `:debug` and can be
|
||||
disabled with `false`
|
||||
* `:pool_size` - The number of tracker shards to launch. Default `1`
|
||||
"""
|
||||
def start_link(tracker, tracker_arg, pool_opts) do
|
||||
name = Keyword.fetch!(pool_opts, :name)
|
||||
Supervisor.start_link(__MODULE__, [tracker, tracker_arg, pool_opts, name], name: name)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init([tracker, tracker_opts, opts, name]) do
|
||||
pool_size = Keyword.get(opts, :pool_size, 1)
|
||||
permdown_on_shutdown = Keyword.get(opts, :permdown_on_shutdown, false)
|
||||
^name = :ets.new(name, [:set, :named_table, read_concurrency: true])
|
||||
true = :ets.insert(name, {:pool_size, pool_size})
|
||||
|
||||
shards =
|
||||
for n <- 0..(pool_size - 1) do
|
||||
shard_name = Shard.name_for_number(name, n)
|
||||
shard_opts = Keyword.put(opts, :shard_number, n)
|
||||
|
||||
%{
|
||||
id: shard_name,
|
||||
start: {Phoenix.Tracker.Shard, :start_link, [tracker, tracker_opts, shard_opts]},
|
||||
restart: :transient
|
||||
}
|
||||
end
|
||||
|
||||
children =
|
||||
if permdown_on_shutdown do
|
||||
shards ++
|
||||
[
|
||||
%{
|
||||
id: :shutdown_handler,
|
||||
start: {Phoenix.Tracker.ShutdownHandler, :start_link, [tracker]}
|
||||
}
|
||||
]
|
||||
else
|
||||
shards
|
||||
end
|
||||
|
||||
opts = [
|
||||
strategy: :one_for_one,
|
||||
max_restarts: pool_size * 2,
|
||||
max_seconds: 1
|
||||
]
|
||||
|
||||
Supervisor.init(children, opts)
|
||||
end
|
||||
|
||||
defp pool_size(tracker_name) do
|
||||
[{:pool_size, size}] = :ets.lookup(tracker_name, :pool_size)
|
||||
size
|
||||
end
|
||||
|
||||
defp shard_multicall(tracker_name, message) do
|
||||
for shard_number <- 0..(pool_size(tracker_name) - 1) do
|
||||
tracker_name
|
||||
|> Shard.name_for_number(shard_number)
|
||||
|> GenServer.call(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
104
phoenix/deps/phoenix_pubsub/lib/phoenix/tracker/clock.ex
Normal file
104
phoenix/deps/phoenix_pubsub/lib/phoenix/tracker/clock.ex
Normal file
@@ -0,0 +1,104 @@
|
||||
defmodule Phoenix.Tracker.Clock do
|
||||
@moduledoc false
|
||||
alias Phoenix.Tracker.State
|
||||
|
||||
@type context :: State.context
|
||||
@type clock :: {State.name, context}
|
||||
|
||||
@doc """
|
||||
Returns a list of replicas from a list of contexts.
|
||||
"""
|
||||
@spec clockset_replicas([clock]) :: [State.name]
|
||||
def clockset_replicas(clockset) do
|
||||
for {replica, _} <- clockset, do: replica
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a replicas context to a clockset, keeping only dominate contexts.
|
||||
"""
|
||||
@spec append_clock([clock], clock) :: [clock]
|
||||
def append_clock(clockset, {_, clock}) when map_size(clock) == 0, do: clockset
|
||||
def append_clock(clockset, {node, clock}) do
|
||||
big_clock = combine_clocks(clockset)
|
||||
cond do
|
||||
dominates?(clock, big_clock) -> [{node, clock}]
|
||||
dominates?(big_clock, clock) -> clockset
|
||||
true -> filter_clocks(clockset, {node, clock})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if one clock causally dominates the other for all replicas.
|
||||
"""
|
||||
@spec dominates?(context, context) :: boolean
|
||||
def dominates?(c1, c2) when map_size(c1) < map_size(c2), do: false
|
||||
def dominates?(c1, c2) do
|
||||
Enum.reduce_while(c2, true, fn {replica, clock}, true ->
|
||||
if Map.get(c1, replica, 0) >= clock do
|
||||
{:cont, true}
|
||||
else
|
||||
{:halt, false}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if one clock causally dominates the other for their shared replicas.
|
||||
"""
|
||||
def dominates_or_equal?(c1, c2) when c1 == %{} and c2 == %{}, do: true
|
||||
def dominates_or_equal?(c1, _c2) when c1 == %{}, do: false
|
||||
def dominates_or_equal?(c1, c2) do
|
||||
Enum.reduce_while(c1, true, fn {replica, clock}, true ->
|
||||
if clock >= Map.get(c2, replica, 0) do
|
||||
{:cont, true}
|
||||
else
|
||||
{:halt, false}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the upper bound causal context of two clocks.
|
||||
"""
|
||||
def upperbound(c1, c2) do
|
||||
Map.merge(c1, c2, fn _, v1, v2 -> max(v1, v2) end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the lower bound causal context of two clocks.
|
||||
"""
|
||||
def lowerbound(c1, c2) do
|
||||
Map.merge(c1, c2, fn _, v1, v2 -> min(v1, v2) end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the clock with just provided replicas.
|
||||
"""
|
||||
def filter_replicas(c, replicas), do: Map.take(c, replicas)
|
||||
|
||||
@doc """
|
||||
Returns replicas from the given clock.
|
||||
"""
|
||||
def replicas(c), do: Map.keys(c)
|
||||
|
||||
defp filter_clocks(clockset, {node, clock}) do
|
||||
clockset
|
||||
|> Enum.reduce({[], false}, fn {node2, clock2}, {set, insert} ->
|
||||
if dominates?(clock, clock2) do
|
||||
{set, true}
|
||||
else
|
||||
{[{node2, clock2}| set], insert || !dominates?(clock2, clock)}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{new_clockset, true} -> [{node, clock} | new_clockset]
|
||||
{new_clockset, false} -> new_clockset
|
||||
end
|
||||
end
|
||||
|
||||
defp combine_clocks(clockset) do
|
||||
clockset
|
||||
|> Enum.map(fn {_, clocks} -> clocks end)
|
||||
|> Enum.reduce(%{}, &upperbound(&1, &2))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
defmodule Phoenix.Tracker.DeltaGeneration do
|
||||
@moduledoc false
|
||||
require Logger
|
||||
alias Phoenix.Tracker.{State, Clock, Replica}
|
||||
|
||||
@doc """
|
||||
Extracts minimal delta from generations to satisfy remote clock.
|
||||
|
||||
Falls back to extracting entire crdt if unable to match delta.
|
||||
"""
|
||||
@spec extract(State.t, [State.delta], State.name, State.context) :: State.delta | State.t
|
||||
def extract(%State{mode: :normal} = state, generations, remote_ref, remote_context) do
|
||||
case delta_fulfilling_clock(generations, remote_context) do
|
||||
{delta, index} ->
|
||||
if index, do: Logger.debug "#{inspect state.replica}: sending delta generation #{index + 1}"
|
||||
State.extract(delta, remote_ref, remote_context)
|
||||
nil ->
|
||||
Logger.debug "#{inspect state.replica}: falling back to sending entire crdt"
|
||||
State.extract(state, remote_ref, remote_context)
|
||||
end
|
||||
end
|
||||
|
||||
@spec push(State.t, [State.delta], State.delta, [pos_integer]) :: [State.delta]
|
||||
def push(%State{mode: :normal} = parent, [] = _generations, %State{mode: :delta} = delta, opts) do
|
||||
parent.delta
|
||||
|> List.duplicate(Enum.count(opts))
|
||||
|> do_push(delta, opts, {delta, []})
|
||||
end
|
||||
def push(%State{mode: :normal} = _parent, generations, %State{mode: :delta} = delta, opts) do
|
||||
do_push(generations, delta, opts, {delta, []})
|
||||
end
|
||||
defp do_push([], _delta, [], {_prev, acc}), do: Enum.reverse(acc)
|
||||
defp do_push([gen | generations], delta, [gen_max | opts], {prev, acc}) do
|
||||
case State.merge_deltas(gen, delta) do
|
||||
{:ok, merged} ->
|
||||
if State.delta_size(merged) <= gen_max do
|
||||
do_push(generations, delta, opts, {merged, [merged | acc]})
|
||||
else
|
||||
do_push(generations, delta, opts, {merged, [prev | acc]})
|
||||
end
|
||||
|
||||
{:error, :not_contiguous} ->
|
||||
do_push(generations, delta, opts, {gen, [gen | acc]})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prunes permanently downed replicates from the delta generation list
|
||||
"""
|
||||
@spec remove_down_replicas([State.delta], Replica.replica_ref) :: [State.delta]
|
||||
def remove_down_replicas(generations, replica_ref) do
|
||||
Enum.map(generations, fn %State{mode: :delta} = gen ->
|
||||
State.remove_down_replicas(gen, replica_ref)
|
||||
end)
|
||||
end
|
||||
|
||||
defp delta_fulfilling_clock(generations, remote_context) do
|
||||
generations
|
||||
|> Enum.with_index()
|
||||
|> Enum.find(fn {%State{range: {local_start, local_end}}, _} ->
|
||||
local_start = Clock.filter_replicas(local_start, Clock.replicas(remote_context))
|
||||
local_end = Clock.filter_replicas(local_end, Clock.replicas(remote_context))
|
||||
|
||||
not Clock.dominates_or_equal?(local_start, local_end) and
|
||||
Clock.dominates_or_equal?(remote_context, local_start) and
|
||||
not Clock.dominates?(remote_context, local_end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
91
phoenix/deps/phoenix_pubsub/lib/phoenix/tracker/replica.ex
Normal file
91
phoenix/deps/phoenix_pubsub/lib/phoenix/tracker/replica.ex
Normal file
@@ -0,0 +1,91 @@
|
||||
defmodule Phoenix.Tracker.Replica do
|
||||
@moduledoc false
|
||||
alias Phoenix.Tracker.Replica
|
||||
|
||||
@type name :: String.t()
|
||||
@type vsn :: integer
|
||||
@type replica_ref :: {name, vsn}
|
||||
|
||||
@type t :: %Replica{
|
||||
name: name,
|
||||
vsn: vsn,
|
||||
last_heartbeat_at: pos_integer | nil,
|
||||
status: :up | :down | :permdown
|
||||
}
|
||||
|
||||
defstruct name: nil,
|
||||
vsn: nil,
|
||||
last_heartbeat_at: nil,
|
||||
status: :up
|
||||
|
||||
@type op_result ::
|
||||
{%{name => Replica.t()}, previous_node :: Replica.t() | nil,
|
||||
updated_node :: Replica.t()}
|
||||
|
||||
@doc """
|
||||
Returns a new Replica with a unique vsn.
|
||||
"""
|
||||
@spec new(name) :: Replica.t()
|
||||
def new(name) do
|
||||
%Replica{name: name, vsn: unique_vsn()}
|
||||
end
|
||||
|
||||
@spec ref(Replica.t()) :: replica_ref
|
||||
def ref(%Replica{name: name, vsn: vsn}), do: {name, vsn}
|
||||
|
||||
@spec put_heartbeat(%{name => Replica.t()}, replica_ref) :: op_result
|
||||
def put_heartbeat(replicas, {name, vsn}) do
|
||||
case Map.fetch(replicas, name) do
|
||||
:error ->
|
||||
new_replica = touch_last_heartbeat(%Replica{name: name, vsn: vsn, status: :up})
|
||||
{Map.put(replicas, name, new_replica), nil, new_replica}
|
||||
|
||||
{:ok, %Replica{} = prev_replica} ->
|
||||
updated_replica = touch_last_heartbeat(%{prev_replica | vsn: vsn, status: :up})
|
||||
{Map.put(replicas, name, updated_replica), prev_replica, updated_replica}
|
||||
end
|
||||
end
|
||||
|
||||
@spec detect_down(%{name => Replica.t()}, Replica.t(), pos_integer, pos_integer) :: op_result
|
||||
def detect_down(replicas, replica, temp_interval, perm_interval, now \\ now_ms()) do
|
||||
downtime = now - replica.last_heartbeat_at
|
||||
|
||||
cond do
|
||||
downtime > perm_interval ->
|
||||
{Map.delete(replicas, replica.name), replica, permdown(replica)}
|
||||
|
||||
downtime > temp_interval ->
|
||||
updated_replica = down(replica)
|
||||
{Map.put(replicas, replica.name, updated_replica), replica, updated_replica}
|
||||
|
||||
true ->
|
||||
{replicas, replica, replica}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches a replica from the map with matching name and version from the ref.
|
||||
"""
|
||||
@spec fetch_by_ref(%{name => Replica.t()}, replica_ref) :: {:ok, Replica.t()} | :error
|
||||
def fetch_by_ref(replicas, {name, vsn}) do
|
||||
case Map.fetch(replicas, name) do
|
||||
{:ok, %Replica{vsn: ^vsn} = replica} -> {:ok, replica}
|
||||
{:ok, %Replica{vsn: _vsn}} -> :error
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp permdown(%Replica{} = replica), do: %{replica | status: :permdown}
|
||||
|
||||
defp down(%Replica{} = replica), do: %{replica | status: :down}
|
||||
|
||||
defp touch_last_heartbeat(%Replica{} = replica) do
|
||||
%{replica | last_heartbeat_at: now_ms()}
|
||||
end
|
||||
|
||||
defp now_ms, do: System.system_time(:millisecond)
|
||||
|
||||
defp unique_vsn do
|
||||
System.system_time(:microsecond) + System.unique_integer([:positive])
|
||||
end
|
||||
end
|
||||
564
phoenix/deps/phoenix_pubsub/lib/phoenix/tracker/shard.ex
Normal file
564
phoenix/deps/phoenix_pubsub/lib/phoenix/tracker/shard.ex
Normal file
@@ -0,0 +1,564 @@
|
||||
defmodule Phoenix.Tracker.Shard do
|
||||
@moduledoc false
|
||||
use GenServer
|
||||
alias Phoenix.Tracker.{Clock, State, Replica, DeltaGeneration}
|
||||
require Logger
|
||||
|
||||
@type presence :: {key :: String.t, meta :: map()}
|
||||
@type topic :: String.t
|
||||
|
||||
@callback init(Keyword.t) :: {:ok, pid} | {:error, reason :: term}
|
||||
@callback handle_diff(%{topic => {joins :: [presence], leaves :: [presence]}}, state :: term) :: {:ok, state :: term}
|
||||
@callback handle_info(message :: term, state :: term) :: {:noreply, state :: term}
|
||||
@optional_callbacks handle_info: 2
|
||||
|
||||
@type t :: %{
|
||||
shard_name: String.t(),
|
||||
pubsub_server: atom(),
|
||||
tracker: module,
|
||||
tracker_state: any,
|
||||
replica: Replica.t(),
|
||||
report_events_to: any,
|
||||
namespaced_topic: String.t(),
|
||||
log_level: boolean | atom,
|
||||
replicas: map,
|
||||
pending_clockset: [],
|
||||
presences: State.t(),
|
||||
broadcast_period: integer,
|
||||
max_silent_periods: integer(),
|
||||
silent_periods: integer(),
|
||||
down_period: integer,
|
||||
permdown_period: integer,
|
||||
clock_sample_periods: integer,
|
||||
deltas: [State.delta()],
|
||||
max_delta_sizes: integer,
|
||||
current_sample_count: integer
|
||||
}
|
||||
|
||||
## Used by Phoenix.Tracker for dispatching to appropriate shard
|
||||
@spec name_for_number(atom, non_neg_integer) :: atom
|
||||
def name_for_number(prefix, n) when is_number(n) do
|
||||
:"#{prefix}_shard#{n}"
|
||||
end
|
||||
|
||||
@spec name_for_topic(atom, topic, non_neg_integer) :: atom
|
||||
def name_for_topic(prefix, topic, pool_size) do
|
||||
shard_number = :erlang.phash2(topic, pool_size)
|
||||
name_for_number(prefix, shard_number)
|
||||
end
|
||||
|
||||
## Client
|
||||
|
||||
@spec track(pid, pid, topic, term, map()) :: {:ok, ref :: binary} | {:error, reason :: term}
|
||||
def track(server_pid, pid, topic, key, meta) when is_pid(pid) and is_map(meta) do
|
||||
GenServer.call(server_pid, {:track, pid, topic, key, meta})
|
||||
end
|
||||
|
||||
@spec untrack(pid, pid, topic, term) :: :ok
|
||||
def untrack(server_pid, pid, topic, key) when is_pid(pid) do
|
||||
GenServer.call(server_pid, {:untrack, pid, topic, key})
|
||||
end
|
||||
def untrack(server_pid, pid) when is_pid(pid) do
|
||||
GenServer.call(server_pid, {:untrack, pid})
|
||||
end
|
||||
|
||||
@spec update(pid, pid, topic, term, map() | (map() -> map())) :: {:ok, ref :: binary} | {:error, reason :: term}
|
||||
def update(server_pid, pid, topic, key, meta) when is_pid(pid) and (is_map(meta) or is_function(meta)) do
|
||||
GenServer.call(server_pid, {:update, pid, topic, key, meta})
|
||||
end
|
||||
|
||||
@spec list(pid | atom, topic) :: [presence]
|
||||
def list(server_pid, topic) do
|
||||
server_pid
|
||||
|> GenServer.call({:list, topic})
|
||||
|> State.get_by_topic(topic)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def dirty_list(shard_name, topic) do
|
||||
State.tracked_values(shard_name, topic, [])
|
||||
end
|
||||
|
||||
@spec get_by_key(pid | atom, topic, term) :: [{pid, map}]
|
||||
def get_by_key(server_pid, topic, key) do
|
||||
server_pid
|
||||
|> GenServer.call({:list, topic})
|
||||
|> State.get_by_key(topic, key)
|
||||
end
|
||||
|
||||
@spec graceful_permdown(pid) :: :ok
|
||||
def graceful_permdown(server_pid) do
|
||||
GenServer.call(server_pid, :graceful_permdown)
|
||||
end
|
||||
|
||||
## Server
|
||||
|
||||
def start_link(tracker, tracker_opts, pool_opts) do
|
||||
number = Keyword.fetch!(pool_opts, :shard_number)
|
||||
tracker_name = Keyword.fetch!(pool_opts, :name)
|
||||
name = name_for_number(tracker_name, number)
|
||||
shard_opts = Keyword.put(pool_opts, :name, name)
|
||||
GenServer.start_link(__MODULE__,
|
||||
[tracker, tracker_opts, shard_opts], name: name)
|
||||
end
|
||||
|
||||
def init([tracker, tracker_opts, shard_opts]) do
|
||||
Process.flag(:trap_exit, true)
|
||||
shard_name = Keyword.fetch!(shard_opts, :name)
|
||||
pubsub_server = Keyword.fetch!(shard_opts, :pubsub_server)
|
||||
broadcast_period = shard_opts[:broadcast_period] || 1500
|
||||
max_silent_periods = shard_opts[:max_silent_periods] || 10
|
||||
down_period = shard_opts[:down_period]
|
||||
|| (broadcast_period * max_silent_periods * 2)
|
||||
permdown_period = shard_opts[:permdown_period] || 1_200_000
|
||||
clock_sample_periods = shard_opts[:clock_sample_periods] || 2
|
||||
log_level = Keyword.get(shard_opts, :log_level, false)
|
||||
max_delta_sizes = shard_opts[:max_delta_sizes] || [100, 1000, 10_000]
|
||||
|
||||
with :ok <- validate_down_period(down_period, broadcast_period),
|
||||
:ok <- validate_permdown_period(permdown_period, down_period),
|
||||
{:ok, tracker_state} <- tracker.init(tracker_opts) do
|
||||
|
||||
node_name = Phoenix.PubSub.node_name(pubsub_server)
|
||||
namespaced_topic = namespaced_topic(shard_name)
|
||||
replica = Replica.new(node_name)
|
||||
|
||||
subscribe(pubsub_server, namespaced_topic)
|
||||
send_stuttered_heartbeat(self(), broadcast_period)
|
||||
|
||||
{:ok, %{shard_name: shard_name,
|
||||
pubsub_server: pubsub_server,
|
||||
tracker: tracker,
|
||||
tracker_state: tracker_state,
|
||||
replica: replica,
|
||||
report_events_to: shard_opts[:report_events_to],
|
||||
namespaced_topic: namespaced_topic,
|
||||
log_level: log_level,
|
||||
replicas: %{},
|
||||
pending_clockset: [],
|
||||
presences: State.new(Replica.ref(replica), shard_name),
|
||||
broadcast_period: broadcast_period,
|
||||
max_silent_periods: max_silent_periods,
|
||||
silent_periods: max_silent_periods,
|
||||
down_period: down_period,
|
||||
permdown_period: permdown_period,
|
||||
clock_sample_periods: clock_sample_periods,
|
||||
deltas: [],
|
||||
max_delta_sizes: max_delta_sizes,
|
||||
current_sample_count: clock_sample_periods}}
|
||||
end
|
||||
end
|
||||
|
||||
def validate_down_period(d_period, b_period) when d_period < (2 * b_period) do
|
||||
{:error, "down_period must be at least twice as large as the broadcast_period"}
|
||||
end
|
||||
def validate_down_period(_d_period, _b_period), do: :ok
|
||||
|
||||
def validate_permdown_period(p_period, d_period) when p_period <= d_period do
|
||||
{:error, "permdown_period must be at least larger than the down_period"}
|
||||
end
|
||||
def validate_permdown_period(_p_period, _d_period), do: :ok
|
||||
|
||||
|
||||
defp send_stuttered_heartbeat(pid, interval) do
|
||||
Process.send_after(pid, :heartbeat, Enum.random(0..trunc(interval * 0.25)))
|
||||
end
|
||||
|
||||
def handle_info(:heartbeat, state) do
|
||||
{:noreply, state
|
||||
|> broadcast_delta_heartbeat()
|
||||
|> request_transfer_from_replicas_needing_synced()
|
||||
|> detect_downs()
|
||||
|> schedule_next_heartbeat()}
|
||||
end
|
||||
|
||||
def handle_info({:pub, :heartbeat, {name, vsn}, :empty, clocks}, state) do
|
||||
{:noreply, state
|
||||
|> put_pending_clock(clocks)
|
||||
|> handle_heartbeat({name, vsn})}
|
||||
end
|
||||
def handle_info({:pub, :heartbeat, {name, vsn}, delta, clocks}, state) do
|
||||
state = handle_heartbeat(state, {name, vsn})
|
||||
{presences, joined, left} = State.merge(state.presences, delta)
|
||||
|
||||
{:noreply, state
|
||||
|> report_diff(joined, left)
|
||||
|> put_presences(presences)
|
||||
|> put_pending_clock(clocks)
|
||||
|> push_delta_generation(delta)}
|
||||
end
|
||||
|
||||
def handle_info({:pub, :transfer_req, ref, {name, _vsn}, {_, clocks}}, state) do
|
||||
log state, fn -> "#{state.replica.name}: transfer_req from #{inspect name}" end
|
||||
delta = DeltaGeneration.extract(state.presences, state.deltas, name, clocks)
|
||||
msg = {:pub, :transfer_ack, ref, Replica.ref(state.replica), delta}
|
||||
direct_broadcast(state, name, msg)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info({:pub, :transfer_ack, _ref, {name, _vsn}, remote_presences}, state) do
|
||||
log(state, fn -> "#{state.replica.name}: transfer_ack from #{inspect name}" end)
|
||||
{presences, joined, left} = State.merge(state.presences, remote_presences)
|
||||
|
||||
{:noreply, state
|
||||
|> report_diff(joined, left)
|
||||
|> push_delta_generation(remote_presences)
|
||||
|> put_presences(presences)}
|
||||
end
|
||||
|
||||
def handle_info({:pub, :graceful_permdown, {_name, _vsn} = ref}, state) do
|
||||
case Replica.fetch_by_ref(state.replicas, ref) do
|
||||
{:ok, replica} -> {:noreply, state |> down(replica) |> permdown(replica)}
|
||||
:error -> {:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:EXIT, pid, _reason}, state) do
|
||||
{:noreply, drop_presence(state, pid)}
|
||||
end
|
||||
|
||||
def handle_info(msg, state) do
|
||||
if function_exported?(state.tracker, :handle_info, 2) do
|
||||
case state.tracker.handle_info(msg, state.tracker_state) do
|
||||
{:noreply, new_tracker_state} ->
|
||||
{:noreply, %{state | tracker_state: new_tracker_state}}
|
||||
|
||||
other ->
|
||||
raise ArgumentError, """
|
||||
expected #{state.tracker}.handle_info/2 to return {:noreply, state}, but got:
|
||||
|
||||
#{inspect other}
|
||||
"""
|
||||
end
|
||||
else
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call(:values, _from, state) do
|
||||
{:reply, :ets.match(state.presences.values, :"$1"), state}
|
||||
end
|
||||
|
||||
def handle_call({:track, pid, topic, key, meta}, _from, state) do
|
||||
case State.get_by_pid(state.presences, pid, topic, key) do
|
||||
nil ->
|
||||
{state, ref} = put_presence(state, pid, topic, key, meta)
|
||||
{:reply, {:ok, ref}, state}
|
||||
_ ->
|
||||
{:reply, {:error, {:already_tracked, pid, topic, key}}, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:untrack, pid, topic, key}, _from, state) do
|
||||
new_state = drop_presence(state, pid, topic, key)
|
||||
if State.get_by_pid(new_state.presences, pid) == [] do
|
||||
Process.unlink(pid)
|
||||
end
|
||||
{:reply, :ok, new_state}
|
||||
end
|
||||
|
||||
def handle_call({:untrack, pid}, _from, state) do
|
||||
Process.unlink(pid)
|
||||
{:reply, :ok, drop_presence(state, pid)}
|
||||
end
|
||||
|
||||
def handle_call({:update, pid, topic, key, meta_updater}, _from, state) when is_function(meta_updater) do
|
||||
handle_update({pid, topic, key, meta_updater}, state)
|
||||
end
|
||||
|
||||
def handle_call({:update, pid, topic, key, new_meta}, _from, state) do
|
||||
handle_update({pid, topic, key, fn _ -> new_meta end}, state)
|
||||
end
|
||||
|
||||
def handle_call(:graceful_permdown, _from, state) do
|
||||
broadcast_from(state, self(), {:pub, :graceful_permdown, Replica.ref(state.replica)})
|
||||
{:stop, :normal, :ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:list, _topic}, _from, state) do
|
||||
{:reply, state.presences, state}
|
||||
end
|
||||
|
||||
def handle_call(:replicas, _from, state) do
|
||||
{:reply, state.replicas, state}
|
||||
end
|
||||
|
||||
def handle_call(:unsubscribe, _from, state) do
|
||||
Phoenix.PubSub.unsubscribe(state.pubsub_server, state.namespaced_topic)
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
def handle_call(:resubscribe, _from, state) do
|
||||
subscribe(state.pubsub_server, state.namespaced_topic)
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
defp subscribe(pubsub_server, namespaced_topic) do
|
||||
Phoenix.PubSub.subscribe(pubsub_server, namespaced_topic, link: true)
|
||||
end
|
||||
|
||||
defp put_update(state, pid, topic, key, meta, %{phx_ref: prev_ref} = prev_meta) do
|
||||
ref = random_ref()
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> Map.put(:phx_ref, ref)
|
||||
|> Map.put(:phx_ref_prev, prev_ref)
|
||||
|
||||
new_state =
|
||||
state
|
||||
|> report_diff_join(topic, key, meta, prev_meta)
|
||||
|> put_presences(State.leave_join(state.presences, pid, topic, key, meta))
|
||||
|
||||
{new_state, ref}
|
||||
end
|
||||
|
||||
defp put_presence(state, pid, topic, key, meta, prev_meta \\ nil) do
|
||||
Process.link(pid)
|
||||
ref = random_ref()
|
||||
meta = Map.put(meta, :phx_ref, ref)
|
||||
new_state =
|
||||
state
|
||||
|> report_diff_join(topic, key, meta, prev_meta)
|
||||
|> put_presences(State.join(state.presences, pid, topic, key, meta))
|
||||
|
||||
{new_state, ref}
|
||||
end
|
||||
|
||||
defp put_presences(state, %State{} = presences), do: %{state | presences: presences}
|
||||
|
||||
defp drop_presence(state, pid, topic, key) do
|
||||
if leave = State.get_by_pid(state.presences, pid, topic, key) do
|
||||
state
|
||||
|> report_diff([], [leave])
|
||||
|> put_presences(State.leave(state.presences, pid, topic, key))
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
defp drop_presence(state, pid) do
|
||||
leaves = State.get_by_pid(state.presences, pid)
|
||||
|
||||
state
|
||||
|> report_diff([], leaves)
|
||||
|> put_presences(State.leave(state.presences, pid))
|
||||
end
|
||||
|
||||
defp handle_heartbeat(state, {name, vsn}) do
|
||||
case Replica.put_heartbeat(state.replicas, {name, vsn}) do
|
||||
{replicas, nil, %Replica{status: :up} = upped} ->
|
||||
up(%{state | replicas: replicas}, upped)
|
||||
|
||||
{replicas, %Replica{vsn: ^vsn, status: :up}, %Replica{vsn: ^vsn, status: :up}} ->
|
||||
%{state | replicas: replicas}
|
||||
|
||||
{replicas, %Replica{vsn: ^vsn, status: :down}, %Replica{vsn: ^vsn, status: :up} = upped} ->
|
||||
up(%{state | replicas: replicas}, upped)
|
||||
|
||||
{replicas, %Replica{vsn: old, status: :up} = downed, %Replica{vsn: ^vsn, status: :up} = upped} when old != vsn ->
|
||||
%{state | replicas: replicas} |> down(downed) |> permdown(downed) |> up(upped)
|
||||
|
||||
{replicas, %Replica{vsn: old, status: :down} = downed, %Replica{vsn: ^vsn, status: :up} = upped} when old != vsn ->
|
||||
%{state | replicas: replicas} |> permdown(downed) |> up(upped)
|
||||
end
|
||||
end
|
||||
|
||||
defp request_transfer_from_replicas_needing_synced(%{current_sample_count: 1} = state) do
|
||||
needs_synced = clockset_to_sync(state)
|
||||
for replica <- needs_synced, do: request_transfer(state, replica)
|
||||
|
||||
%{state | pending_clockset: [], current_sample_count: state.clock_sample_periods}
|
||||
end
|
||||
defp request_transfer_from_replicas_needing_synced(state) do
|
||||
%{state | current_sample_count: state.current_sample_count - 1}
|
||||
end
|
||||
|
||||
defp request_transfer(state, {name, _vsn}) do
|
||||
log state, fn -> "#{state.replica.name}: transfer_req from #{name}" end
|
||||
ref = make_ref()
|
||||
msg = {:pub, :transfer_req, ref, Replica.ref(state.replica), clock(state)}
|
||||
direct_broadcast(state, name, msg)
|
||||
end
|
||||
|
||||
defp detect_downs(%{permdown_period: perm_int, down_period: temp_int} = state) do
|
||||
Enum.reduce(state.replicas, state, fn {_name, replica}, acc ->
|
||||
case Replica.detect_down(acc.replicas, replica, temp_int, perm_int) do
|
||||
{replicas, %Replica{status: :up}, %Replica{status: :permdown} = down_rep} ->
|
||||
%{acc | replicas: replicas} |> down(down_rep) |> permdown(down_rep)
|
||||
|
||||
{replicas, %Replica{status: :down}, %Replica{status: :permdown} = down_rep} ->
|
||||
permdown(%{acc | replicas: replicas}, down_rep)
|
||||
|
||||
{replicas, %Replica{status: :up}, %Replica{status: :down} = down_rep} ->
|
||||
down(%{acc | replicas: replicas}, down_rep)
|
||||
|
||||
{replicas, %Replica{status: unchanged}, %Replica{status: unchanged}} ->
|
||||
%{acc | replicas: replicas}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp schedule_next_heartbeat(state) do
|
||||
Process.send_after(self(), :heartbeat, state.broadcast_period)
|
||||
state
|
||||
end
|
||||
|
||||
defp clock(state), do: State.clocks(state.presences)
|
||||
|
||||
@spec clockset_to_sync(t) :: [State.name]
|
||||
defp clockset_to_sync(state) do
|
||||
my_ref = Replica.ref(state.replica)
|
||||
|
||||
state.pending_clockset
|
||||
|> Clock.append_clock(clock(state))
|
||||
|> Clock.clockset_replicas()
|
||||
|> Enum.filter(fn ref -> ref != my_ref end)
|
||||
end
|
||||
|
||||
defp put_pending_clock(state, clocks) do
|
||||
%{state | pending_clockset: Clock.append_clock(state.pending_clockset, clocks)}
|
||||
end
|
||||
|
||||
defp up(state, remote_replica) do
|
||||
report_event(state, {:replica_up, remote_replica.name})
|
||||
log state, fn -> "#{state.replica.name}: replica up from #{inspect remote_replica.name}" end
|
||||
{presences, joined, []} = State.replica_up(state.presences, Replica.ref(remote_replica))
|
||||
|
||||
state
|
||||
|> report_diff(joined, [])
|
||||
|> put_presences(presences)
|
||||
end
|
||||
|
||||
defp down(state, remote_replica) do
|
||||
report_event(state, {:replica_down, remote_replica.name})
|
||||
log state, fn -> "#{state.replica.name}: replica down from #{inspect remote_replica.name}" end
|
||||
{presences, [], left} = State.replica_down(state.presences, Replica.ref(remote_replica))
|
||||
|
||||
state
|
||||
|> report_diff([], left)
|
||||
|> put_presences(presences)
|
||||
end
|
||||
|
||||
defp permdown(state, %Replica{name: name} = remote_replica) do
|
||||
report_event(state, {:replica_permdown, name})
|
||||
log state, fn -> "#{state.replica.name}: permanent replica down detected #{name}" end
|
||||
replica_ref = Replica.ref(remote_replica)
|
||||
presences = State.remove_down_replicas(state.presences, replica_ref)
|
||||
deltas = DeltaGeneration.remove_down_replicas(state.deltas, replica_ref)
|
||||
|
||||
case Replica.fetch_by_ref(state.replicas, replica_ref) do
|
||||
{:ok, _replica} ->
|
||||
replicas = Map.delete(state.replicas, name)
|
||||
%{state | presences: presences, replicas: replicas, deltas: deltas}
|
||||
_ ->
|
||||
%{state | presences: presences, deltas: deltas}
|
||||
end
|
||||
end
|
||||
|
||||
defp report_event(%{report_events_to: nil}, _event), do: :ok
|
||||
defp report_event(%{report_events_to: pid} = state, event) do
|
||||
send(pid, {event, state.replica.name})
|
||||
end
|
||||
|
||||
defp namespaced_topic(shard_name) do
|
||||
"phx_presence:#{shard_name}"
|
||||
end
|
||||
|
||||
defp broadcast_from(state, from, msg) do
|
||||
Phoenix.PubSub.broadcast_from!(state.pubsub_server, from, state.namespaced_topic, msg)
|
||||
end
|
||||
|
||||
defp direct_broadcast(state, target_node, msg) do
|
||||
Phoenix.PubSub.direct_broadcast!(target_node, state.pubsub_server, state.namespaced_topic, msg)
|
||||
end
|
||||
|
||||
defp broadcast_delta_heartbeat(%{presences: presences} = state) do
|
||||
cond do
|
||||
State.has_delta?(presences) ->
|
||||
delta = presences.delta
|
||||
new_presences = presences |> State.reset_delta() |> State.compact()
|
||||
|
||||
broadcast_from(state, self(), {:pub, :heartbeat, Replica.ref(state.replica), delta, clock(state)})
|
||||
%{state | presences: new_presences, silent_periods: 0}
|
||||
|> push_delta_generation(delta)
|
||||
|
||||
state.silent_periods >= state.max_silent_periods ->
|
||||
broadcast_from(state, self(), {:pub, :heartbeat, Replica.ref(state.replica), :empty, clock(state)})
|
||||
%{state | silent_periods: 0}
|
||||
|
||||
true -> update_in(state.silent_periods, &(&1 + 1))
|
||||
end
|
||||
end
|
||||
|
||||
defp report_diff(state, [], []), do: state
|
||||
defp report_diff(state, joined, left) do
|
||||
join_diff = Enum.reduce(joined, %{}, fn {{topic, _pid, key}, meta, _}, acc ->
|
||||
Map.update(acc, topic, {[{key, meta}], []}, fn {joins, leaves} ->
|
||||
{[{key, meta} | joins], leaves}
|
||||
end)
|
||||
end)
|
||||
full_diff = Enum.reduce(left, join_diff, fn {{topic, _pid, key}, meta, _}, acc ->
|
||||
Map.update(acc, topic, {[], [{key, meta}]}, fn {joins, leaves} ->
|
||||
{joins, [{key, meta} | leaves]}
|
||||
end)
|
||||
end)
|
||||
|
||||
full_diff
|
||||
|> state.tracker.handle_diff(state.tracker_state)
|
||||
|> handle_tracker_result(state)
|
||||
end
|
||||
|
||||
defp report_diff_join(state, topic, key, meta, nil = _prev_meta) do
|
||||
%{topic => {[{key, meta}], []}}
|
||||
|> state.tracker.handle_diff(state.tracker_state)
|
||||
|> handle_tracker_result(state)
|
||||
end
|
||||
defp report_diff_join(state, topic, key, meta, prev_meta) do
|
||||
%{topic => {[{key, meta}], [{key, prev_meta}]}}
|
||||
|> state.tracker.handle_diff(state.tracker_state)
|
||||
|> handle_tracker_result(state)
|
||||
end
|
||||
|
||||
defp handle_tracker_result({:ok, tracker_state}, state) do
|
||||
%{state | tracker_state: tracker_state}
|
||||
end
|
||||
defp handle_tracker_result(other, state) do
|
||||
raise ArgumentError, """
|
||||
expected #{state.tracker}.handle_diff/2 to return {:ok, state}, but got:
|
||||
|
||||
#{inspect other}
|
||||
"""
|
||||
end
|
||||
|
||||
defp handle_update({pid, topic, key, meta_updater}, state) do
|
||||
case State.get_by_pid(state.presences, pid, topic, key) do
|
||||
nil ->
|
||||
{:reply, {:error, :nopresence}, state}
|
||||
{{_topic, _pid, ^key}, prev_meta, {_replica, _}} ->
|
||||
{state, ref} = put_update(state, pid, topic, key, meta_updater.(prev_meta), prev_meta)
|
||||
{:reply, {:ok, ref}, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp push_delta_generation(state, {%State{mode: :normal}, _}) do
|
||||
%{state | deltas: []}
|
||||
end
|
||||
defp push_delta_generation(%{deltas: deltas} = state, %State{mode: :delta} = delta) do
|
||||
new_deltas = DeltaGeneration.push(state.presences, deltas, delta, state.max_delta_sizes)
|
||||
%{state | deltas: new_deltas}
|
||||
end
|
||||
|
||||
defp random_ref() do
|
||||
binary = <<
|
||||
System.system_time(:nanosecond)::64,
|
||||
:erlang.phash2({node(), self()})::16,
|
||||
:erlang.unique_integer()::16
|
||||
>>
|
||||
|
||||
Base.url_encode64(binary)
|
||||
end
|
||||
|
||||
defp log(%{log_level: false}, _msg_func), do: :ok
|
||||
defp log(%{log_level: level}, msg), do: Logger.log(level, msg)
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
defmodule Phoenix.Tracker.ShutdownHandler do
|
||||
@moduledoc false
|
||||
use GenServer
|
||||
|
||||
def start_link(tracker) do
|
||||
GenServer.start_link(__MODULE__, tracker, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def init(tracker) do
|
||||
Process.flag(:trap_exit, true)
|
||||
{:ok, tracker}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def terminate(_reason, tracker) do
|
||||
Phoenix.Tracker.graceful_permdown(tracker)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
687
phoenix/deps/phoenix_pubsub/lib/phoenix/tracker/state.ex
Normal file
687
phoenix/deps/phoenix_pubsub/lib/phoenix/tracker/state.ex
Normal file
@@ -0,0 +1,687 @@
|
||||
defmodule Phoenix.Tracker.State do
|
||||
@moduledoc false
|
||||
alias Phoenix.Tracker.{State, Clock}
|
||||
|
||||
@type name :: term
|
||||
@type topic :: String.t()
|
||||
@type key :: term
|
||||
@type meta :: map
|
||||
@type ets_id :: :ets.tid()
|
||||
@type clock :: pos_integer
|
||||
@type tag :: {name, clock}
|
||||
@type cloud :: MapSet.t()
|
||||
@type clouds :: %{name => cloud}
|
||||
@type context :: %{name => clock}
|
||||
@type values :: ets_id | :extracted | %{tag => {pid, topic, key, meta}}
|
||||
@type value :: {{topic, pid, key}, meta, tag}
|
||||
@type key_meta :: {key, meta}
|
||||
@type delta :: %State{mode: :delta}
|
||||
@type pid_lookup :: {pid, topic, key}
|
||||
|
||||
@type t :: %State{
|
||||
replica: name,
|
||||
context: context,
|
||||
clouds: clouds,
|
||||
values: values,
|
||||
pids: ets_id,
|
||||
mode: :unset | :delta | :normal,
|
||||
delta: :unset | delta,
|
||||
replicas: %{name => :up | :down},
|
||||
range: {context, context}
|
||||
}
|
||||
|
||||
defstruct replica: nil,
|
||||
context: %{},
|
||||
clouds: %{},
|
||||
values: nil,
|
||||
pids: nil,
|
||||
mode: :unset,
|
||||
delta: :unset,
|
||||
replicas: %{},
|
||||
range: {%{}, %{}}
|
||||
|
||||
@compile {:inline, tag: 1, clock: 1, put_tag: 2, delete_tag: 2, remove_delta_tag: 2}
|
||||
|
||||
@doc """
|
||||
Creates a new set for the replica.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Phoenix.Tracker.State.new(:replica1, :shard_name)
|
||||
%Phoenix.Tracker.State{...}
|
||||
|
||||
"""
|
||||
@spec new(name, atom) :: t
|
||||
def new(replica, shard_name) do
|
||||
reset_delta(%State{
|
||||
replica: replica,
|
||||
context: %{replica => 0},
|
||||
mode: :normal,
|
||||
values: :ets.new(shard_name, [:named_table, :protected, :ordered_set]),
|
||||
pids: :ets.new(:pids, [:duplicate_bag]),
|
||||
replicas: %{replica => :up}
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the causal context for the set.
|
||||
"""
|
||||
@spec clocks(t) :: {name, context}
|
||||
def clocks(%State{replica: rep, context: ctx} = state) do
|
||||
# Exclude down replicas from clocks as they are also not included in
|
||||
# deltas. Otherwise if this node knows of a down node X in permdown grace
|
||||
# period, another node Y which came up after X went down will keep
|
||||
# requesting full state from this node as the clock of Y will be dominated
|
||||
# by the clock of this node.
|
||||
{rep, Map.drop(ctx, down_replicas(state))}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a new element to the set.
|
||||
"""
|
||||
@spec join(t, pid, topic, key, meta) :: t
|
||||
def join(%State{} = state, pid, topic, key, meta \\ %{}) do
|
||||
add(state, pid, topic, key, meta)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates an element via leave and join.
|
||||
|
||||
Atomically updates ETS local entry.
|
||||
"""
|
||||
@spec leave_join(t, pid, topic, key, meta) :: t
|
||||
def leave_join(%State{} = state, pid, topic, key, meta) do
|
||||
# Produce remove-like delta
|
||||
[{{^topic, ^pid, ^key}, _meta, tag}] = :ets.lookup(state.values, {topic, pid, key})
|
||||
pruned_clouds = delete_tag(state.clouds, tag)
|
||||
new_delta = remove_delta_tag(state.delta, tag)
|
||||
state = bump_clock(%{state | clouds: pruned_clouds, delta: new_delta})
|
||||
|
||||
# Update ETS entry and produce add-like delta
|
||||
%State{} = state = bump_clock(state)
|
||||
tag = tag(state)
|
||||
true = :ets.insert(state.values, {{topic, pid, key}, meta, tag})
|
||||
put_in(state.delta.values[tag], {pid, topic, key, meta})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes an element from the set.
|
||||
"""
|
||||
@spec leave(t, pid, topic, key) :: t
|
||||
def leave(%State{pids: pids} = state, pid, topic, key) do
|
||||
pids
|
||||
|> :ets.match_object({pid, topic, key})
|
||||
|> case do
|
||||
[{^pid, ^topic, ^key}] -> remove(state, pid, topic, key)
|
||||
[] -> state
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes all elements from the set for the given pid.
|
||||
"""
|
||||
@spec leave(t, pid) :: t
|
||||
def leave(%State{pids: pids} = state, pid) do
|
||||
pids
|
||||
|> :ets.lookup(pid)
|
||||
|> Enum.reduce(state, fn {^pid, topic, key}, acc ->
|
||||
remove(acc, pid, topic, key)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of elements in the set belonging to an online replica.
|
||||
"""
|
||||
@spec online_list(t) :: [value]
|
||||
def online_list(%State{values: values} = state) do
|
||||
replicas = down_replicas(state)
|
||||
:ets.select(values, [{{:_, :_, {:"$1", :_}}, not_in(:"$1", replicas), [:"$_"]}])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of elements for the topic who belong to an online replica.
|
||||
"""
|
||||
@spec get_by_topic(t, topic) :: [key_meta]
|
||||
def get_by_topic(%State{values: values} = state, topic) do
|
||||
tracked_values(values, topic, down_replicas(state))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of elements for the topic who belong to an online replica.
|
||||
"""
|
||||
@spec get_by_key(t, topic, key) :: [key_meta]
|
||||
def get_by_key(%State{values: values} = state, topic, key) do
|
||||
case tracked_key(values, topic, key, down_replicas(state)) do
|
||||
[] -> []
|
||||
[_ | _] = metas -> metas
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs table lookup for tracked elements in the topic.
|
||||
|
||||
Filters out those present on downed replicas.
|
||||
"""
|
||||
def tracked_values(table, topic, down_replicas) do
|
||||
:ets.select(
|
||||
table,
|
||||
[
|
||||
{{{topic, :_, :"$1"}, :"$2", {:"$3", :_}}, not_in(:"$3", down_replicas),
|
||||
[{{:"$1", :"$2"}}]}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs table lookup for tracked key in the topic.
|
||||
|
||||
Filters out those present on downed replicas.
|
||||
"""
|
||||
def tracked_key(table, topic, key, down_replicas) do
|
||||
:ets.select(
|
||||
table,
|
||||
[
|
||||
{{{topic, :"$1", key}, :"$2", {:"$3", :_}}, not_in(:"$3", down_replicas),
|
||||
[{{:"$1", :"$2"}}]}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
defp not_in(_pos, []), do: []
|
||||
defp not_in(pos, replicas), do: [not: ors(pos, replicas)]
|
||||
defp ors(pos, [rep]), do: {:"=:=", pos, {rep}}
|
||||
defp ors(pos, [rep | rest]), do: {:or, {:"=:=", pos, {rep}}, ors(pos, rest)}
|
||||
|
||||
@doc """
|
||||
Returns the element matching the pid, topic, and key.
|
||||
"""
|
||||
@spec get_by_pid(t, pid, topic, key) :: value | nil
|
||||
def get_by_pid(%State{values: values}, pid, topic, key) do
|
||||
case :ets.lookup(values, {topic, pid, key}) do
|
||||
[] -> nil
|
||||
[one] -> one
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all elements for the pid.
|
||||
"""
|
||||
@spec get_by_pid(t, pid) :: [value]
|
||||
def get_by_pid(%State{pids: pids, values: values}, pid) do
|
||||
case :ets.lookup(pids, pid) do
|
||||
[] ->
|
||||
[]
|
||||
|
||||
matches ->
|
||||
:ets.select(
|
||||
values,
|
||||
Enum.map(matches, fn {^pid, topic, key} ->
|
||||
{{{topic, pid, key}, :_, :_}, [], [:"$_"]}
|
||||
end)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if set has a non-empty delta.
|
||||
"""
|
||||
@spec has_delta?(t) :: boolean
|
||||
def has_delta?(%State{delta: %State{clouds: clouds}}) do
|
||||
Enum.find(clouds, fn {_name, cloud} -> MapSet.size(cloud) != 0 end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Resets the set's delta.
|
||||
"""
|
||||
@spec reset_delta(t) :: t
|
||||
def reset_delta(%State{context: ctx, replica: replica} = state) do
|
||||
delta_ctx = Map.take(ctx, [replica])
|
||||
delta = %State{replica: replica, values: %{}, range: {delta_ctx, delta_ctx}, mode: :delta}
|
||||
%{state | delta: delta}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extracts the set's elements from ets into a mergeable list.
|
||||
|
||||
Used when merging two sets.
|
||||
"""
|
||||
|
||||
@spec extract(t, remote_ref :: name, context) :: t | {t, values}
|
||||
def extract(
|
||||
%State{mode: :delta, values: values, clouds: clouds} = state,
|
||||
remote_ref,
|
||||
remote_context
|
||||
) do
|
||||
{start_ctx, end_ctx} = state.range
|
||||
known_keys = Map.keys(remote_context)
|
||||
pruned_clouds = Map.take(clouds, known_keys)
|
||||
pruned_start = Map.take(start_ctx, known_keys)
|
||||
pruned_end = Map.take(end_ctx, known_keys)
|
||||
|
||||
map =
|
||||
Enum.reduce(values, [], fn
|
||||
{{^remote_ref, _clock}, _data}, acc ->
|
||||
acc
|
||||
|
||||
{{replica, _clock} = tag, data}, acc ->
|
||||
if Map.has_key?(remote_context, replica) do
|
||||
[{tag, data} | acc]
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|> :maps.from_list()
|
||||
|
||||
%{state | values: map, clouds: pruned_clouds, range: {pruned_start, pruned_end}}
|
||||
end
|
||||
|
||||
def extract(
|
||||
%State{mode: :normal, values: values, clouds: clouds} = state,
|
||||
remote_ref,
|
||||
remote_context
|
||||
) do
|
||||
known_keys = Map.keys(remote_context)
|
||||
pruned_clouds = Map.take(clouds, known_keys)
|
||||
pruned_context = Map.take(state.context, known_keys)
|
||||
# fn {{topic, pid, key}, meta, {replica, clock}} when replica !== remote_ref ->
|
||||
# {{replica, clock}, {pid, topic, key, meta}}
|
||||
# end
|
||||
ms = [
|
||||
{
|
||||
{{:"$1", :"$2", :"$3"}, :"$4", {:"$5", :"$6"}},
|
||||
[{:"=/=", :"$5", {:const, remote_ref}}],
|
||||
[{{{{:"$5", :"$6"}}, {{:"$2", :"$1", :"$3", :"$4"}}}}]
|
||||
}
|
||||
]
|
||||
|
||||
data =
|
||||
foldl(values, [], ms, fn {{replica, _} = tag, data}, acc ->
|
||||
if match?(%{^replica => _}, remote_context) do
|
||||
[{tag, data} | acc]
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
{%{
|
||||
state
|
||||
| clouds: pruned_clouds,
|
||||
context: pruned_context,
|
||||
pids: nil,
|
||||
values: nil,
|
||||
delta: :unset
|
||||
}, Map.new(data)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Merges two sets, or a delta into a set.
|
||||
|
||||
Returns a 3-tuple of the updated set, and the joined and left elements.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {s1, joined, left} =
|
||||
Phoenix.Tracker.State.merge(s1, Phoenix.Tracker.State.extract(s2))
|
||||
|
||||
{%Phoenix.Tracker.State{}, [...], [...]}
|
||||
"""
|
||||
@spec merge(local :: t, {remote :: t, values} | delta) ::
|
||||
{new_local :: t, joins :: [value], leaves :: [value]}
|
||||
def merge(%State{} = local, %State{mode: :delta, values: remote_map} = remote) do
|
||||
merge(local, remote, remote_map)
|
||||
end
|
||||
|
||||
def merge(%State{} = local, {%State{} = remote, remote_map}) do
|
||||
merge(local, remote, remote_map)
|
||||
end
|
||||
|
||||
defp merge(%State{} = local, remote, remote_map) do
|
||||
{added_pids, joins} = accumulate_joins(local, remote_map)
|
||||
{clouds, delta, leaves, removed_pids} = observe_removes(local, remote, remote_map)
|
||||
|
||||
# We diff ETS deletes and inserts, this way if there is an update
|
||||
# operation (leave + join) we handle it atomically via insert into
|
||||
# the :ordered_set table
|
||||
|
||||
added_value_keys = for {value_key, _meta, _tag} <- joins, do: value_key
|
||||
removed_value_keys = for {value_key, _meta, _tag} <- leaves, do: value_key
|
||||
value_keys_to_remove = removed_value_keys -- added_value_keys
|
||||
|
||||
pids_to_remove = removed_pids -- added_pids
|
||||
pids_to_add = added_pids -- removed_pids
|
||||
|
||||
for value_key <- value_keys_to_remove do
|
||||
:ets.delete(local.values, value_key)
|
||||
end
|
||||
|
||||
for pid <- pids_to_remove do
|
||||
:ets.match_delete(local.pids, pid)
|
||||
end
|
||||
|
||||
true = :ets.insert(local.values, joins)
|
||||
true = :ets.insert(local.pids, pids_to_add)
|
||||
|
||||
known_remote_context = Map.take(remote.context, Map.keys(local.context))
|
||||
ctx = Clock.upperbound(local.context, known_remote_context)
|
||||
|
||||
new_state =
|
||||
%{local | clouds: clouds, delta: delta}
|
||||
|> put_context(ctx)
|
||||
|> compact()
|
||||
|
||||
{new_state, joins, leaves}
|
||||
end
|
||||
|
||||
@spec accumulate_joins(t, values) :: joins :: {[pid_lookup], [values]}
|
||||
defp accumulate_joins(local, remote_map) do
|
||||
%State{context: context, clouds: clouds} = local
|
||||
|
||||
Enum.reduce(remote_map, {[], []}, fn {{replica, _} = tag, {pid, topic, key, meta}},
|
||||
{pids, adds} ->
|
||||
if not match?(%{^replica => _}, context) or in?(context, clouds, tag) do
|
||||
{pids, adds}
|
||||
else
|
||||
{[{pid, topic, key} | pids], [{{topic, pid, key}, meta, tag} | adds]}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec observe_removes(t, t, map) ::
|
||||
{clouds, delta, leaves :: [value], removed_pids :: [pid_lookup]}
|
||||
defp observe_removes(%State{values: values, delta: delta} = local, remote, remote_map) do
|
||||
unioned_clouds = union_clouds(local, remote)
|
||||
%State{context: remote_context, clouds: remote_clouds} = remote
|
||||
init = {unioned_clouds, delta, [], []}
|
||||
local_replica = local.replica
|
||||
# fn {_, _, {replica, _}} = result when replica != local_replica -> result end
|
||||
ms = [
|
||||
{
|
||||
{:_, :_, {:"$1", :_}},
|
||||
[{:"/=", :"$1", {:const, local_replica}}],
|
||||
[:"$_"]
|
||||
}
|
||||
]
|
||||
|
||||
foldl(values, init, ms, fn {{topic, pid, key}, _, tag} = el,
|
||||
{clouds, delta, leaves, removed_pids} ->
|
||||
if not match?(%{^tag => _}, remote_map) and in?(remote_context, remote_clouds, tag) do
|
||||
{delete_tag(clouds, tag), remove_delta_tag(delta, tag), [el | leaves],
|
||||
[{pid, topic, key} | removed_pids]}
|
||||
else
|
||||
{clouds, delta, leaves, removed_pids}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp put_tag(clouds, {name, _clock} = tag) do
|
||||
case clouds do
|
||||
%{^name => cloud} -> %{clouds | name => MapSet.put(cloud, tag)}
|
||||
_ -> Map.put(clouds, name, MapSet.new([tag]))
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_tag(clouds, {name, _clock} = tag) do
|
||||
case clouds do
|
||||
%{^name => cloud} -> %{clouds | name => MapSet.delete(cloud, tag)}
|
||||
_ -> clouds
|
||||
end
|
||||
end
|
||||
|
||||
defp union_clouds(%State{mode: :delta} = local, %State{} = remote) do
|
||||
Enum.reduce(remote.clouds, local.clouds, fn {name, remote_cloud}, acc ->
|
||||
Map.update(acc, name, remote_cloud, &MapSet.union(&1, remote_cloud))
|
||||
end)
|
||||
end
|
||||
|
||||
defp union_clouds(%State{mode: :normal, context: local_ctx} = local, %State{} = remote) do
|
||||
Enum.reduce(remote.clouds, local.clouds, fn {name, remote_cloud}, acc ->
|
||||
if Map.has_key?(local_ctx, name) do
|
||||
Map.update(acc, name, remote_cloud, &MapSet.union(&1, remote_cloud))
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def merge_deltas(
|
||||
%State{mode: :delta} = local,
|
||||
%State{mode: :delta, values: remote_values} = remote
|
||||
) do
|
||||
%{
|
||||
values: local_values,
|
||||
range: {local_start, local_end},
|
||||
context: local_context,
|
||||
clouds: local_clouds
|
||||
} = local
|
||||
|
||||
%{range: {remote_start, remote_end}, context: remote_context, clouds: remote_clouds} = remote
|
||||
|
||||
if (Clock.dominates_or_equal?(remote_end, local_start) and
|
||||
Clock.dominates_or_equal?(local_end, remote_start)) or
|
||||
(Clock.dominates_or_equal?(local_end, remote_start) and
|
||||
Clock.dominates_or_equal?(remote_end, local_start)) do
|
||||
new_start = Clock.lowerbound(local_start, remote_start)
|
||||
new_end = Clock.upperbound(local_end, remote_end)
|
||||
clouds = union_clouds(local, remote)
|
||||
|
||||
filtered_locals =
|
||||
for {tag, value} <- local_values,
|
||||
match?(%{^tag => _}, remote_values) or not in?(remote_context, remote_clouds, tag),
|
||||
do: {tag, value}
|
||||
|
||||
merged_vals =
|
||||
for {tag, value} <- remote_values,
|
||||
not match?(%{^tag => _}, local_values) and not in?(local_context, local_clouds, tag),
|
||||
do: {tag, value}
|
||||
|
||||
all_vals = filtered_locals ++ merged_vals
|
||||
|
||||
{:ok, %{local | clouds: clouds, values: Map.new(all_vals), range: {new_start, new_end}}}
|
||||
else
|
||||
{:error, :not_contiguous}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Marks a replica as up in the set and returns rejoined users.
|
||||
"""
|
||||
@spec replica_up(t, name) :: {t, joins :: [values], leaves :: []}
|
||||
def replica_up(%State{replicas: replicas, context: ctx} = state, replica) do
|
||||
{%{state | context: Map.put_new(ctx, replica, 0), replicas: Map.put(replicas, replica, :up)},
|
||||
replica_users(state, replica), []}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Marks a replica as down in the set and returns left users.
|
||||
"""
|
||||
@spec replica_down(t, name) :: {t, joins :: [], leaves :: [values]}
|
||||
def replica_down(%State{replicas: replicas} = state, replica) do
|
||||
{%{state | replicas: Map.put(replicas, replica, :down)}, [], replica_users(state, replica)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes all elements for replicas that are permanently gone.
|
||||
"""
|
||||
@spec remove_down_replicas(t, name) :: t
|
||||
def remove_down_replicas(
|
||||
%State{mode: :normal, context: ctx, values: values, pids: pids} = state,
|
||||
replica
|
||||
) do
|
||||
new_ctx = Map.delete(ctx, replica)
|
||||
# fn {key, _, {^replica, _}} -> key end
|
||||
ms = [{{:"$1", :_, {replica, :_}}, [], [:"$1"]}]
|
||||
|
||||
foldl(values, nil, ms, fn {topic, pid, key} = values_key, _ ->
|
||||
:ets.delete(values, values_key)
|
||||
:ets.match_delete(pids, {pid, topic, key})
|
||||
nil
|
||||
end)
|
||||
|
||||
new_clouds = Map.delete(state.clouds, replica)
|
||||
new_delta = remove_down_replicas(state.delta, replica)
|
||||
|
||||
%{state | context: new_ctx, clouds: new_clouds, delta: new_delta}
|
||||
end
|
||||
|
||||
def remove_down_replicas(%State{mode: :delta, range: range} = delta, replica) do
|
||||
{start_ctx, end_ctx} = range
|
||||
new_start = Map.delete(start_ctx, replica)
|
||||
new_end = Map.delete(end_ctx, replica)
|
||||
new_clouds = Map.delete(delta.clouds, replica)
|
||||
|
||||
new_vals =
|
||||
Enum.reduce(delta.values, delta.values, fn
|
||||
{{^replica, _clock} = tag, {_pid, _topic, _key, _meta}}, vals ->
|
||||
Map.delete(vals, tag)
|
||||
|
||||
{{_replica, _clock} = _tag, {_pid, _topic, _key, _meta}}, vals ->
|
||||
vals
|
||||
end)
|
||||
|
||||
%{delta | range: {new_start, new_end}, clouds: new_clouds, values: new_vals}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the dize of the delta.
|
||||
"""
|
||||
@spec delta_size(delta) :: pos_integer
|
||||
def delta_size(%State{mode: :delta, clouds: clouds, values: values}) do
|
||||
Enum.reduce(clouds, map_size(values), fn {_name, cloud}, sum ->
|
||||
sum + MapSet.size(cloud)
|
||||
end)
|
||||
end
|
||||
|
||||
@spec add(t, pid, topic, key, meta) :: t
|
||||
defp add(%State{} = state, pid, topic, key, meta) do
|
||||
state
|
||||
|> bump_clock()
|
||||
|> do_add(pid, topic, key, meta)
|
||||
end
|
||||
|
||||
defp do_add(%State{} = state, pid, topic, key, meta) do
|
||||
tag = tag(state)
|
||||
true = :ets.insert(state.values, {{topic, pid, key}, meta, tag})
|
||||
true = :ets.insert(state.pids, {pid, topic, key})
|
||||
put_in(state.delta.values[tag], {pid, topic, key, meta})
|
||||
end
|
||||
|
||||
@spec remove(t, pid, topic, key) :: t
|
||||
defp remove(%State{pids: pids, values: values} = state, pid, topic, key) do
|
||||
[{{^topic, ^pid, ^key}, _meta, tag}] = :ets.lookup(values, {topic, pid, key})
|
||||
1 = :ets.select_delete(values, [{{{topic, pid, key}, :_, :_}, [], [true]}])
|
||||
1 = :ets.select_delete(pids, [{{pid, topic, key}, [], [true]}])
|
||||
pruned_clouds = delete_tag(state.clouds, tag)
|
||||
new_delta = remove_delta_tag(state.delta, tag)
|
||||
|
||||
bump_clock(%{state | clouds: pruned_clouds, delta: new_delta})
|
||||
end
|
||||
|
||||
@spec remove_delta_tag(delta, tag) :: delta
|
||||
defp remove_delta_tag(%{mode: :delta, values: values, clouds: clouds} = delta, tag) do
|
||||
%{delta | clouds: put_tag(clouds, tag), values: Map.delete(values, tag)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compacts a sets causal history.
|
||||
|
||||
Called as needed and after merges.
|
||||
"""
|
||||
@spec compact(t) :: t
|
||||
def compact(%State{context: ctx, clouds: clouds} = state) do
|
||||
{new_ctx, new_clouds} =
|
||||
Enum.reduce(clouds, {ctx, clouds}, fn {name, cloud}, {ctx_acc, clouds_acc} ->
|
||||
{new_ctx, new_cloud} = do_compact(ctx_acc, Enum.sort(MapSet.to_list(cloud)))
|
||||
{new_ctx, Map.put(clouds_acc, name, MapSet.new(new_cloud))}
|
||||
end)
|
||||
|
||||
put_context(%{state | clouds: new_clouds}, new_ctx)
|
||||
end
|
||||
|
||||
@spec do_compact(context, sorted_cloud_list :: list) :: {context, cloud}
|
||||
defp do_compact(ctx, cloud) do
|
||||
Enum.reduce(cloud, {ctx, []}, fn {replica, clock} = tag, {ctx_acc, cloud_acc} ->
|
||||
case ctx_acc do
|
||||
%{^replica => ctx_clock} when ctx_clock + 1 == clock ->
|
||||
{%{ctx_acc | replica => clock}, cloud_acc}
|
||||
|
||||
%{^replica => ctx_clock} when ctx_clock >= clock ->
|
||||
{ctx_acc, cloud_acc}
|
||||
|
||||
_ when clock == 1 ->
|
||||
{Map.put(ctx_acc, replica, clock), cloud_acc}
|
||||
|
||||
_ ->
|
||||
{ctx_acc, [tag | cloud_acc]}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@compile {:inline, in?: 3, in_ctx?: 3, in_clouds?: 3}
|
||||
|
||||
defp in?(context, clouds, {replica, clock} = tag) do
|
||||
in_ctx?(context, replica, clock) or in_clouds?(clouds, replica, tag)
|
||||
end
|
||||
|
||||
defp in_ctx?(ctx, replica, clock) do
|
||||
case ctx do
|
||||
%{^replica => replica_clock} -> replica_clock >= clock
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp in_clouds?(clouds, replica, tag) do
|
||||
case clouds do
|
||||
%{^replica => cloud} -> MapSet.member?(cloud, tag)
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@spec tag(t) :: tag
|
||||
defp tag(%State{replica: rep} = state), do: {rep, clock(state)}
|
||||
|
||||
@spec clock(t) :: clock
|
||||
defp clock(%State{replica: rep, context: ctx}), do: Map.get(ctx, rep, 0)
|
||||
|
||||
@spec bump_clock(t) :: t
|
||||
defp bump_clock(
|
||||
%State{mode: :normal, replica: rep, clouds: clouds, context: ctx, delta: delta} = state
|
||||
) do
|
||||
new_clock = clock(state) + 1
|
||||
new_ctx = Map.put(ctx, rep, new_clock)
|
||||
|
||||
%{
|
||||
state
|
||||
| clouds: put_tag(clouds, {rep, new_clock}),
|
||||
delta: %{delta | clouds: put_tag(delta.clouds, {rep, new_clock})}
|
||||
}
|
||||
|> put_context(new_ctx)
|
||||
end
|
||||
|
||||
defp put_context(%State{delta: delta, replica: rep} = state, new_ctx) do
|
||||
{start_clock, end_clock} = delta.range
|
||||
new_end = Map.put(end_clock, rep, Map.get(new_ctx, rep, 0))
|
||||
%{state | context: new_ctx, delta: %{delta | range: {start_clock, new_end}}}
|
||||
end
|
||||
|
||||
@spec down_replicas(t) :: [name]
|
||||
defp down_replicas(%State{replicas: replicas}) do
|
||||
for {replica, :down} <- replicas, do: replica
|
||||
end
|
||||
|
||||
@spec replica_users(t, name) :: [value]
|
||||
defp replica_users(%State{values: values}, replica) do
|
||||
:ets.match_object(values, {:_, :_, {replica, :_}})
|
||||
end
|
||||
|
||||
@fold_batch_size 1000
|
||||
|
||||
defp foldl(table, initial, ms, func) do
|
||||
foldl(:ets.select(table, ms, @fold_batch_size), initial, func)
|
||||
end
|
||||
|
||||
defp foldl(:"$end_of_table", acc, _func), do: acc
|
||||
|
||||
defp foldl({objects, cont}, acc, func) do
|
||||
foldl(:ets.select(cont), Enum.reduce(objects, acc, func), func)
|
||||
end
|
||||
end
|
||||
87
phoenix/deps/phoenix_pubsub/mix.exs
Normal file
87
phoenix/deps/phoenix_pubsub/mix.exs
Normal file
@@ -0,0 +1,87 @@
|
||||
defmodule Phoenix.PubSub.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
@version "2.2.0"
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :phoenix_pubsub,
|
||||
version: @version,
|
||||
elixir: "~> 1.6",
|
||||
name: "Phoenix.PubSub",
|
||||
description: "Distributed PubSub and Presence platform",
|
||||
homepage_url: "http://www.phoenixframework.org",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
package: package(),
|
||||
docs: docs(),
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
defp elixirc_paths(:test), do: ["lib", "test/support"]
|
||||
defp elixirc_paths(_), do: ["lib"]
|
||||
|
||||
def application do
|
||||
[
|
||||
mod: {Phoenix.PubSub.Application, []},
|
||||
extra_applications: [:logger, :crypto]
|
||||
]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:ex_doc, ">= 0.0.0", only: :docs}
|
||||
]
|
||||
end
|
||||
|
||||
defp package do
|
||||
[
|
||||
maintainers: ["Chris McCord", "José Valim", "Alexander Songe", "Gary Rennie"],
|
||||
licenses: ["MIT"],
|
||||
links: %{github: "https://github.com/phoenixframework/phoenix_pubsub"},
|
||||
files: ~w(lib test/shared CHANGELOG.md LICENSE.md mix.exs README.md)
|
||||
]
|
||||
end
|
||||
|
||||
defp docs do
|
||||
[
|
||||
main: "Phoenix.PubSub",
|
||||
source_ref: "v#{@version}",
|
||||
source_url: "https://github.com/phoenixframework/phoenix_pubsub",
|
||||
before_closing_body_tag: %{
|
||||
html: """
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/mermaid@11.6.0/dist/mermaid.min.js"></script>
|
||||
<script>
|
||||
let initialized = false;
|
||||
|
||||
window.addEventListener("exdoc:loaded", () => {
|
||||
if (!initialized) {
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: document.body.className.includes("dark") ? "dark" : "default"
|
||||
});
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
let id = 0;
|
||||
for (const codeEl of document.querySelectorAll("pre code.mermaid")) {
|
||||
const preEl = codeEl.parentElement;
|
||||
const graphDefinition = codeEl.textContent;
|
||||
const graphEl = document.createElement("div");
|
||||
const graphId = "mermaid-graph-" + id++;
|
||||
|
||||
mermaid.render(graphId, graphDefinition).then(({svg, bindFunctions}) => {
|
||||
graphEl.innerHTML = svg;
|
||||
bindFunctions?.(graphEl);
|
||||
preEl.insertAdjacentElement("afterend", graphEl);
|
||||
preEl.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
""",
|
||||
epub: ""
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
198
phoenix/deps/phoenix_pubsub/test/shared/pubsub_test.exs
Normal file
198
phoenix/deps/phoenix_pubsub/test/shared/pubsub_test.exs
Normal file
@@ -0,0 +1,198 @@
|
||||
defmodule Phoenix.PubSubTest do
|
||||
@moduledoc """
|
||||
Sets up PubSub Adapter testcases.
|
||||
|
||||
## Usage
|
||||
|
||||
To test a PubSub adapter, set the `:test_adapter` on the `:phoenix_pubsub`
|
||||
configuration and require this file, ie:
|
||||
|
||||
# your_pubsub_adapter_test.exs
|
||||
Application.put_env(:phoenix_pubsub, :test_adapter, {Phoenix.PubSub.PG2, []})
|
||||
Code.require_file "../deps/phoenix_pubsub/test/shared/pubsub_test.exs", __DIR__
|
||||
|
||||
"""
|
||||
|
||||
use ExUnit.Case, async: true
|
||||
alias Phoenix.PubSub
|
||||
|
||||
defp subscribers(config, topic) do
|
||||
Registry.lookup(config.pubsub, topic)
|
||||
end
|
||||
|
||||
defp rpc(pid, func) do
|
||||
Agent.get(pid, fn :ok -> func.() end)
|
||||
end
|
||||
|
||||
defp spawn_pid do
|
||||
{:ok, pid} = Agent.start_link(fn -> :ok end)
|
||||
pid
|
||||
end
|
||||
|
||||
defmodule CustomDispatcher do
|
||||
def dispatch(entries, from, message) do
|
||||
for {pid, metadata} <- entries do
|
||||
send(pid, {:custom, metadata, from, message})
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
setup config do
|
||||
size = config[:pool_size] || 1
|
||||
registry_size = config[:registry_size] || config[:registry_pool_size] || config[:pool_size] || 1
|
||||
{adapter, adapter_opts} = Application.get_env(:phoenix_pubsub, :test_adapter)
|
||||
adapter_opts = [adapter: adapter, name: config.test, pool_size: size, registry_size: registry_size] ++ adapter_opts
|
||||
start_supervised!({Phoenix.PubSub, adapter_opts})
|
||||
|
||||
opts = %{
|
||||
pubsub: config.test,
|
||||
topic: to_string(config.test),
|
||||
pool_size: size,
|
||||
node: Phoenix.PubSub.node_name(config.test),
|
||||
adapter_name: Module.concat(config.test, "Adapter")
|
||||
}
|
||||
|
||||
{:ok, opts}
|
||||
end
|
||||
|
||||
test "node_name/1 returns the node name", config do
|
||||
assert is_atom(config.node) or is_binary(config.node)
|
||||
end
|
||||
|
||||
for size <- [1, 8] do
|
||||
@tag pool_size: size
|
||||
test "pool #{size}: subscribe and unsubscribe", config do
|
||||
pid = spawn_pid()
|
||||
assert subscribers(config, config.topic) |> length == 0
|
||||
assert rpc(pid, fn -> PubSub.subscribe(config.pubsub, config.topic) end)
|
||||
assert subscribers(config, config.topic) == [{pid, nil}]
|
||||
assert rpc(pid, fn -> PubSub.unsubscribe(config.pubsub, config.topic) end)
|
||||
assert subscribers(config, config.topic) |> length == 0
|
||||
end
|
||||
|
||||
@tag pool_size: size
|
||||
test "pool #{size}: broadcast/3 and broadcast!/3 publishes message to each subscriber",
|
||||
config do
|
||||
PubSub.subscribe(config.pubsub, config.topic)
|
||||
:ok = PubSub.broadcast(config.pubsub, config.topic, :ping)
|
||||
assert_receive :ping
|
||||
:ok = PubSub.broadcast!(config.pubsub, config.topic, :ping)
|
||||
assert_receive :ping
|
||||
end
|
||||
|
||||
@tag pool_size: size
|
||||
test "pool #{size}: broadcast/3 does not publish message to other topic subscribers",
|
||||
config do
|
||||
PubSub.subscribe(config.pubsub, "unknown")
|
||||
|
||||
Enum.each(0..10, fn _ ->
|
||||
rpc(spawn_pid(), fn -> PubSub.subscribe(config.pubsub, config.topic) end)
|
||||
end)
|
||||
|
||||
:ok = PubSub.broadcast(config.pubsub, config.topic, :ping)
|
||||
refute_received :ping
|
||||
end
|
||||
|
||||
@tag pool_size: size
|
||||
test "pool #{size}: broadcast_from/4 and broadcast_from!/4 skips sender", config do
|
||||
PubSub.subscribe(config.pubsub, config.topic)
|
||||
|
||||
PubSub.broadcast_from(config.pubsub, self(), config.topic, :ping)
|
||||
refute_received :ping
|
||||
|
||||
PubSub.broadcast_from!(config.pubsub, self(), config.topic, :ping)
|
||||
refute_received :ping
|
||||
end
|
||||
|
||||
@tag pool_size: size
|
||||
test "pool #{size}: unsubscribe on not subscribed topic noops", config do
|
||||
assert :ok = PubSub.unsubscribe(config.pubsub, config.topic)
|
||||
assert subscribers(config, config.topic) == []
|
||||
end
|
||||
|
||||
@tag pool_size: size
|
||||
test "pool #{size}: direct_broadcast sends to given node", config do
|
||||
PubSub.subscribe(config.pubsub, config.topic)
|
||||
|
||||
PubSub.direct_broadcast(config.node, config.pubsub, config.topic, :ping)
|
||||
assert_receive :ping
|
||||
|
||||
PubSub.direct_broadcast!(config.node, config.pubsub, config.topic, :ping)
|
||||
assert_receive :ping
|
||||
end
|
||||
|
||||
@tag pool_size: size
|
||||
test "pool #{size}: direct_broadcast sends to unknown node", config do
|
||||
PubSub.subscribe(config.pubsub, config.topic)
|
||||
|
||||
PubSub.direct_broadcast(:"IDONTKNOW@127.0.0.1", config.pubsub, config.topic, :ping)
|
||||
refute_received :ping
|
||||
|
||||
PubSub.direct_broadcast!(:"IDONTKNOW@127.0.0.1", config.pubsub, config.topic, :ping)
|
||||
refute_received :ping
|
||||
end
|
||||
|
||||
@tag pool_size: size
|
||||
test "pool #{size}: local_broadcast sends to the current node", config do
|
||||
PubSub.subscribe(config.pubsub, config.topic)
|
||||
|
||||
PubSub.local_broadcast(config.pubsub, config.topic, :ping)
|
||||
assert_receive :ping
|
||||
end
|
||||
|
||||
@tag pool_size: size
|
||||
test "pool #{size}: local_broadcast_from/5 skips sender", config do
|
||||
PubSub.subscribe(config.pubsub, config.topic)
|
||||
|
||||
PubSub.local_broadcast_from(config.pubsub, self(), config.topic, :ping)
|
||||
refute_received :ping
|
||||
end
|
||||
|
||||
@tag pool_size: size
|
||||
test "pool #{size}: with custom dispatching", %{topic: topic, test: test, node: node} do
|
||||
PubSub.subscribe(test, topic)
|
||||
PubSub.subscribe(test, topic, metadata: :special)
|
||||
|
||||
PubSub.broadcast(test, topic, :broadcast, CustomDispatcher)
|
||||
assert_receive {:custom, nil, :none, :broadcast}
|
||||
assert_receive {:custom, :special, :none, :broadcast}
|
||||
|
||||
PubSub.broadcast_from(test, self(), topic, :broadcast_from, CustomDispatcher)
|
||||
assert_receive {:custom, nil, pid, :broadcast_from} when pid == self()
|
||||
assert_receive {:custom, :special, pid, :broadcast_from} when pid == self()
|
||||
|
||||
PubSub.local_broadcast(test, topic, :local, CustomDispatcher)
|
||||
assert_receive {:custom, nil, :none, :local}
|
||||
assert_receive {:custom, :special, :none, :local}
|
||||
|
||||
PubSub.local_broadcast_from(test, self(), topic, :local_from, CustomDispatcher)
|
||||
assert_receive {:custom, nil, pid, :local_from} when pid == self()
|
||||
assert_receive {:custom, :special, pid, :local_from} when pid == self()
|
||||
|
||||
PubSub.direct_broadcast(node, test, topic, :direct, CustomDispatcher)
|
||||
assert_receive {:custom, nil, :none, :direct}
|
||||
assert_receive {:custom, :special, :none, :direct}
|
||||
end
|
||||
end
|
||||
|
||||
@tag pool_size: 4
|
||||
@tag registry_size: 2
|
||||
test "PubSub pool size can be configured separately from the Registry partitions",
|
||||
config do
|
||||
assert {:duplicate, 2, _} = :ets.lookup_element(config.pubsub, -2, 2)
|
||||
|
||||
assert :persistent_term.get(config.adapter_name) ==
|
||||
{config.adapter_name, :"#{config.adapter_name}_2", :"#{config.adapter_name}_3", :"#{config.adapter_name}_4"}
|
||||
end
|
||||
|
||||
@tag pool_size: 3
|
||||
test "Registry partitions are configured with the same pool size as PubSub if not specified",
|
||||
config do
|
||||
assert {:duplicate, 3, _} = :ets.lookup_element(config.pubsub, -2, 2)
|
||||
|
||||
assert :persistent_term.get(config.adapter_name) ==
|
||||
{config.adapter_name, :"#{config.adapter_name}_2", :"#{config.adapter_name}_3"}
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user