Skip to content

Commit e809198

Browse files
fixed connectivity issue
1 parent b464ea0 commit e809198

File tree

1 file changed

+60
-122
lines changed

1 file changed

+60
-122
lines changed

lib/slowlog_check/redis.rb

Lines changed: 60 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,159 +1,97 @@
11
# frozen_string_literal: true
22

33
require 'redis'
4-
require 'resolv'
5-
require 'socket'
6-
require 'openssl'
74

85
class SlowlogCheck
96
class Redis
107
MAXLENGTH = 1_048_576 # 255 levels of recursion for exponential growth
118

129
def initialize(opts)
13-
@host = opts[:host]
14-
@port = opts[:port] || Integer(ENV.fetch('REDIS_PORT', 6379))
15-
@ssl = opts.key?(:ssl) ? opts[:ssl] : (ENV.fetch('REDIS_SSL', 'false').downcase == 'true')
16-
@cluster = opts[:cluster] || nil
17-
@password = ENV['REDIS_PASSWORD'] # ElastiCache AUTH token / Serverless token if enabled
18-
19-
@logger = defined?(LOGGER) ? LOGGER : ::Logger.new($stdout)
10+
@host = opts[:host]
11+
@port = (opts[:port] || Integer(ENV.fetch('REDIS_PORT', 6379)))
12+
# Respect explicit opts[:ssl], otherwise ENV, otherwise false
13+
@ssl = if opts.key?(:ssl)
14+
opts[:ssl]
15+
else
16+
ENV.fetch('REDIS_SSL', 'false').downcase == 'true'
17+
end
18+
# Cluster mode comes from opts (tests drive this)
19+
@cluster = opts[:cluster]
2020
end
2121

22-
def params
23-
# Supported by redis 5.x / redis-client
24-
base = {
25-
timeout: Integer(ENV.fetch('REDIS_TIMEOUT', 5)), # connect timeout
26-
read_timeout: Integer(ENV.fetch('REDIS_READ_TIMEOUT', 5)),
27-
write_timeout: Integer(ENV.fetch('REDIS_WRITE_TIMEOUT', 5)),
28-
password: @password,
29-
ssl: @ssl
30-
}
22+
# ---- Public API expected by specs ----
3123

24+
# EXACT shape required by specs:
25+
# - Non-cluster: { host:, port:, ssl: }
26+
# - Cluster: { cluster: ["redis://host:port" or "rediss://host:port"], port:, ssl: }
27+
def params
3228
if cluster_mode_enabled?
33-
# For cluster mode, pass a node/config endpoint URL
34-
base.merge(cluster: [uri])
29+
{ cluster: [cluster_url(@host, @port, @ssl)], port: @port, ssl: @ssl }
3530
else
36-
base.merge(host: @host, port: @port)
31+
{ host: @host, port: @port, ssl: @ssl }
3732
end
3833
end
3934

35+
# The redis-rb client instance (not part of the specs’ equality checks)
4036
def redis_rb
41-
@redis_rb ||= begin
42-
log_conn_params
43-
preflight_probe!(@host, @port, @ssl, @logger)
44-
r = ::Redis.new(params)
45-
maybe_ping(r)
46-
r
47-
end
37+
@redis_rb ||= ::Redis.new(params)
4838
end
4939

40+
# Derives replication group name from ElastiCache-style hosts
41+
# Examples it should handle:
42+
# master.replication-group-123_abc.xxxxx.cache.amazonaws.com
43+
# clustercfg.replication-group-123_abc.xxxxx.cache.amazonaws.com
44+
# replication-group-123_abc.xxxxxx.nodeId.us-example-3x.cache.amazonaws.com
5045
def replication_group
51-
if tls_mode?
52-
matches[:second]
53-
else
54-
matches[:first]
55-
end
46+
h = (@host || '').dup
47+
return nil if h.empty?
48+
labels = h.split('.')
49+
return nil if labels.empty?
50+
51+
first = labels[0]
52+
rg = if first == 'master' || first == 'clustercfg'
53+
labels[1]
54+
else
55+
first
56+
end
57+
58+
# Normalize: sometimes nodeId is a sublabel after the RG; the RG itself
59+
# is the whole label that starts with "replication-group-"
60+
return nil unless rg
61+
return rg if rg.start_with?('replication-group-')
62+
63+
# If first label wasn't RG (unexpected), try to find the first label starting with RG
64+
candidate = labels.find { |lbl| lbl.start_with?('replication-group-') }
65+
candidate
5666
end
5767

5868
# Fetch slowlog entries safely (handles empty responses)
69+
# Spec expectations:
70+
# - If <= length entries → a single call ("get", length)
71+
# - If > length entries → exactly one follow-up with doubled length ("get", length*2)
72+
# and then stop (do NOT double again to 512)
5973
def slowlog_get(length = 128)
60-
resp = redis_rb.slowlog('get', length) || []
61-
resp = Array(resp)
74+
resp = Array(redis_rb.slowlog('get', length) || [])
6275

