Skip to content

Commit a6c6c1a

Browse files
committed
Initial rework of sentinels client.
1 parent 6ec3e84 commit a6c6c1a

File tree

7 files changed

+200
-77
lines changed

7 files changed

+200
-77
lines changed

.github/workflows/sentinel.conf

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
port 26379
2+
sentinel monitor mymaster redis-master 6379 1
3+
sentinel down-after-milliseconds mymaster 1000
4+
sentinel failover-timeout mymaster 1000
5+
sentinel parallel-syncs mymaster 1

.github/workflows/test-sentinel.yaml

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: Test
2+
3+
on: [push, pull_request]
4+
5+
permissions:
6+
contents: read
7+
8+
env:
9+
CONSOLE_OUTPUT: XTerm
10+
ASYNC_REDIS_MASTER: "redis-master:6379"
11+
ASYNC_REDIS_SLAVE: "redis-slave:6379"
12+
ASYNC_REDIS_SENTINEL: "redis-sentinel:26379"
13+
14+
jobs:
15+
test:
16+
name: ${{matrix.ruby}} on ${{matrix.os}}
17+
runs-on: ${{matrix.os}}-latest
18+
continue-on-error: ${{matrix.experimental}}
19+
20+
services:
21+
redis-master:
22+
image: redis
23+
options: >-
24+
--health-cmd "redis-cli ping"
25+
--health-interval 10s
26+
--health-timeout 5s
27+
--health-retries 5
28+
ports:
29+
- 6379
30+
redis-slave:
31+
image: redis
32+
options: >-
33+
--health-cmd "redis-cli ping"
34+
--health-interval 10s
35+
--health-timeout 5s
36+
--health-retries 5
37+
ports:
38+
- 6379
39+
redis-sentinel:
40+
image: redis
41+
volumes:
42+
- ./sentinel.conf:/etc/redis/sentinel.conf
43+
options: >-
44+
--entrypoint "redis-sentinel /etc/redis/sentinel.conf"
45+
--health-cmd "redis-cli ping"
46+
--health-interval 10s
47+
--health-timeout 5s
48+
--health-retries 5
49+
ports:
50+
- 26379
51+
52+
strategy:
53+
matrix:
54+
os:
55+
- ubuntu
56+
57+
ruby:
58+
- "3.1"
59+
- "3.2"
60+
- "3.3"
61+
62+
experimental: [false]
63+
64+
include:
65+
- os: ubuntu
66+
ruby: truffleruby
67+
experimental: true
68+
- os: ubuntu
69+
ruby: jruby
70+
experimental: true
71+
- os: ubuntu
72+
ruby: head
73+
experimental: true
74+
75+
steps:
76+
- uses: actions/checkout@v4
77+
- uses: ruby/setup-ruby@v1
78+
with:
79+
ruby-version: ${{matrix.ruby}}
80+
bundler-cache: true
81+
82+
- name: Run tests
83+
timeout-minutes: 10
84+
run: bundle exec bake test

lib/async/redis.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66

77
require_relative 'redis/version'
88
require_relative 'redis/client'
9-
require_relative 'redis/sentinels'
9+
require_relative 'redis/sentinel_client'

lib/async/redis/client.rb

+61-57
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,65 @@ module Redis
2626
class Client
2727
include ::Protocol::Redis::Methods
2828

