pinot-client 1.21.0 → 1.23.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 +14 -5
- data/lib/pinot/connection.rb +15 -6
- data/lib/pinot/connection_factory.rb +14 -2
- data/lib/pinot/prepared_statement.rb +4 -4
- data/lib/pinot/transport.rb +2 -1
- 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: 0752e62700790e556388c588d9a305049a5935fc4da0afdefeecab830a9dde0c
|
|
4
|
+
data.tar.gz: 4f5b1e86e84817febf245ec13498ec300ac68688c691eda58a2f4aafa79d219c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 684b1e6b39e4e1f5f84bed527de2b50f056d101b81ba7621549454d81cc08f49dd044ce2d3dfa5cbb8084075339c0b4c45a89f32c7cf185f1aa73e6c9e40ec95
|
|
7
|
+
data.tar.gz: 4ea1edf2abac766c358f0e99ed073a05e500d19fb77101475c96f6b82fa4d308a0025565bc1d8d55103da7c7af470654343f9b2b5b57366838e257853e493223
|
|
@@ -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,10 +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,
|
|
28
|
-
:pool_size,
|
|
29
|
-
:keep_alive_timeout
|
|
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
|
|
30
33
|
|
|
31
34
|
def initialize(
|
|
32
35
|
broker_list: [],
|
|
@@ -42,7 +45,10 @@ module Pinot
|
|
|
42
45
|
max_retries: 0,
|
|
43
46
|
retry_interval_ms: 200,
|
|
44
47
|
pool_size: nil,
|
|
45
|
-
keep_alive_timeout: nil
|
|
48
|
+
keep_alive_timeout: nil,
|
|
49
|
+
circuit_breaker_enabled: false,
|
|
50
|
+
circuit_breaker_threshold: 5,
|
|
51
|
+
circuit_breaker_timeout: 30
|
|
46
52
|
)
|
|
47
53
|
@broker_list = broker_list
|
|
48
54
|
@http_timeout = http_timeout
|
|
@@ -58,6 +64,9 @@ module Pinot
|
|
|
58
64
|
@retry_interval_ms = retry_interval_ms
|
|
59
65
|
@pool_size = pool_size
|
|
60
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
|
|
61
70
|
end
|
|
62
71
|
|
|
63
72
|
def validate!
|
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)
|
|
@@ -25,12 +27,15 @@ module Pinot
|
|
|
25
27
|
@trace = false
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
def execute_sql(table, query, query_timeout_ms: nil)
|
|
30
|
+
def execute_sql(table, query, query_timeout_ms: nil, headers: {})
|
|
29
31
|
Pinot::Instrumentation.instrument(table: table, query: query) do
|
|
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
|
+
extra_request_headers: headers)
|
|
38
|
+
end
|
|
34
39
|
end
|
|
35
40
|
end
|
|
36
41
|
|
|
@@ -38,11 +43,10 @@ module Pinot
|
|
|
38
43
|
execute_sql(table, query, query_timeout_ms: timeout_ms)
|
|
39
44
|
end
|
|
40
45
|
|
|
41
|
-
def execute_sql_with_params(table, query_pattern, params, query_timeout_ms: nil)
|
|
46
|
+
def execute_sql_with_params(table, query_pattern, params, query_timeout_ms: nil, headers: {})
|
|
42
47
|
query = format_query(query_pattern, params)
|
|
43
|
-
execute_sql(table, query, query_timeout_ms: query_timeout_ms)
|
|
48
|
+
execute_sql(table, query, query_timeout_ms: query_timeout_ms, headers: headers)
|
|
44
49
|
rescue => e
|
|
45
|
-
# Re-raise format errors directly (they already have the right message)
|
|
46
50
|
raise e
|
|
47
51
|
end
|
|
48
52
|
|
|
@@ -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
|
|
@@ -49,7 +49,8 @@ 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
|
|
|
@@ -71,7 +72,8 @@ 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
|
|
@@ -96,4 +98,14 @@ module Pinot
|
|
|
96
98
|
end
|
|
97
99
|
end
|
|
98
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
|
|
99
111
|
end
|
|
@@ -60,7 +60,7 @@ module Pinot
|
|
|
60
60
|
nil
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
def execute
|
|
63
|
+
def execute(headers: {})
|
|
64
64
|
@mutex.synchronize do
|
|
65
65
|
raise PreparedStatementClosedError, "prepared statement is closed" if @closed
|
|
66
66
|
@parameters.each_with_index do |p, i|
|
|
@@ -72,10 +72,10 @@ module Pinot
|
|
|
72
72
|
rescue => e
|
|
73
73
|
raise "failed to build query: #{e.message}"
|
|
74
74
|
end
|
|
75
|
-
@connection.execute_sql(@table, query)
|
|
75
|
+
@connection.execute_sql(@table, query, headers: headers)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
def execute_with_params(*params)
|
|
78
|
+
def execute_with_params(*params, headers: {})
|
|
79
79
|
@mutex.synchronize { raise PreparedStatementClosedError, "prepared statement is closed" if @closed }
|
|
80
80
|
if params.length != @param_count
|
|
81
81
|
raise "expected #{@param_count} parameters, got #{params.length}"
|
|
@@ -85,7 +85,7 @@ module Pinot
|
|
|
85
85
|
rescue => e
|
|
86
86
|
raise "failed to build query: #{e.message}"
|
|
87
87
|
end
|
|
88
|
-
@connection.execute_sql(@table, query)
|
|
88
|
+
@connection.execute_sql(@table, query, headers: headers)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
def clear_parameters
|
data/lib/pinot/transport.rb
CHANGED
|
@@ -188,7 +188,7 @@ module Pinot
|
|
|
188
188
|
@retry_interval_ms = retry_interval_ms
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
-
def execute(broker_address, request)
|
|
191
|
+
def execute(broker_address, request, extra_request_headers: {})
|
|
192
192
|
logger.debug "Pinot query to #{broker_address}: #{request.query}"
|
|
193
193
|
|
|
194
194
|
attempts = 0
|
|
@@ -202,6 +202,7 @@ module Pinot
|
|
|
202
202
|
headers = DEFAULT_HEADERS
|
|
203
203
|
.merge(@extra_headers)
|
|
204
204
|
.merge("X-Correlation-Id" => SecureRandom.uuid)
|
|
205
|
+
.merge(extra_request_headers)
|
|
205
206
|
|
|
206
207
|
resp = @http_client.post(url, body: body, headers: headers)
|
|
207
208
|
|
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.23.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
|