redisent 0.0.1 → 0.1.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gems +2 -0
- data/{README → README.md} +8 -0
- data/lib/redisent.rb +137 -12
- data/makefile +4 -0
- data/redisent.gemspec +14 -0
- data/test/all.rb +95 -0
- data/test/helper.rb +1 -0
- metadata +30 -22
- data/test/redisent_test.rb +0 -38
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a94d9eca24c1582cf8ed550514a0a4d4e9b5d3fa
|
4
|
+
data.tar.gz: 8901e7d03da59f40b9e377ac87fd456819fd3120
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3621af7ae4f480b177a0f3c4c221f754fef2cb574939ed308f2655a1ad725475f7bcba0e69b46ce284a3d4424808d35fa754eae5047cb55c1d3a982f69900d19
|
7
|
+
data.tar.gz: 8a2478cbbd51e01196eae3c22d78d73fc90ff3e1774977f6b5ddd5a6bf63388551496e8dacb4a5c02ff0cb23a08342cef422d6894018e2cc1014ed4726881d22
|
data/.gems
ADDED
data/{README → README.md}
RENAMED
@@ -35,6 +35,14 @@ redis = Redisent.new(sentinels, master, options)
|
|
35
35
|
If the sentinels can't be reached, or if there is no master available,
|
36
36
|
you will get the exception `Redis::CannotConnectError`.
|
37
37
|
|
38
|
+
## Failover
|
39
|
+
|
40
|
+
In case of a failover, it is important that the clients don't engage
|
41
|
+
with the failed master even if it's restored. For that reason, clients
|
42
|
+
must connect to the Redis sentinels in order to get the address of the
|
43
|
+
promoted master, and the way to accomplish that is by using
|
44
|
+
Redisent.new each time a reconnection is needed.
|
45
|
+
|
38
46
|
## Installation
|
39
47
|
|
40
48
|
You can install it using rubygems:
|
data/lib/redisent.rb
CHANGED
@@ -1,23 +1,148 @@
|
|
1
|
-
require "
|
1
|
+
require "redic"
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
class Redisent
|
4
|
+
class UnreachableHosts < ArgumentError; end
|
5
|
+
class UnknownMaster < ArgumentError; end
|
6
|
+
|
7
|
+
ECONN = [
|
8
|
+
Errno::ECONNREFUSED,
|
9
|
+
Errno::EINVAL,
|
10
|
+
]
|
11
|
+
|
12
|
+
attr_reader :hosts
|
13
|
+
attr_reader :healthy
|
14
|
+
attr_reader :invalid
|
15
|
+
attr_reader :unknown
|
16
|
+
attr_reader :prime
|
17
|
+
attr_reader :scout
|
18
|
+
|
19
|
+
def initialize(hosts:, name:, client: Redic, auth: nil)
|
20
|
+
@name = name
|
21
|
+
@auth = auth
|
22
|
+
|
23
|
+
# Client library
|
24
|
+
@client = client
|
25
|
+
|
26
|
+
# Hosts according to availability
|
27
|
+
@healthy = []
|
28
|
+
@invalid = []
|
29
|
+
@unknown = []
|
30
|
+
|
31
|
+
# Last known healthy hosts
|
32
|
+
@hosts = hosts
|
33
|
+
|
34
|
+
# Primary client
|
35
|
+
@prime = @client.new
|
36
|
+
|
37
|
+
# Scout client
|
38
|
+
@scout = @client.new
|
39
|
+
|
40
|
+
explore!
|
41
|
+
end
|
42
|
+
|
43
|
+
def url
|
44
|
+
@prime.url
|
45
|
+
end
|
46
|
+
|
47
|
+
private def explore!
|
48
|
+
@unknown = []
|
49
|
+
@invalid = []
|
50
|
+
@healthy = []
|
51
|
+
|
52
|
+
@hosts.each do |host|
|
6
53
|
begin
|
7
|
-
|
8
|
-
|
9
|
-
|
54
|
+
@scout.configure(sentinel_url(host))
|
55
|
+
|
56
|
+
sentinels = @scout.call("SENTINEL", "sentinels", @name)
|
57
|
+
|
58
|
+
if RuntimeError === sentinels
|
59
|
+
unknown.push(host)
|
60
|
+
else
|
61
|
+
healthy.push(host)
|
62
|
+
|
63
|
+
sentinels.each do |sentinel|
|
64
|
+
info = Hash[*sentinel]
|
65
|
+
|
66
|
+
healthy.push(sprintf("%s:%s", info["ip"], info["port"]))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
@scout.quit
|
71
|
+
|
72
|
+
rescue *ECONN
|
73
|
+
invalid.push(host)
|
10
74
|
end
|
11
75
|
end
|
12
76
|
|
13
|
-
|
77
|
+
if healthy.any?
|
78
|
+
@hosts.replace(healthy)
|
79
|
+
@prime.configure(master)
|
80
|
+
return true
|
81
|
+
end
|
82
|
+
|
83
|
+
if invalid.any?
|
84
|
+
raise UnreachableHosts, invalid
|
85
|
+
end
|
86
|
+
|
87
|
+
if unknown.any?
|
88
|
+
raise UnknownMaster, @name
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def call(*args)
|
93
|
+
forward do
|
94
|
+
@prime.call(*args)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def call!(*args)
|
99
|
+
forward do
|
100
|
+
@prime.call!(*args)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def queue(*args)
|
105
|
+
@prime.queue(*args)
|
14
106
|
end
|
15
107
|
|
16
|
-
def
|
17
|
-
|
108
|
+
def commit
|
109
|
+
buffer = @prime.buffer
|
18
110
|
|
19
|
-
|
111
|
+
forward do
|
112
|
+
@prime.buffer.replace(buffer)
|
113
|
+
@prime.commit
|
114
|
+
end
|
115
|
+
end
|
20
116
|
|
21
|
-
|
117
|
+
def forward
|
118
|
+
yield
|
119
|
+
rescue
|
120
|
+
explore!
|
121
|
+
retry
|
122
|
+
end
|
123
|
+
|
124
|
+
private def sentinel_url(host)
|
125
|
+
sprintf("redis://%s", host)
|
126
|
+
end
|
127
|
+
|
128
|
+
private def redis_url(host)
|
129
|
+
if @auth then
|
130
|
+
sprintf("redis://:%s@%s", @auth, host)
|
131
|
+
else
|
132
|
+
sprintf("redis://%s", host)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
private def master
|
137
|
+
hosts.each do |host|
|
138
|
+
begin
|
139
|
+
@scout.configure(sentinel_url(host))
|
140
|
+
ip, port = @scout.call("SENTINEL", "get-master-addr-by-name", @name)
|
141
|
+
|
142
|
+
break redis_url(sprintf("%s:%s", ip, port))
|
143
|
+
rescue *ECONN
|
144
|
+
$stderr.puts($!.inspect)
|
145
|
+
end
|
146
|
+
end
|
22
147
|
end
|
23
148
|
end
|
data/makefile
ADDED
data/redisent.gemspec
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "redisent"
|
3
|
+
s.version = "0.1.0.alpha"
|
4
|
+
s.summary = "Sentinel aware Redis client."
|
5
|
+
s.description = "Redisent is a wrapper for the Redis client that fetches configuration details from sentinels."
|
6
|
+
s.authors = ["Michel Martens"]
|
7
|
+
s.email = ["michel@soveran.com"]
|
8
|
+
s.homepage = "https://github.com/soveran/redisent"
|
9
|
+
s.files = `git ls-files`.split("\n")
|
10
|
+
s.license = "MIT"
|
11
|
+
|
12
|
+
s.add_dependency "redic", "~> 1.5"
|
13
|
+
s.add_development_dependency "cutest", "~> 0"
|
14
|
+
end
|
data/test/all.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require_relative "helper"
|
2
|
+
|
3
|
+
require "stringio"
|
4
|
+
|
5
|
+
module Silencer
|
6
|
+
@output = nil
|
7
|
+
|
8
|
+
def self.start
|
9
|
+
$olderr = $stderr
|
10
|
+
$stderr = StringIO.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.stop
|
14
|
+
@output = $stderr.string
|
15
|
+
$stderr = $olderr
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.output
|
19
|
+
@output
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
SENTINEL_HOSTS = [
|
24
|
+
"127.0.0.1:27000",
|
25
|
+
"127.0.0.1:27001",
|
26
|
+
"127.0.0.1:27002",
|
27
|
+
"127.0.0.1:27003",
|
28
|
+
"127.0.0.1:27004",
|
29
|
+
]
|
30
|
+
|
31
|
+
SENTINEL_BAD_HOSTS = SENTINEL_HOSTS[0,1]
|
32
|
+
SENTINEL_GOOD_HOSTS = SENTINEL_HOSTS[1,4]
|
33
|
+
|
34
|
+
prepare do
|
35
|
+
c = Redic.new
|
36
|
+
|
37
|
+
SENTINEL_GOOD_HOSTS.each do |host|
|
38
|
+
c.configure(sprintf("redis://%s", host))
|
39
|
+
|
40
|
+
c.call("SENTINEL", "monitor", "master-6379", "127.0.0.1", "6379", "3")
|
41
|
+
c.call("QUIT")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
setup do
|
46
|
+
Redic.new.tap do |c|
|
47
|
+
c.call("FLUSHDB")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
test "invalid hosts" do
|
52
|
+
assert_raise(Redisent::UnreachableHosts) do
|
53
|
+
Redisent.new(hosts: SENTINEL_BAD_HOSTS, name: "master-6379")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
test "invalid master" do
|
58
|
+
assert_raise(Redisent::UnknownMaster) do
|
59
|
+
Redisent.new(hosts: SENTINEL_GOOD_HOSTS, name: "master-6380")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
setup do
|
64
|
+
Redisent.new(hosts: SENTINEL_GOOD_HOSTS, name: "master-6379")
|
65
|
+
end
|
66
|
+
|
67
|
+
test "call" do |c|
|
68
|
+
assert_equal "PONG", c.call("PING")
|
69
|
+
end
|
70
|
+
|
71
|
+
test "call!" do |c|
|
72
|
+
assert_equal "PONG", c.call("PING")
|
73
|
+
end
|
74
|
+
|
75
|
+
test "queue/commit" do |c|
|
76
|
+
assert_equal [["PING"]], c.queue("PING")
|
77
|
+
assert_equal [["PING"], ["PING"]], c.queue("PING")
|
78
|
+
assert_equal ["PONG", "PONG"], c.commit
|
79
|
+
end
|
80
|
+
|
81
|
+
test "retry on connection failures" do |c|
|
82
|
+
assert_equal "PONG", c.call("PING")
|
83
|
+
|
84
|
+
# Simulate a server disconnection.
|
85
|
+
c.prime.configure(sprintf("redis://%s", SENTINEL_BAD_HOSTS.first))
|
86
|
+
|
87
|
+
assert_equal "PONG", c.call("PING")
|
88
|
+
|
89
|
+
# Simulate a server disconnection.
|
90
|
+
c.prime.configure(sprintf("redis://%s", SENTINEL_BAD_HOSTS.first))
|
91
|
+
|
92
|
+
assert_equal [["PING"]], c.queue("PING")
|
93
|
+
assert_equal [["PING"], ["PING"]], c.queue("PING")
|
94
|
+
assert_equal ["PONG", "PONG"], c.commit
|
95
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "../lib/redisent"
|
metadata
CHANGED
@@ -1,38 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redisent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.1.0.alpha
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Michel Martens
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2017-01-23 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
16
|
-
requirement:
|
17
|
-
none: false
|
14
|
+
name: redic
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - "~>"
|
20
18
|
- !ruby/object:Gem::Version
|
21
|
-
version: '
|
19
|
+
version: '1.5'
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
|
-
version_requirements:
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
25
27
|
- !ruby/object:Gem::Dependency
|
26
28
|
name: cutest
|
27
|
-
requirement:
|
28
|
-
none: false
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
29
30
|
requirements:
|
30
|
-
- -
|
31
|
+
- - "~>"
|
31
32
|
- !ruby/object:Gem::Version
|
32
33
|
version: '0'
|
33
34
|
type: :development
|
34
35
|
prerelease: false
|
35
|
-
version_requirements:
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
36
41
|
description: Redisent is a wrapper for the Redis client that fetches configuration
|
37
42
|
details from sentinels.
|
38
43
|
email:
|
@@ -41,33 +46,36 @@ executables: []
|
|
41
46
|
extensions: []
|
42
47
|
extra_rdoc_files: []
|
43
48
|
files:
|
49
|
+
- ".gems"
|
44
50
|
- LICENSE
|
45
|
-
- README
|
51
|
+
- README.md
|
46
52
|
- lib/redisent.rb
|
47
|
-
-
|
53
|
+
- makefile
|
54
|
+
- redisent.gemspec
|
55
|
+
- test/all.rb
|
56
|
+
- test/helper.rb
|
48
57
|
homepage: https://github.com/soveran/redisent
|
49
58
|
licenses:
|
50
59
|
- MIT
|
60
|
+
metadata: {}
|
51
61
|
post_install_message:
|
52
62
|
rdoc_options: []
|
53
63
|
require_paths:
|
54
64
|
- lib
|
55
65
|
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
-
none: false
|
57
66
|
requirements:
|
58
|
-
- -
|
67
|
+
- - ">="
|
59
68
|
- !ruby/object:Gem::Version
|
60
69
|
version: '0'
|
61
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
-
none: false
|
63
71
|
requirements:
|
64
|
-
- -
|
72
|
+
- - ">"
|
65
73
|
- !ruby/object:Gem::Version
|
66
|
-
version:
|
74
|
+
version: 1.3.1
|
67
75
|
requirements: []
|
68
76
|
rubyforge_project:
|
69
|
-
rubygems_version:
|
77
|
+
rubygems_version: 2.4.5.1
|
70
78
|
signing_key:
|
71
|
-
specification_version:
|
79
|
+
specification_version: 4
|
72
80
|
summary: Sentinel aware Redis client.
|
73
81
|
test_files: []
|
data/test/redisent_test.rb
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
require File.expand_path("../lib/redisent", File.dirname(__FILE__))
|
2
|
-
|
3
|
-
test "basics" do |redis|
|
4
|
-
redis = Redisent.new(
|
5
|
-
["redis://localhost:27378/",
|
6
|
-
"redis://localhost:27379/",
|
7
|
-
"redis://localhost:27380/",
|
8
|
-
"redis://localhost:27381/"],
|
9
|
-
"server-1", :timeout => 5)
|
10
|
-
|
11
|
-
redis.set("foo", 1)
|
12
|
-
|
13
|
-
assert_equal "1", redis.get("foo")
|
14
|
-
assert_equal "6379", redis.info["tcp_port"]
|
15
|
-
assert_equal 5.0, redis.client.timeout
|
16
|
-
end
|
17
|
-
|
18
|
-
test "no available sentinel" do
|
19
|
-
assert_raise Redis::CannotConnectError do
|
20
|
-
redis = Redisent.new(
|
21
|
-
["redis://localhost:27478/",
|
22
|
-
"redis://localhost:27479/",
|
23
|
-
"redis://localhost:27480/",
|
24
|
-
"redis://localhost:27481/"],
|
25
|
-
"server-1")
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
test "no available master" do
|
30
|
-
assert_raise Redis::CannotConnectError do
|
31
|
-
redis = Redisent.new(
|
32
|
-
["redis://localhost:27478/",
|
33
|
-
"redis://localhost:27479/",
|
34
|
-
"redis://localhost:27480/",
|
35
|
-
"redis://localhost:27481/"],
|
36
|
-
"server-2")
|
37
|
-
end
|
38
|
-
end
|