pinot-client 1.21.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f91a5f7f465b546a3a49c78d2076097eac9b6af8fd9fbee7f8c1c45df06bea16
4
- data.tar.gz: 0a04dd7945a401c01e107c95062b738ff987a23706338d7436e88ff8e50bec13
3
+ metadata.gz: 8fa5d08fb08bee353252e2da56932948941faa5fdbdd4f3410e5ff8887755e04
4
+ data.tar.gz: c8bb9cbe06c5d209d8f9f63031776fc604a153730b4be207b7fd6d8a3b69e5f0
5
5
  SHA512:
6
- metadata.gz: aeee0c948cca37c67a7b8bb103201ea4a4ad1dbe9e08476ca679efb10c4ea9951ff4ade65af6e8f7cc11e3c55b33ca66f969b0e4271fe97a8bd62bc7265c24aa
7
- data.tar.gz: 8701f59b1adc42307e0774484a24d5c0e0cd33e8da378d9d2ccc65ad0240befcd094d58a136dc5031150fa687e88bbf87cbdde29e02d69e96462eedaf31dcbdc
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,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)
@@ -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
- @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
+ 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
@@ -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
data/lib/pinot/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pinot
2
- VERSION = "1.21.0"
2
+ VERSION = "1.22.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.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