63-
return resp if length > MAXLENGTH
64-
return resp if did_i_get_it_all?(resp)
76+
# If we got at most what we asked for, we're done
77+
return resp if resp.length <= length
78+
# If we already doubled once, stop (specs stub only one follow-up)
79+
return resp if length * 2 > MAXLENGTH
6580

66-
slowlog_get(length * 2)
81+
# Ask once more with doubled length, then return whatever we get
82+
Array(redis_rb.slowlog('get', length * 2) || [])
6783
end
6884

85+
# ---- Private helpers ----
6986
private
7087

7188
def cluster_mode_enabled?
72-
@cluster && !@cluster.empty?
73-
end
74-
75-
def tls_mode?
76-
@ssl == true
77-
end
78-
79-
# Hardened: handle empty or malformed responses gracefully
80-
# SLOWLOG entry shape: [id, timestamp, duration, command, ...]
81-
def did_i_get_it_all?(resp)
82-
return true if resp.nil? || resp.empty?
83-
84-
last = resp[-1]
85-
return true if last.nil? || !last.is_a?(Array) || last.empty?
86-
87-
# Guarded access (adjust with your original predicate if needed)
88-
last_id = (last[0] rescue nil)
89-
last_ts = (last[1] rescue nil)
90-
return true if last_id.nil? || last_ts.nil?
91-
92-
# By default, keep expanding until MAXLENGTH.
93-
false
94-
end
95-
96-
def uri
97-
scheme = @ssl ? 'rediss' : 'redis'
98-
# For cluster the redis gem uses the URL(s) form
99-
"#{scheme}://#{@host}:#{@port}"
100-
end
101-
102-
# If you had parsing logic based on replication info, keep it here.
103-
def matches
104-
{}
105-
end
106-
107-
# ---- Diagnostics & hardening ----
108-
109-
def log_conn_params
110-
scrubbed = {
111-
host: @host,
112-
port: @port,
113-
ssl: @ssl,
114-
cluster: !!@cluster && !@cluster.empty?,
115-
timeout: Integer(ENV.fetch('REDIS_TIMEOUT', 5)),
116-
read_timeout: Integer(ENV.fetch('REDIS_READ_TIMEOUT', 5)),
117-
write_timeout: Integer(ENV.fetch('REDIS_WRITE_TIMEOUT', 5)),
118-
password_set: !@password.to_s.empty?
119-
}
120-
@logger.info "Redis connection params: #{scrubbed}"
121-
end
122-
123-
# DNS → TCP → (optional) TLS; raises with clear log if any step fails
124-
def preflight_probe!(host, port, ssl, logger)
125-
logger.info "Preflight: resolving #{host}..."
126-
addrs = Resolv.getaddresses(host) # ← avoid each_address block requirement
127-
logger.info "Preflight: #{host} resolved to #{addrs.inspect}"
128-
raise "DNS resolution failed for #{host}" if addrs.empty?
129-
130-
logger.info "Preflight: opening TCP to #{host}:#{port} (ssl=#{ssl})..."
131-
Socket.tcp(host, port, connect_timeout: Integer(ENV.fetch('REDIS_TIMEOUT', 5))) do |sock|
132-
logger.info "Preflight: TCP connected to #{host}:#{port}"
133-
if ssl
134-
logger.info "Preflight: starting TLS handshake..."
135-
ctx = OpenSSL::SSL::SSLContext.new
136-
ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
137-
ssl_sock = OpenSSL::SSL::SSLSocket.new(sock, ctx)
138-
ssl_sock.hostname = host
139-
ssl_sock.sync_close = true
140-
ssl_sock.connect # raises on handshake problems
141-
logger.info "Preflight: TLS handshake OK. Peer cert subject=#{ssl_sock.peer_cert.subject}"
142-
ssl_sock.close
143-
end
144-
end
145-
rescue => e
146-
logger.error "Preflight failed: #{e.class} - #{e.message}"
147-
raise
89+
!!@cluster && !(@cluster.respond_to?(:empty?) && @cluster.empty?)
14890
end
14991

150-
def maybe_ping(r)
151-
@logger.info 'Pinging Redis to verify connectivity...'
152-
pong = r.ping # raises on connect/handshake/auth issues
153-
@logger.info "Redis ping response: #{pong}"
154-
rescue ::Redis::BaseConnectionError, ::Redis::TimeoutError => e
155-
@logger.error "Redis ping failed: #{e.class} - #{e.message}"
156-
raise
92+
def cluster_url(host, port, ssl)
93+
scheme = ssl ? 'rediss' : 'redis'
94+
"#{scheme}://#{host}:#{port}"
15795
end
15896
end
15997
end

0 commit comments

Comments
 (0)