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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f91a5f7f465b546a3a49c78d2076097eac9b6af8fd9fbee7f8c1c45df06bea16
4
- data.tar.gz: 0a04dd7945a401c01e107c95062b738ff987a23706338d7436e88ff8e50bec13
3
+ metadata.gz: 0752e62700790e556388c588d9a305049a5935fc4da0afdefeecab830a9dde0c
4
+ data.tar.gz: 4f5b1e86e84817febf245ec13498ec300ac68688c691eda58a2f4aafa79d219c
5
5
  SHA512:
6
- metadata.gz: aeee0c948cca37c67a7b8bb103201ea4a4ad1dbe9e08476ca679efb10c4ea9951ff4ade65af6e8f7cc11e3c55b33ca66f969b0e4271fe97a8bd62bc7265c24aa
7
- data.tar.gz: 8701f59b1adc42307e0774484a24d5c0e0cd33e8da378d9d2ccc65ad0240befcd094d58a136dc5031150fa687e88bbf87cbdde29e02d69e96462eedaf31dcbdc
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, # 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
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!
@@ -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, query_timeout_ms: 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
- @transport.execute(broker, build_request(query, timeout_ms: effective_timeout))
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Pinot
2
- VERSION = "1.21.0"
2
+ VERSION = "1.23.0"
3
3
  end
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.21.0
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