pinot-client 1.20.0 → 1.22.0
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.
- checksums.yaml +4 -4
- data/lib/pinot/circuit_breaker.rb +115 -0
- data/lib/pinot/config.rb +26 -4
- data/lib/pinot/connection.rb +11 -2
- data/lib/pinot/connection_factory.rb +26 -4
- data/lib/pinot/errors.rb +3 -0
- data/lib/pinot/transport.rb +21 -10
- data/lib/pinot/version.rb +1 -1
- data/lib/pinot.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fa5d08fb08bee353252e2da56932948941faa5fdbdd4f3410e5ff8887755e04
|
|
4
|
+
data.tar.gz: c8bb9cbe06c5d209d8f9f63031776fc604a153730b4be207b7fd6d8a3b69e5f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d5c85b83effd2ce352b09c03d350eedd817415a18994ed23ef7be972f80dfb8d72cbbbace9c83bfddedb095b6695af0b8b4538f2f0023ee40844e6b554849a9
|
|
7
|
+
data.tar.gz: c450ffb393a8798ae3ea3e608cf2cb976bdf5aeb6f7145121764c41bed33069807fdb3087d02c3963552f79568fb6f2020088794cccf47f97218d10fe7041c99
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
module Pinot
|
|
2
|
+
# Per-broker circuit breaker. States: CLOSED (normal), OPEN (rejecting), HALF_OPEN (probing).
|
|
3
|
+
#
|
|
4
|
+
# failure_threshold - consecutive failures before opening (default 5)
|
|
5
|
+
# open_timeout - seconds to stay OPEN before moving to HALF_OPEN (default 30)
|
|
6
|
+
class CircuitBreaker
|
|
7
|
+
CLOSED = :closed
|
|
8
|
+
OPEN = :open
|
|
9
|
+
HALF_OPEN = :half_open
|
|
10
|
+
|
|
11
|
+
BrokerCircuitOpenError = Class.new(BrokerNotFoundError)
|
|
12
|
+
|
|
13
|
+
attr_reader :state, :failure_count
|
|
14
|
+
|
|
15
|
+
def initialize(failure_threshold: 5, open_timeout: 30)
|
|
16
|
+
@failure_threshold = failure_threshold
|
|
17
|
+
@open_timeout = open_timeout
|
|
18
|
+
@mutex = Mutex.new
|
|
19
|
+
@state = CLOSED
|
|
20
|
+
@failure_count = 0
|
|
21
|
+
@opened_at = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Call the block; record success/failure and enforce open-circuit rejection.
|
|
25
|
+
def call(broker_address)
|
|
26
|
+
@mutex.synchronize { check_state! }
|
|
27
|
+
begin
|
|
28
|
+
result = yield
|
|
29
|
+
@mutex.synchronize { on_success }
|
|
30
|
+
result
|
|
31
|
+
rescue BrokerUnavailableError, Errno::ECONNRESET, Errno::ECONNREFUSED,
|
|
32
|
+
Errno::ETIMEDOUT, Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout => e
|
|
33
|
+
@mutex.synchronize { on_failure }
|
|
34
|
+
raise
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def open?
|
|
39
|
+
@mutex.synchronize { @state == OPEN }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reset
|
|
43
|
+
@mutex.synchronize do
|
|
44
|
+
@state = CLOSED
|
|
45
|
+
@failure_count = 0
|
|
46
|
+
@opened_at = nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def check_state!
|
|
53
|
+
return if @state == CLOSED
|
|
54
|
+
|
|
55
|
+
if @state == OPEN
|
|
56
|
+
if elapsed_since_open >= @open_timeout
|
|
57
|
+
@state = HALF_OPEN
|
|
58
|
+
else
|
|
59
|
+
raise BrokerCircuitOpenError, "circuit open for broker (#{remaining_open_time.ceil}s remaining)"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
# HALF_OPEN: allow the probe through
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def on_success
|
|
66
|
+
@state = CLOSED
|
|
67
|
+
@failure_count = 0
|
|
68
|
+
@opened_at = nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def on_failure
|
|
72
|
+
@failure_count += 1
|
|
73
|
+
if @state == HALF_OPEN || @failure_count >= @failure_threshold
|
|
74
|
+
@state = OPEN
|
|
75
|
+
@opened_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def elapsed_since_open
|
|
80
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - @opened_at
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def remaining_open_time
|
|
84
|
+
@open_timeout - elapsed_since_open
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Registry of per-broker CircuitBreakers, shared across transport calls.
|
|
89
|
+
class CircuitBreakerRegistry
|
|
90
|
+
def initialize(failure_threshold: 5, open_timeout: 30)
|
|
91
|
+
@failure_threshold = failure_threshold
|
|
92
|
+
@open_timeout = open_timeout
|
|
93
|
+
@breakers = {}
|
|
94
|
+
@mutex = Mutex.new
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def for(broker_address)
|
|
98
|
+
@mutex.synchronize do
|
|
99
|
+
@breakers[broker_address] ||= CircuitBreaker.new(
|
|
100
|
+
failure_threshold: @failure_threshold,
|
|
101
|
+
open_timeout: @open_timeout
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def open?(broker_address)
|
|
107
|
+
@mutex.synchronize { @breakers[broker_address]&.open? || false }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Remove all state (useful for testing).
|
|
111
|
+
def reset_all
|
|
112
|
+
@mutex.synchronize { @breakers.clear }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/pinot/config.rb
CHANGED
|
@@ -23,8 +23,13 @@ module Pinot
|
|
|
23
23
|
attr_accessor :broker_list, :http_timeout, :query_timeout_ms, :extra_http_header,
|
|
24
24
|
:use_multistage_engine, :controller_config, :logger, :tls_config,
|
|
25
25
|
:grpc_config, :zookeeper_config,
|
|
26
|
-
:max_retries,
|
|
27
|
-
:retry_interval_ms
|
|
26
|
+
:max_retries, # Integer, default 0 (no retry)
|
|
27
|
+
:retry_interval_ms, # Integer ms base interval, default 200
|
|
28
|
+
:pool_size, # max idle connections per broker, default 5
|
|
29
|
+
:keep_alive_timeout, # seconds before idle connection is reaped, default 30
|
|
30
|
+
:circuit_breaker_enabled, # bool, default false
|
|
31
|
+
:circuit_breaker_threshold, # consecutive failures before opening, default 5
|
|
32
|
+
:circuit_breaker_timeout # seconds circuit stays open, default 30
|
|
28
33
|
|
|
29
34
|
def initialize(
|
|
30
35
|
broker_list: [],
|
|
@@ -38,7 +43,12 @@ module Pinot
|
|
|
38
43
|
grpc_config: nil,
|
|
39
44
|
zookeeper_config: nil,
|
|
40
45
|
max_retries: 0,
|
|
41
|
-
retry_interval_ms: 200
|
|
46
|
+
retry_interval_ms: 200,
|
|
47
|
+
pool_size: nil,
|
|
48
|
+
keep_alive_timeout: nil,
|
|
49
|
+
circuit_breaker_enabled: false,
|
|
50
|
+
circuit_breaker_threshold: 5,
|
|
51
|
+
circuit_breaker_timeout: 30
|
|
42
52
|
)
|
|
43
53
|
@broker_list = broker_list
|
|
44
54
|
@http_timeout = http_timeout
|
|
@@ -50,9 +60,13 @@ module Pinot
|
|
|
50
60
|
@tls_config = tls_config
|
|
51
61
|
@grpc_config = grpc_config
|
|
52
62
|
@zookeeper_config = zookeeper_config
|
|
53
|
-
@query_timeout_ms = query_timeout_ms
|
|
54
63
|
@max_retries = max_retries
|
|
55
64
|
@retry_interval_ms = retry_interval_ms
|
|
65
|
+
@pool_size = pool_size
|
|
66
|
+
@keep_alive_timeout = keep_alive_timeout
|
|
67
|
+
@circuit_breaker_enabled = circuit_breaker_enabled
|
|
68
|
+
@circuit_breaker_threshold = circuit_breaker_threshold
|
|
69
|
+
@circuit_breaker_timeout = circuit_breaker_timeout
|
|
56
70
|
end
|
|
57
71
|
|
|
58
72
|
def validate!
|
|
@@ -75,6 +89,14 @@ module Pinot
|
|
|
75
89
|
raise ConfigurationError, "query_timeout_ms must be positive, got: #{query_timeout_ms}"
|
|
76
90
|
end
|
|
77
91
|
|
|
92
|
+
if !pool_size.nil? && pool_size < 1
|
|
93
|
+
raise ConfigurationError, "pool_size must be at least 1, got: #{pool_size}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if !keep_alive_timeout.nil? && keep_alive_timeout <= 0
|
|
97
|
+
raise ConfigurationError, "keep_alive_timeout must be positive, got: #{keep_alive_timeout}"
|
|
98
|
+
end
|
|
99
|
+
|
|
78
100
|
self
|
|
79
101
|
end
|
|
80
102
|
end
|
data/lib/pinot/connection.rb
CHANGED
|
@@ -4,13 +4,15 @@ module Pinot
|
|
|
4
4
|
class Connection
|
|
5
5
|
attr_accessor :query_timeout_ms
|
|
6
6
|
|
|
7
|
-
def initialize(transport:, broker_selector:, use_multistage_engine: false, logger: nil,
|
|
7
|
+
def initialize(transport:, broker_selector:, use_multistage_engine: false, logger: nil,
|
|
8
|
+
query_timeout_ms: nil, circuit_breaker_registry: nil)
|
|
8
9
|
@transport = transport
|
|
9
10
|
@broker_selector = broker_selector
|
|
10
11
|
@use_multistage_engine = use_multistage_engine
|
|
11
12
|
@trace = false
|
|
12
13
|
@logger = logger
|
|
13
14
|
@query_timeout_ms = query_timeout_ms
|
|
15
|
+
@circuit_breaker_registry = circuit_breaker_registry
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def use_multistage_engine=(val)
|
|
@@ -30,7 +32,9 @@ module Pinot
|
|
|
30
32
|
logger.debug "Executing SQL on table=#{table}: #{query}"
|
|
31
33
|
broker = @broker_selector.select_broker(table)
|
|
32
34
|
effective_timeout = query_timeout_ms || @query_timeout_ms
|
|
33
|
-
|
|
35
|
+
run_with_circuit_breaker(broker) do
|
|
36
|
+
@transport.execute(broker, build_request(query, timeout_ms: effective_timeout))
|
|
37
|
+
end
|
|
34
38
|
end
|
|
35
39
|
end
|
|
36
40
|
|
|
@@ -97,6 +101,11 @@ module Pinot
|
|
|
97
101
|
|
|
98
102
|
private
|
|
99
103
|
|
|
104
|
+
def run_with_circuit_breaker(broker, &block)
|
|
105
|
+
return yield unless @circuit_breaker_registry
|
|
106
|
+
@circuit_breaker_registry.for(broker).call(broker, &block)
|
|
107
|
+
end
|
|
108
|
+
|
|
100
109
|
def logger
|
|
101
110
|
@logger || Pinot::Logging.logger
|
|
102
111
|
end
|
|
@@ -34,7 +34,7 @@ module Pinot
|
|
|
34
34
|
selector = ZookeeperBrokerSelector.new(zk_path: config.zookeeper_config.zk_path)
|
|
35
35
|
selector.init
|
|
36
36
|
|
|
37
|
-
inner = http_client ||
|
|
37
|
+
inner = http_client || build_http_client(config)
|
|
38
38
|
transport = JsonHttpTransport.new(
|
|
39
39
|
http_client: inner,
|
|
40
40
|
extra_headers: config.extra_http_header || {},
|
|
@@ -49,11 +49,12 @@ module Pinot
|
|
|
49
49
|
broker_selector: selector,
|
|
50
50
|
use_multistage_engine: config.use_multistage_engine || false,
|
|
51
51
|
logger: config.logger,
|
|
52
|
-
query_timeout_ms: config.query_timeout_ms
|
|
52
|
+
query_timeout_ms: config.query_timeout_ms,
|
|
53
|
+
circuit_breaker_registry: build_circuit_breaker_registry(config)
|
|
53
54
|
)
|
|
54
55
|
end
|
|
55
56
|
|
|
56
|
-
inner = http_client ||
|
|
57
|
+
inner = http_client || build_http_client(config)
|
|
57
58
|
|
|
58
59
|
transport = JsonHttpTransport.new(
|
|
59
60
|
http_client: inner,
|
|
@@ -71,13 +72,24 @@ module Pinot
|
|
|
71
72
|
broker_selector: selector,
|
|
72
73
|
use_multistage_engine: config.use_multistage_engine || false,
|
|
73
74
|
logger: config.logger,
|
|
74
|
-
query_timeout_ms: config.query_timeout_ms
|
|
75
|
+
query_timeout_ms: config.query_timeout_ms,
|
|
76
|
+
circuit_breaker_registry: build_circuit_breaker_registry(config)
|
|
75
77
|
)
|
|
76
78
|
|
|
77
79
|
selector.init
|
|
78
80
|
conn
|
|
79
81
|
end
|
|
80
82
|
|
|
83
|
+
def self.build_http_client(config)
|
|
84
|
+
HttpClient.new(
|
|
85
|
+
timeout: config.http_timeout,
|
|
86
|
+
tls_config: config.tls_config,
|
|
87
|
+
pool_size: config.pool_size,
|
|
88
|
+
keep_alive_timeout: config.keep_alive_timeout
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
private_class_method :build_http_client
|
|
92
|
+
|
|
81
93
|
def self.build_selector(config, http_client)
|
|
82
94
|
if config.broker_list && !config.broker_list.empty?
|
|
83
95
|
SimpleBrokerSelector.new(config.broker_list)
|
|
@@ -86,4 +98,14 @@ module Pinot
|
|
|
86
98
|
end
|
|
87
99
|
end
|
|
88
100
|
private_class_method :build_selector
|
|
101
|
+
|
|
102
|
+
def self.build_circuit_breaker_registry(config)
|
|
103
|
+
return nil unless config.circuit_breaker_enabled
|
|
104
|
+
|
|
105
|
+
CircuitBreakerRegistry.new(
|
|
106
|
+
failure_threshold: config.circuit_breaker_threshold || 5,
|
|
107
|
+
open_timeout: config.circuit_breaker_timeout || 30
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
private_class_method :build_circuit_breaker_registry
|
|
89
111
|
end
|
data/lib/pinot/errors.rb
CHANGED
|
@@ -3,6 +3,9 @@ module Pinot
|
|
|
3
3
|
class BrokerNotFoundError < Error; end
|
|
4
4
|
class TableNotFoundError < Error; end
|
|
5
5
|
class TransportError < Error; end
|
|
6
|
+
class BrokerUnavailableError < TransportError; end
|
|
7
|
+
class QueryTimeoutError < TransportError; end
|
|
8
|
+
class RateLimitError < TransportError; end
|
|
6
9
|
class PreparedStatementClosedError < Error; end
|
|
7
10
|
class ConfigurationError < Error; end
|
|
8
11
|
end
|
data/lib/pinot/transport.rb
CHANGED
|
@@ -6,14 +6,16 @@ require "openssl"
|
|
|
6
6
|
|
|
7
7
|
module Pinot
|
|
8
8
|
class HttpClient
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
DEFAULT_POOL_SIZE = 5
|
|
10
|
+
DEFAULT_KEEP_ALIVE_TIMEOUT = 30
|
|
11
11
|
|
|
12
12
|
PoolEntry = Struct.new(:http, :checked_in_at)
|
|
13
13
|
|
|
14
|
-
def initialize(timeout: nil, tls_config: nil)
|
|
14
|
+
def initialize(timeout: nil, tls_config: nil, pool_size: nil, keep_alive_timeout: nil)
|
|
15
15
|
@timeout = timeout
|
|
16
16
|
@tls_config = tls_config
|
|
17
|
+
@max_pool_size = pool_size || DEFAULT_POOL_SIZE
|
|
18
|
+
@keep_alive_timeout = keep_alive_timeout || DEFAULT_KEEP_ALIVE_TIMEOUT
|
|
17
19
|
@pool = {}
|
|
18
20
|
@pool_mutex = Mutex.new
|
|
19
21
|
@reaper = start_reaper
|
|
@@ -70,7 +72,7 @@ module Pinot
|
|
|
70
72
|
entries = @pool[key] ||= []
|
|
71
73
|
fresh = nil
|
|
72
74
|
while (entry = entries.pop)
|
|
73
|
-
if now - entry.checked_in_at <
|
|
75
|
+
if now - entry.checked_in_at < @keep_alive_timeout
|
|
74
76
|
fresh = entry.http
|
|
75
77
|
break
|
|
76
78
|
else
|
|
@@ -85,7 +87,7 @@ module Pinot
|
|
|
85
87
|
def checkin(key, http)
|
|
86
88
|
@pool_mutex.synchronize do
|
|
87
89
|
pool_for_key = @pool[key] ||= []
|
|
88
|
-
if pool_for_key.size <
|
|
90
|
+
if pool_for_key.size < @max_pool_size
|
|
89
91
|
pool_for_key.push(PoolEntry.new(http, Process.clock_gettime(Process::CLOCK_MONOTONIC)))
|
|
90
92
|
else
|
|
91
93
|
http.finish rescue nil
|
|
@@ -96,7 +98,7 @@ module Pinot
|
|
|
96
98
|
def start_reaper
|
|
97
99
|
t = Thread.new do
|
|
98
100
|
loop do
|
|
99
|
-
sleep
|
|
101
|
+
sleep @keep_alive_timeout / 2.0
|
|
100
102
|
reap_stale_connections
|
|
101
103
|
end
|
|
102
104
|
end
|
|
@@ -109,7 +111,7 @@ module Pinot
|
|
|
109
111
|
@pool_mutex.synchronize do
|
|
110
112
|
@pool.each_value do |entries|
|
|
111
113
|
entries.reject! do |entry|
|
|
112
|
-
if now - entry.checked_in_at >=
|
|
114
|
+
if now - entry.checked_in_at >= @keep_alive_timeout
|
|
113
115
|
entry.http.finish rescue nil
|
|
114
116
|
true
|
|
115
117
|
else
|
|
@@ -167,6 +169,15 @@ module Pinot
|
|
|
167
169
|
Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout
|
|
168
170
|
].freeze
|
|
169
171
|
|
|
172
|
+
# HTTP status codes that map to specific error classes and are safe to retry
|
|
173
|
+
HTTP_ERROR_MAP = {
|
|
174
|
+
"429" => RateLimitError,
|
|
175
|
+
"503" => BrokerUnavailableError,
|
|
176
|
+
"504" => BrokerUnavailableError
|
|
177
|
+
}.freeze
|
|
178
|
+
|
|
179
|
+
RETRYABLE_HTTP_ERRORS = [RateLimitError, BrokerUnavailableError].freeze
|
|
180
|
+
|
|
170
181
|
def initialize(http_client:, extra_headers: {}, timeout_ms: nil, logger: nil,
|
|
171
182
|
max_retries: 0, retry_interval_ms: 200)
|
|
172
183
|
@http_client = http_client
|
|
@@ -194,9 +205,9 @@ module Pinot
|
|
|
194
205
|
|
|
195
206
|
resp = @http_client.post(url, body: body, headers: headers)
|
|
196
207
|
|
|
197
|
-
if resp.code
|
|
208
|
+
if (error_class = HTTP_ERROR_MAP[resp.code])
|
|
198
209
|
logger.error "Pinot broker returned HTTP #{resp.code}"
|
|
199
|
-
raise
|
|
210
|
+
raise error_class, "http exception with HTTP status code #{resp.code}"
|
|
200
211
|
end
|
|
201
212
|
|
|
202
213
|
unless resp.code.to_i == 200
|
|
@@ -209,7 +220,7 @@ module Pinot
|
|
|
209
220
|
rescue JSON::ParserError => e
|
|
210
221
|
raise e.message
|
|
211
222
|
end
|
|
212
|
-
rescue
|
|
223
|
+
rescue *RETRYABLE_HTTP_ERRORS, *RETRYABLE_ERRORS => e
|
|
213
224
|
if attempts < max_attempts
|
|
214
225
|
sleep_ms = (@retry_interval_ms || 200) * (2 ** (attempts - 1))
|
|
215
226
|
sleep(sleep_ms / 1000.0)
|
data/lib/pinot/version.rb
CHANGED
data/lib/pinot.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative "pinot/table_aware_broker_selector"
|
|
|
18
18
|
require_relative "pinot/controller_response"
|
|
19
19
|
require_relative "pinot/controller_based_broker_selector"
|
|
20
20
|
require_relative "pinot/transport"
|
|
21
|
+
require_relative "pinot/circuit_breaker"
|
|
21
22
|
require_relative "pinot/connection"
|
|
22
23
|
require_relative "pinot/prepared_statement"
|
|
23
24
|
require_relative "pinot/connection_factory"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pinot-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.22.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Xiang Fu
|
|
@@ -104,6 +104,7 @@ files:
|
|
|
104
104
|
- README.md
|
|
105
105
|
- lib/pinot.rb
|
|
106
106
|
- lib/pinot/broker_selector.rb
|
|
107
|
+
- lib/pinot/circuit_breaker.rb
|
|
107
108
|
- lib/pinot/config.rb
|
|
108
109
|
- lib/pinot/connection.rb
|
|
109
110
|
- lib/pinot/connection_factory.rb
|