Skip to content

Commit 37594c0

Browse files
committed
Initial rework of sentinels client.
1 parent c87f30b commit 37594c0

File tree

9 files changed

+215
-77
lines changed

9 files changed

+215
-77
lines changed

.github/workflows/test-sentinel.yaml

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Test Sentinel
2+
3+
on: [push, pull_request]
4+
5+
permissions:
6+
contents: read
7+
8+
env:
9+
CONSOLE_OUTPUT: XTerm
10+
11+
jobs:
12+
test:
13+
name: ${{matrix.ruby}} on ${{matrix.os}}
14+
runs-on: ${{matrix.os}}-latest
15+
continue-on-error: ${{matrix.experimental}}
16+
17+
strategy:
18+
matrix:
19+
os:
20+
- ubuntu
21+
22+
ruby:
23+
- "3.1"
24+
- "3.2"
25+
- "3.3"
26+
27+
experimental: [false]
28+
29+
steps:
30+
- uses: actions/checkout@v4
31+
32+
- name: Install Docker Compose
33+
run: |
34+
sudo apt-get update
35+
sudo apt-get install -y docker-compose
36+
37+
- name: Run tests
38+
timeout-minutes: 10
39+
env:
40+
RUBY_VERSION: ${{matrix.ruby}}
41+
run: docker-compose -f sentinel/docker-compose.yaml up tests

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

+22-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
@@ -45,28 +47,30 @@ def resolve_address
4547
raise ArgumentError, "Unknown instance role #{@role}"
4648
end => address
4749

50+
Console.info(self, "Resolved #{@role} address: #{address}")
51+
4852
address or raise RuntimeError, "Unable to fetch #{@role} via Sentinel."
4953
end
5054

5155
def resolve_master
52-
@sentinel_endpoints.each do |sentinel_endpoint|
53-
client = Client.new(sentinel_endpoint, protocol: @protocol)
56+
@endpoints.each do |endpoint|
57+
client = Client.new(endpoint)
5458

5559
begin
5660
address = client.call('sentinel', 'get-master-addr-by-name', @master_name)
5761
rescue Errno::ECONNREFUSED
5862
next
5963
end
6064

61-
return ::IO::Endpoint.tcp(address[0], address[1]) if address
65+
return Endpoint.remote(address[0], address[1]) if address
6266
end
6367

64-
nil
68+
return nil
6569
end
6670

6771
def resolve_slave
68-
@sentinel_endpoints.each do |sentinel_endpoint|
69-
client = Client.new(sentinel_endpoint, protocol: @protocol)
72+
@endpoints.each do |endpoint|
73+
client = Client.new(endpoint)
7074

7175
begin
7276
reply = client.call('sentinel', 'slaves', @master_name)
@@ -78,10 +82,10 @@ def resolve_slave
7882
next if slaves.empty?
7983

8084
slave = select_slave(slaves)
81-
return ::IO::Endpoint.tcp(slave['ip'], slave['port'])
85+
return Endpoint.remote(slave['ip'], slave['port'])
8286
end
8387

84-
nil
88+
return nil
8589
end
8690

8791
def available_slaves(reply)

sentinel/docker-compose.yaml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
services:
2+
redis-master:
3+
image: redis
4+
redis-slave:
5+
image: redis
6+
command: redis-server --slaveof redis-master 6379
7+
depends_on:
8+
- redis-master
9+
redis-sentinel:
10+
image: redis
11+
command: redis-sentinel /etc/redis/sentinel.conf
12+
volumes:
13+
- ./sentinel.conf:/etc/redis/sentinel.conf
14+
depends_on:
15+
- redis-master
16+
- redis-slave
17+
tests:
18+
image: ruby:${RUBY_VERSION:-latest}
19+
volumes:
20+
- ../:/code
21+
command: bash -c "cd /code && bundle install && bundle exec sus sentinel/test"
22+
depends_on:
23+
- redis-master
24+
- redis-slave
25+
- redis-sentinel

sentinel/readme.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Sentinel Testing
2+
3+
To test sentinels, you need to set up master, slave and sentinel instances.
4+
5+
## Setup
6+
7+
``` bash
8+
$ docker-compose -f config/sentinel/docker-compose.yaml up -d
9+
[+] Running 3/3
10+
✔ Container sentinel-redis-master-1 Running 0.0s
11+
✔ Container sentinel-redis-slave-1 Running 0.0s
12+
✔ Container sentinel-redis-sentinel-1 Started 0.2s
13+
```
14+
15+
## Test
16+
17+
``` bash
18+
$ ASYNC_REDIS_MASTER=redis://redis-master:6379 ASYNC_REDIS_SLAVE=redis://redis-slave:6379 ASYNC_REDIS_SENTINEL=redis://redis-sentinel:26379 bundle exec sus
19+
```

sentinel/sentinel.conf

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
port 26379
2+
sentinel resolve-hostnames yes
3+
sentinel monitor mymaster redis-master 6379 1
4+
sentinel down-after-milliseconds mymaster 1000
5+
sentinel failover-timeout mymaster 1000
6+
sentinel parallel-syncs mymaster 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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) {"redis://redis-master:6379"}
16+
let(:slave_host) {"redis://redis-slave:6379"}
17+
let(:sentinel_host) {"redis://redis-sentinel:26379"}
18+
19+
let(:sentinels) {[
20+
Async::Redis::Endpoint.parse(sentinel_host)
21+
]}
22+
23+
let(:client) {subject.new(sentinels)}
24+
let(:slave_client) {subject.new(sentinels, role: :slave)}
25+
26+
it "should resolve master address" do
27+
unless master_host and slave_host and sentinel_host
28+
skip("No sentinel host provided.")
29+
end
30+
31+
client.set("key", "value")
32+
33+
expect(slave_client.get("key")).to be == "value"
34+
end
35+
end

0 commit comments

Comments
 (0)