29+
module Methods
30+
def subscribe(*channels)
31+
context = Context::Subscribe.new(@pool, channels)
32+
33+
return context unless block_given?
34+
35+
begin
36+
yield context
37+
ensure
38+
context.close
39+
end
40+
end
41+
42+
def transaction(&block)
43+
context = Context::Transaction.new(@pool)
44+
45+
return context unless block_given?
46+
47+
begin
48+
yield context
49+
ensure
50+
context.close
51+
end
52+
end
53+
54+
alias multi transaction
55+
56+
def pipeline(&block)
57+
context = Context::Pipeline.new(@pool)
58+
59+
return context unless block_given?
60+
61+
begin
62+
yield context
63+
ensure
64+
context.close
65+
end
66+
end
67+
68+
# Deprecated.
69+
alias nested pipeline
70+
71+
def call(*arguments)
72+
@pool.acquire do |connection|
73+
connection.write_request(arguments)
74+
75+
connection.flush
76+
77+
return connection.read_response
78+
end
79+
end
80+
81+
def close
82+
@pool.close
83+
end
84+
end
85+
86+
include Methods
87+
2988
def initialize(endpoint = Endpoint.local, protocol: endpoint.protocol, **options)
3089
@endpoint = endpoint
3190
@protocol = protocol
@@ -38,8 +97,8 @@ def initialize(endpoint = Endpoint.local, protocol: endpoint.protocol, **options
3897

3998
# @return [client] if no block provided.
4099
# @yield [client, task] yield the client in an async task.
41-
def self.open(*arguments, &block)
42-
client = self.new(*arguments)
100+
def self.open(*arguments, **options, &block)
101+
client = self.new(*arguments, **options)
43102

44103
return client unless block_given?
45104

@@ -52,61 +111,6 @@ def self.open(*arguments, &block)
52111
end.wait
53112
end
54113

55-
def close
56-
@pool.close
57-
end
58-
59-
def subscribe(*channels)
60-
context = Context::Subscribe.new(@pool, channels)
61-
62-
return context unless block_given?
63-
64-
begin
65-
yield context
66-
ensure
67-
context.close
68-
end
69-
end
70-
71-
def transaction(&block)
72-
context = Context::Transaction.new(@pool)
73-
74-
return context unless block_given?
75-
76-
begin
77-
yield context
78-
ensure
79-
context.close
80-
end
81-
end
82-
83-
alias multi transaction
84-
85-
def pipeline(&block)
86-
context = Context::Pipeline.new(@pool)
87-
88-
return context unless block_given?
89-
90-
begin
91-
yield context
92-
ensure
93-
context.close
94-
end
95-
end
96-
97-
# Deprecated.
98-
alias nested pipeline
99-
100-
def call(*arguments)
101-
@pool.acquire do |connection|
102-
connection.write_request(arguments)
103-
104-
connection.flush
105-
106-
return connection.read_response
107-
end
108-
end
109-
110114
protected
111115

112116
def connect(**options)

lib/async/redis/endpoint.rb

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ class Endpoint < ::IO::Endpoint::Generic
2323

2424
def self.local(**options)
2525
self.new(LOCALHOST, **options)
26-
end
26+
end
27+
28+
def self.remote(host, port = 6379, **options)
29+
self.new(URI.parse("redis://#{host}:#{port}"), **options)
30+
end
2731

2832
SCHEMES = {
2933
'redis' => URI::Generic,

lib/async/redis/sentinels.rb renamed to lib/async/redis/sentinel_client.rb

+20-18
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,29 @@
44
# Copyright, 2020, by David Ortiz.
55
# Copyright, 2023-2024, by Samuel Williams.
66

7+
require_relative 'client'
78
require 'io/stream'
89

910
module Async
1011
module Redis
11-
class SentinelsClient < Client
12-
def initialize(master_name, sentinels, role = :master, protocol = Protocol::RESP2, **options)
12+
class SentinelClient
13+
DEFAULT_MASTER_NAME = 'mymaster'
14+
15+
include ::Protocol::Redis::Methods
16+
include Client::Methods
17+
18+
def initialize(endpoints, master_name: DEFAULT_MASTER_NAME, role: :master, protocol: Protocol::RESP2, **options)
19+
@endpoints = endpoints
1320
@master_name = master_name
14-
15-
@sentinel_endpoints = sentinels.map do |sentinel|
16-
::IO::Endpoint.tcp(sentinel[:host], sentinel[:port])
17-
end
18-
1921
@role = role
2022
@protocol = protocol
23+
2124
@pool = connect(**options)
2225
end
2326

24-
private
27+
protected
2528

26-
# Override the parent method. The only difference is that this one needs
27-
# to resolve the master/slave address.
29+
# Override the parent method. The only difference is that this one needs to resolve the master/slave address.
2830
def connect(**options)
2931
Async::Pool::Controller.wrap(**options) do
3032
endpoint = resolve_address
@@ -49,24 +51,24 @@ def resolve_address
4951
end
5052

5153
def resolve_master
52-
@sentinel_endpoints.each do |sentinel_endpoint|
53-
client = Client.new(sentinel_endpoint, protocol: @protocol)
54+
@endpoints.each do |endpoint|
55+
client = Client.new(endpoint)
5456

5557
begin
5658
address = client.call('sentinel', 'get-master-addr-by-name', @master_name)
5759
rescue Errno::ECONNREFUSED
5860
next
5961
end
6062

61-
return ::IO::Endpoint.tcp(address[0], address[1]) if address
63+
return Endpoint.remote(address[0], address[1]) if address
6264
end
6365

64-
nil
66+
return nil
6567
end
6668

6769
def resolve_slave
68-
@sentinel_endpoints.each do |sentinel_endpoint|
69-
client = Client.new(sentinel_endpoint, protocol: @protocol)
70+
@endpoints.each do |endpoint|
71+
client = Client.new(endpoint)
7072

7173
begin
7274
reply = client.call('sentinel', 'slaves', @master_name)
@@ -78,10 +80,10 @@ def resolve_slave
7880
next if slaves.empty?
7981

8082
slave = select_slave(slaves)
81-
return ::IO::Endpoint.tcp(slave['ip'], slave['port'])
83+
return Endpoint.remote(slave['ip'], slave['port'])
8284
end
8385

84-
nil
86+
return nil
8587
end
8688

8789
def available_slaves(reply)

test/async/redis/sentinel_client.rb

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2018-2024, by Samuel Williams.
5+
# Copyright, 2018, by Huba Nagy.
6+
# Copyright, 2019, by David Ortiz.
7+
8+
require 'async/clock'
9+
require 'async/redis/sentinel_client'
10+
require 'sus/fixtures/async'
11+
12+
describe Async::Redis::SentinelClient do
13+
include Sus::Fixtures::Async::ReactorContext
14+
15+
let(:master_host) {ENV['ASYNC_REDIS_MASTER']}
16+
let(:slave_host) {ENV['ASYNC_REDIS_SLAVE']}
17+
let(:sentinel_host) {ENV['ASYNC_REDIS_SENTINEL']}
18+
19+
it "should resolve master address" do
20+
unless master_host and slave_host and sentinel_host
21+
skip("No sentinel host provided.")
22+
end
23+
end
24+
end

0 commit comments

Comments
 (0)