Skip to content

Commit e85d88c

Browse files
committed
Delegate redis connections to Redix
1 parent 1ea978c commit e85d88c

File tree

5 files changed

+208
-50
lines changed

5 files changed

+208
-50
lines changed

README.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,20 @@ children = [
1919
# ...,
2020
{Phoenix.PubSub,
2121
adapter: Phoenix.PubSub.Redis,
22-
host: "192.168.1.100",
22+
# redis_opts: [host: "example.com", port: 9999],
23+
redis_opts: "redis://localhost:6379",
2324
node_name: System.get_env("NODE")}
2425
```
2526

2627
Config Options
2728

28-
Option | Description | Default |
29-
:-----------------------| :------------------------------------------------------------------------ | :------------- |
30-
`:name` | The required name to register the PubSub processes, ie: `MyApp.PubSub` | |
31-
`:node_name` | The required and unique name of the node, ie: `System.get_env("NODE")` | |
32-
`:url` | The redis-server URL, ie: `redis://username:password@host:port` | |
33-
`:host` | The redis-server host IP | `"127.0.0.1"` |
34-
`:port` | The redis-server port | `6379` |
35-
`:password` | The redis-server password | `""` |
36-
`:compression_level` | Compression level applied to serialized terms (`0` - none, `9` - highest) | `0` |
37-
`:socket_opts` | The redis-server network layer options | `[]` |
29+
Option | Description | Default |
30+
:-----------------------| :----------------------------------------------------------------------------------------- | :------------- |
31+
`:name` | The required name to register the PubSub processes, ie: `MyApp.PubSub` | |
32+
`:node_name` | The required and unique name of the node, ie: `System.get_env("NODE")` | |
33+
`:compression_level` | Compression level applied to serialized terms (`0` - none, `9` - highest) | `0` |
34+
`:redis_pool_size` | The size of the redis connection pool. | `5` |
35+
`:redis_opts` | Redis connection opts. See: https://hexdocs.pm/redix/Redix.html#start_link/1-redis-options | |
3836

3937

4038
## License

lib/phoenix_pubsub_redis/redis.ex

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,50 @@ defmodule Phoenix.PubSub.Redis do
66
77
{Phoenix.PubSub,
88
adapter: Phoenix.PubSub.Redis,
9-
host: "192.168.1.100",
9+
redis_opts: [host: "192.168.1.100"],
1010
node_name: System.get_env("NODE")}
1111
1212
## Options
1313
1414
* `:url` - The url to the redis server ie: `redis://username:password@host:port`
1515
* `:name` - The required name to register the PubSub processes, ie: `MyApp.PubSub`
1616
* `:node_name` - The required name of the node, defaults to Erlang --sname flag. It must be unique.
17-
* `:host` - The redis-server host IP, defaults `"127.0.0.1"`
18-
* `:port` - The redis-server port, defaults `6379`
19-
* `:username` - The redis-server username
20-
* `:password` - The redis-server password, defaults `""`
21-
* `:ssl` - The redis-server ssl option, defaults `false`
2217
* `:redis_pool_size` - The size of the redis connection pool. Defaults `5`
2318
* `:compression_level` - Compression level applied to serialized terms - from `0` (no compression), to `9` (highest). Defaults `0`
24-
* `:socket_opts` - List of options that are passed to the network layer when connecting to the Redis server. Default `[]`
25-
* `:sentinel` - Redix sentinel configuration. Default to `nil`
19+
* `:redis_opts` - Redis connection opts. See: https://hexdocs.pm/redix/Redix.html#start_link/1-redis-options
2620
2721
"""
2822

2923
use Supervisor
3024

3125
@behaviour Phoenix.PubSub.Adapter
32-
@redis_pool_size 5
33-
@redis_opts [:host, :port, :username, :password, :database, :ssl, :socket_opts, :sentinel]
34-
@defaults [host: "127.0.0.1", port: 6379]
26+
27+
@schema NimbleOptions.new!(
28+
node_name: [
29+
type: :atom,
30+
doc: "The name of the node. Defaults to the Erlang `--sname` flag. Must be unique."
31+
],
32+
redis_pool_size: [
33+
type: :pos_integer,
34+
default: 5,
35+
doc: "The size of the Redis connection pool."
36+
],
37+
compression_level: [
38+
type: {:in, 0..9},
39+
default: 0,
40+
doc: "Compression level applied to serialized terms — `0` (none) to `9` (highest)."
41+
],
42+
redis_opts: [
43+
type: {:or, [:string, :keyword_list]},
44+
default: [],
45+
doc:
46+
"Redix connection options — either a Redis URL string or a keyword list. " <>
47+
"See `Redix.start_link/1` for more information."
48+
]
49+
)
50+
51+
# Using top-level configuration keys for Redis configuration is deprecated
52+
@redis_top_level_keys [:host, :port, :password, :database, :ssl, :socket_opts, :sentinel, :url]
3553

3654
## Adapter callbacks
3755

@@ -58,15 +76,14 @@ defmodule Phoenix.PubSub.Redis do
5876

5977
@impl true
6078
def init(opts) do
79+
opts = build_opts(opts)
6180
pubsub_name = Keyword.fetch!(opts, :name)
6281
adapter_name = Keyword.fetch!(opts, :adapter_name)
63-
compression_level = Keyword.get(opts, :compression_level, 0)
64-
65-
opts = handle_url_opts(opts)
66-
opts = Keyword.merge(@defaults, opts)
67-
redis_opts = Keyword.take(opts, @redis_opts)
82+
compression_level = Keyword.fetch!(opts, :compression_level)
83+
redis_opts = Keyword.fetch!(opts, :redis_opts)
84+
node_name = Keyword.fetch!(opts, :node_name)
85+
redis_pool_size = Keyword.fetch!(opts, :redis_pool_size)
6886

69-
node_name = opts[:node_name] || node()
7087
validate_node_name!(node_name)
7188

7289
:ets.new(adapter_name, [:public, :named_table, read_concurrency: true])
@@ -76,7 +93,7 @@ defmodule Phoenix.PubSub.Redis do
7693
pool_opts = [
7794
name: {:local, adapter_name},
7895
worker_module: Redix,
79-
size: opts[:redis_pool_size] || @redis_pool_size,
96+
size: redis_pool_size,
8097
max_overflow: 0
8198
]
8299

@@ -88,28 +105,53 @@ defmodule Phoenix.PubSub.Redis do
88105
Supervisor.init(children, strategy: :rest_for_one)
89106
end
90107

91-
defp handle_url_opts(opts) do
92-
if opts[:url] do
93-
merge_url_opts(opts)
94-
else
95-
opts
96-
end
108+
@doc false
109+
def build_opts(opts) do
110+
{internal, user_opts} =
111+
Keyword.split(opts, [:name, :adapter_name, :adapter, :pool_size, :registry_size])
112+
113+
{top_level_redis, user_opts} = Keyword.split(user_opts, @redis_top_level_keys)
114+
115+
validated =
116+
user_opts
117+
|> Keyword.put_new(:node_name, node())
118+
|> NimbleOptions.validate!(@schema)
119+
120+
redis_opts = build_redis_opts(top_level_redis, validated[:redis_opts])
121+
122+
internal
123+
|> Keyword.put(:node_name, validated[:node_name])
124+
|> Keyword.put(:compression_level, validated[:compression_level])
125+
|> Keyword.put(:redis_pool_size, validated[:redis_pool_size])
126+
|> Keyword.put(:redis_opts, redis_opts)
97127
end
98128

99-
defp merge_url_opts(opts) do
100-
info = URI.parse(opts[:url])
129+
defp build_redis_opts(top_level, redis_opts) do
130+
provided =
131+
Enum.filter([top_level != [] && :top_level_keys, redis_opts != [] && :redis_opts], & &1)
132+
133+
case provided do
134+
[] ->
135+
[]
136+
137+
[:top_level_keys] ->
138+
keys = top_level |> Keyword.keys() |> Enum.map_join(", ", &inspect/1)
101139

102-
user_opts =
103-
case String.split(info.userinfo || "", ":") do
104-
[""] -> []
105-
[username] -> [username: username]
106-
["", password] -> [password: password]
107-
[username, password] -> [username: username, password: password]
108-
end
140+
IO.warn(
141+
"Passing Redis connection keys at the top level is deprecated. " <>
142+
"Move #{keys} inside the :redis_opts option instead.",
143+
[]
144+
)
109145

110-
opts
111-
|> Keyword.merge(user_opts)
112-
|> Keyword.merge(host: info.host, port: info.port || @defaults[:port])
146+
if Keyword.keys(top_level) == [:url], do: top_level[:url], else: top_level
147+
148+
[:redis_opts] ->
149+
redis_opts
150+
151+
_multiple ->
152+
raise ArgumentError,
153+
"only one of :redis_opts or top-level Redis keys may be provided, not both"
154+
end
113155
end
114156

115157
defp validate_node_name!(node_name) do

lib/phoenix_pubsub_redis/redis_server.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ defmodule Phoenix.PubSub.RedisServer do
6161
node_name: node_name,
6262
redix_pid: nil,
6363
reconnect_timer: nil,
64-
redis_opts: [sync_connect: true] ++ redis_opts
64+
redis_opts: redis_opts
6565
}
6666

6767
{:ok, establish_conn(state)}
@@ -130,7 +130,7 @@ defmodule Phoenix.PubSub.RedisServer do
130130
end
131131

132132
defp establish_conn(%{redis_opts: redis_opts} = state) do
133-
case Redix.PubSub.start_link(redis_opts) do
133+
case start_redix_pubsub(redis_opts) do
134134
{:ok, redix_pid} ->
135135
establish_success(%{state | redix_pid: redix_pid})
136136

@@ -139,5 +139,13 @@ defmodule Phoenix.PubSub.RedisServer do
139139
end
140140
end
141141

142+
defp start_redix_pubsub(url) when is_binary(url) do
143+
Redix.PubSub.start_link(url, sync_connect: true)
144+
end
145+
146+
defp start_redix_pubsub(opts) do
147+
Redix.PubSub.start_link([sync_connect: true] ++ opts)
148+
end
149+
142150
defp redis_namespace(adapter_name), do: "phx:#{adapter_name}"
143151
end

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ defmodule PhoenixPubsubRedis.Mixfile do
3030
[
3131
phoenix_pubsub(),
3232
{:redix, "~> 1.0"},
33+
{:nimble_options, "~> 1.0"},
3334
{:ex_doc, ">= 0.0.0", only: :docs},
3435
{:poolboy, "~> 1.5.1 or ~> 1.6"}
3536
]

test/phoenix_pubsub_redis_test.exs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,118 @@ Application.put_env(
22
:phoenix_pubsub,
33
:test_adapter,
44
{Phoenix.PubSub.Redis,
5-
url: System.get_env("REDIS_URL", "redis://localhost:6379"),
5+
redis_opts: System.get_env("REDIS_URL", "redis://localhost:6379"),
66
node_name: :SAMPLE,
77
compression_level: 1}
88
)
99

1010
Code.require_file("#{Mix.Project.deps_paths()[:phoenix_pubsub]}/test/shared/pubsub_test.exs")
11+
12+
defmodule Phoenix.PubSub.RedisTest do
13+
use ExUnit.Case, async: true
14+
15+
import ExUnit.CaptureIO
16+
17+
alias Phoenix.PubSub.Redis
18+
19+
describe "build_opts/1" do
20+
test "build opts with defaults" do
21+
assert Map.new(Redis.build_opts(name: Phoenix.TestName, adapter_name: :adapter_name)) ==
22+
Map.new(
23+
node_name: node(),
24+
name: Phoenix.TestName,
25+
adapter_name: :adapter_name,
26+
redis_pool_size: 5,
27+
redis_opts: [],
28+
compression_level: 0
29+
)
30+
end
31+
32+
test "fill redis opts as-is" do
33+
assert Map.new(
34+
Redis.build_opts(
35+
name: Phoenix.TestName,
36+
adapter_name: :adapter_name,
37+
redis_opts: [
38+
host: "example.com",
39+
port: 5000,
40+
password: "password",
41+
database: 1,
42+
ssl: true,
43+
socket_opts: [verify: :no_verify],
44+
sentinel: [
45+
sentinels: [
46+
"redis://sent1.example.com:26379",
47+
"redis://sent2.example.com:26379"
48+
],
49+
group: "main"
50+
]
51+
]
52+
)
53+
) ==
54+
Map.new(
55+
node_name: node(),
56+
name: Phoenix.TestName,
57+
adapter_name: :adapter_name,
58+
redis_pool_size: 5,
59+
compression_level: 0,
60+
redis_opts: [
61+
host: "example.com",
62+
port: 5000,
63+
password: "password",
64+
database: 1,
65+
ssl: true,
66+
socket_opts: [verify: :no_verify],
67+
sentinel: [
68+
sentinels: [
69+
"redis://sent1.example.com:26379",
70+
"redis://sent2.example.com:26379"
71+
],
72+
group: "main"
73+
]
74+
]
75+
)
76+
end
77+
78+
test "warns when top-level Redis keys are used" do
79+
warning =
80+
capture_io(:stderr, fn ->
81+
Redis.build_opts(name: Phoenix.TestName, adapter_name: :adapter_name, host: "localhost")
82+
end)
83+
84+
assert warning =~ "Passing Redis connection keys at the top level is deprecated"
85+
assert warning =~ ":host"
86+
end
87+
88+
test "raises when both top-level keys and redis_opts are provided" do
89+
assert_raise ArgumentError,
90+
"only one of :redis_opts or top-level Redis keys may be provided, not both",
91+
fn ->
92+
Redis.build_opts(
93+
name: Phoenix.TestName,
94+
adapter_name: :adapter_name,
95+
host: "example.com",
96+
redis_opts: [password: "password"]
97+
)
98+
end
99+
end
100+
101+
test "url string is passed through directly as redis_opts" do
102+
assert Map.new(
103+
Redis.build_opts(
104+
name: Phoenix.TestName,
105+
adapter_name: :adapter_name,
106+
redis_opts: "rediss://username:password@example.com:5000/1"
107+
)
108+
) ==
109+
Map.new(
110+
node_name: node(),
111+
name: Phoenix.TestName,
112+
adapter_name: :adapter_name,
113+
redis_pool_size: 5,
114+
compression_level: 0,
115+
redis_opts: "rediss://username:password@example.com:5000/1"
116+
)
117+
end
118+
end
119+
end

0 commit comments

Comments
 (0)