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.
@@ -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
@@ -0,0 +1,2 @@
1
+ redic -v 1.5.0
2
+ cutest -v 1.2.3
@@ -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:
@@ -1,23 +1,148 @@
1
- require "redis"
1
+ require "redic"
2
2
 
3
- module Redisent
4
- def self.new(sentinels, master, options = {})
5
- sentinels.each do |sentinel|
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
- master = find_master(sentinel, master, options)
8
- return master if master
9
- rescue Redis::CannotConnectError
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
- raise Redis::CannotConnectError
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 self.find_master(sentinel, master, options)
17
- redis = Redis.new(url: sentinel)
108
+ def commit
109
+ buffer = @prime.buffer
18
110
 
19
- host, port = redis.sentinel("get-master-addr-by-name", master)
111
+ forward do
112
+ @prime.buffer.replace(buffer)
113
+ @prime.commit
114
+ end
115
+ end
20
116
 
21
- Redis.new(options.merge(:host => host, :port => port))
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
@@ -0,0 +1,4 @@
1
+ .PHONY: test
2
+
3
+ test:
4
+ cutest -r ./test/helper.rb ./test/*.rb
@@ -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
@@ -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
@@ -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.1
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: 2012-10-09 00:00:00.000000000 Z
11
+ date: 2017-01-23 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
- name: redis
16
- requirement: &2155997400 !ruby/object:Gem::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: '0'
19
+ version: '1.5'
22
20
  type: :runtime
23
21
  prerelease: false
24
- version_requirements: *2155997400
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: &2155996720 !ruby/object:Gem::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: *2155996720
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
- - test/redisent_test.rb
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: '0'
74
+ version: 1.3.1
67
75
  requirements: []
68
76
  rubyforge_project:
69
- rubygems_version: 1.8.11
77
+ rubygems_version: 2.4.5.1
70
78
  signing_key:
71
- specification_version: 3
79
+ specification_version: 4
72
80
  summary: Sentinel aware Redis client.
73
81
  test_files: []
@@ -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