Skip to content

Commit b4d7869

Browse files
committed
Add an inherit_socket option to disable fork safety check
When `inherit_socket` is set to true, we disable safety check that prevents a forked child from sharing a socket with its parent; this is potentially useful in order to prohibit excessive connection churn on redis when ALL of the following are met: - many short-lived forked children of one process need to talk to redis, AND - your own code prevents the parent process from using the redis connection while a child is alive Improper use of this option will result in corrupted and/or incorrect responses. `inherit_socket` is disabled by default.
1 parent 08e1346 commit b4d7869

File tree

4 files changed

+89
-4
lines changed

4 files changed

+89
-4
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
HyperLogLog (Redis 2.8.9, #432)
66
* Added support for `bitpos` command (Redis 2.9.11, #412)
77
* Added support for automatically reconnecting in forked child (#414)
8+
* Added support for expert-mode option `inherit_socket`, which disables
9+
fork-safety check and enables a socket to be shared with a child
10+
process (#409)
811
* Added MRI Ruby 2.1.0 to CI
912
* Fix truncation of meaningful information in certain timeouts (#430)
1013
* Fix handling of connection retries that could result in retry loop (#415)

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,18 @@ end
158158
# => 1
159159
```
160160

161+
## Expert-Mode Options
162+
163+
- `inherit_socket: true`: disable safety check that prevents a forked child
164+
from sharing a socket with its parent; this is potentially useful in order to mitigate connection churn when:
165+
- many short-lived forked children of one process need to talk
166+
to redis, AND
167+
- your own code prevents the parent process from using the redis
168+
connection while a child is alive
169+
170+
Improper use of `inherit_socket` will result in corrupted and/or incorrect
171+
responses.
172+
161173
## Alternate drivers
162174

163175
By default, redis-rb uses Ruby's socket library to talk with Redis.

lib/redis/client.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class Client
1616
:db => 0,
1717
:driver => nil,
1818
:id => nil,
19-
:tcp_keepalive => 0
19+
:tcp_keepalive => 0,
20+
:inherit_socket => false
2021
}
2122

2223
def options
@@ -59,6 +60,10 @@ def driver
5960
@options[:driver]
6061
end
6162

63+
def inherit_socket?
64+
@options[:inherit_socket]
65+
end
66+
6267
attr_accessor :logger
6368
attr_reader :connection
6469
attr_reader :command_map
@@ -310,10 +315,11 @@ def ensure_connected
310315
tries += 1
311316

312317
if connected?
313-
if Process.pid != @pid
318+
unless inherit_socket? || Process.pid == @pid
314319
raise InheritedError,
315320
"Tried to use a connection from a child process without reconnecting. " +
316-
"You need to reconnect to Redis after forking."
321+
"You need to reconnect to Redis after forking " +
322+
"or set :inherit_socket to true."
317323
end
318324
else
319325
connect
@@ -372,7 +378,7 @@ def _parse_options(options)
372378

373379
# Use default when option is not specified or nil
374380
defaults.keys.each do |key|
375-
options[key] ||= defaults[key]
381+
options[key] = defaults[key] if options[key].nil?
376382
end
377383

378384
if options[:path]

test/fork_safety_test.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# encoding: UTF-8
2+
3+
require File.expand_path("helper", File.dirname(__FILE__))
4+
5+
class TestForkSafety < Test::Unit::TestCase
6+
7+
include Helper::Client
8+
9+
driver(:ruby, :hiredis) do
10+
def test_fork_safety
11+
redis = Redis.new(OPTIONS)
12+
redis.set "foo", 1
13+
14+
child_pid = fork do
15+
begin
16+
# InheritedError triggers a reconnect,
17+
# so we need to disable reconnects to force
18+
# the exception bubble up
19+
redis.without_reconnect do
20+
redis.set "foo", 2
21+
end
22+
rescue Redis::InheritedError
23+
exit 127
24+
end
25+
end
26+
27+
_, status = Process.wait2(child_pid)
28+
29+
assert_equal 127, status.exitstatus
30+
assert_equal "1", redis.get("foo")
31+
32+
rescue NotImplementedError => error
33+
raise unless error.message =~ /fork is not available/
34+
skip(error.message)
35+
end
36+
37+
def test_fork_safety_with_enabled_inherited_socket
38+
redis = Redis.new(OPTIONS.merge(:inherit_socket => true))
39+
redis.set "foo", 1
40+
41+
child_pid = fork do
42+
begin
43+
# InheritedError triggers a reconnect,
44+
# so we need to disable reconnects to force
45+
# the exception bubble up
46+
redis.without_reconnect do
47+
redis.set "foo", 2
48+
end
49+
rescue Redis::InheritedError
50+
exit 127
51+
end
52+
end
53+
54+
_, status = Process.wait2(child_pid)
55+
56+
assert_equal 0, status.exitstatus
57+
assert_equal "2", redis.get("foo")
58+
59+
rescue NotImplementedError => error
60+
raise unless error.message =~ /fork is not available/
61+
skip(error.message)
62+
end
63+
end
64+
end

0 commit comments

Comments
 (0)