redisent 0.0.1 → 0.1.0.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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