pinot-client 1.7.0 → 1.10.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: ce082ab7ebdfd2003c324e13981a48c4735196a424a98f40ca812cf755846cb5
4
- data.tar.gz: 8bb6cc60572a748c9acb0817ccc58fdb4a4bc369b016503392b7326d79d01ecf
3
+ metadata.gz: ddb501e91483838437691807ff09f98be16584f5b5e9e25c32982581db9fc226
4
+ data.tar.gz: 69f4df0c9fae29d42388b5fa6e1f55a1c722f70673c91b5ad43339762ad052fe
5
5
  SHA512:
6
- metadata.gz: df40aa52c3ac2101d8cdaf308b741eb056eb17163beabff92c57b5800047ae570d63e10369fb5778040a2986b1c5b9f959df71ec425f3ffeb9eb49f9366a6c31
7
- data.tar.gz: bdfe5f80fdee0c0b9b8b4ce1b216a16f098bc27df369a9a62983aa7a0672866260215cfa5ebacb678bb068d389824812491ae2b537f9abe1ea1d9fdf899fda15
6
+ metadata.gz: 974f22a9db31c53e6a0534400347df98b419a1f6fffc351594bc6ea06456d211cda4ede80390fdf7c9b67a0f0559a6772a8225cc552b2844fb28e33ea11220b6
7
+ data.tar.gz: 711718da0cb66cd42457c657f58fc29450049094a1f6bf9012557a3b484dc59ca2d66f45bbf9c2c716597779a084fc59ab7fb15730fbac4648ac47823291d9c4
data/README.md CHANGED
@@ -62,6 +62,30 @@ The client periodically polls the controller's `/v2/brokers/tables` API and auto
62
62
  client = Pinot.from_controller("localhost:9000")
63
63
  ```
64
64
 
65
+ ### Via gRPC
66
+
67
+ Requires the `grpc` gem (`gem "grpc", "~> 1.65"` in your Gemfile).
68
+
69
+ ```ruby
70
+ grpc_config = Pinot::GrpcConfig.new(
71
+ broker_list: ["localhost:8090"],
72
+ timeout: 10,
73
+ extra_metadata: { "Authorization" => "Bearer <token>" }
74
+ )
75
+ config = Pinot::ClientConfig.new(grpc_config: grpc_config)
76
+ client = Pinot.from_config(config)
77
+ ```
78
+
79
+ ### From ZooKeeper (dynamic broker discovery)
80
+
81
+ Requires the `zk` gem (`gem "zk"` in your Gemfile). Watches `/EXTERNALVIEW/brokerResource` and automatically picks up broker changes.
82
+
83
+ ```ruby
84
+ zk_config = Pinot::ZookeeperConfig.new(zk_path: "localhost:2181")
85
+ config = Pinot::ClientConfig.new(zookeeper_config: zk_config)
86
+ client = Pinot.from_config(config)
87
+ ```
88
+
65
89
  ### From a `ClientConfig`
66
90
 
67
91
  ```ruby
@@ -79,6 +103,8 @@ config = Pinot::ClientConfig.new(
79
103
  client = Pinot.from_config(config)
80
104
  ```
81
105
 
106
+ `validate!` raises `Pinot::ConfigurationError` early if the config is invalid.
107
+
82
108
  ### With TLS
83
109
 
84
110
  ```ruby
@@ -111,6 +137,20 @@ config = Pinot::ClientConfig.new(
111
137
  client = Pinot.from_config(config)
112
138
  ```
113
139
 
140
+ ## Instrumentation
141
+
142
+ Hook into every query execution for metrics, tracing, or alerting:
143
+
144
+ ```ruby
145
+ Pinot::Instrumentation.on_query = lambda do |event|
146
+ puts "#{event[:table]} #{event[:query][0..50]} " \
147
+ "#{event[:duration_ms].round(1)}ms " \
148
+ "success=#{event[:success]}"
149
+ end
150
+ ```
151
+
152
+ The event hash contains: `:table`, `:query`, `:duration_ms` (Float), `:success` (Boolean), `:error` (Exception or nil).
153
+
114
154
  ## Executing Queries
115
155
 
116
156
  ### Simple SQL
@@ -148,6 +188,19 @@ config = Pinot::ClientConfig.new(
148
188
  client = Pinot.from_config(config)
149
189
  ```
150
190
 
191
+ ### Per-request timeout
192
+
193
+ ```ruby
194
+ # Global default via config
195
+ config = Pinot::ClientConfig.new(
196
+ broker_list: ["localhost:8000"],
197
+ query_timeout_ms: 5000
198
+ )
199
+
200
+ # One-off override per query
201
+ resp = client.execute_sql_with_timeout("myTable", "SELECT * FROM myTable", 3000)
202
+ ```
203
+
151
204
  ### Trace
152
205
 
153
206
  ```ruby
@@ -182,6 +235,19 @@ rescue Pinot::Error => e
182
235
  end
183
236
  ```
184
237
 
238
+ ## Retry
239
+
240
+ Configure automatic retry with exponential backoff for transient errors (connection reset, timeout, HTTP 503):
241
+
242
+ ```ruby
243
+ config = Pinot::ClientConfig.new(
244
+ broker_list: ["localhost:8000"],
245
+ max_retries: 3,
246
+ retry_interval_ms: 200 # base interval; doubles each attempt (200ms, 400ms, 800ms)
247
+ )
248
+ client = Pinot.from_config(config)
249
+ ```
250
+
185
251
  ## Reading Results
186
252
 
187
253
  `execute_sql` returns a `Pinot::BrokerResponse`. Results are in `result_table`:
data/lib/pinot/config.rb CHANGED
@@ -22,7 +22,9 @@ module Pinot
22
22
  class ClientConfig
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
- :grpc_config, :zookeeper_config
25
+ :grpc_config, :zookeeper_config,
26
+ :max_retries, # Integer, default 0 (no retry)
27
+ :retry_interval_ms # Integer ms base interval, default 200
26
28
 
27
29
  def initialize(
28
30
  broker_list: [],
@@ -34,7 +36,9 @@ module Pinot
34
36
  logger: nil,
35
37
  tls_config: nil,
36
38
  grpc_config: nil,
37
- zookeeper_config: nil
39
+ zookeeper_config: nil,
40
+ max_retries: 0,
41
+ retry_interval_ms: 200
38
42
  )
39
43
  @broker_list = broker_list
40
44
  @http_timeout = http_timeout
@@ -47,6 +51,8 @@ module Pinot
47
51
  @grpc_config = grpc_config
48
52
  @zookeeper_config = zookeeper_config
49
53
  @query_timeout_ms = query_timeout_ms
54
+ @max_retries = max_retries
55
+ @retry_interval_ms = retry_interval_ms
50
56
  end
51
57
 
52
58
  def validate!
@@ -37,7 +37,9 @@ module Pinot
37
37
  transport = JsonHttpTransport.new(
38
38
  http_client: inner,
39
39
  extra_headers: config.extra_http_header || {},
40
- logger: config.logger
40
+ logger: config.logger,
41
+ max_retries: config.max_retries || 0,
42
+ retry_interval_ms: config.retry_interval_ms || 200
41
43
  )
42
44
 
43
45
  return Connection.new(
@@ -53,7 +55,9 @@ module Pinot
53
55
  transport = JsonHttpTransport.new(
54
56
  http_client: inner,
55
57
  extra_headers: config.extra_http_header || {},
56
- logger: config.logger
58
+ logger: config.logger,
59
+ max_retries: config.max_retries || 0,
60
+ retry_interval_ms: config.retry_interval_ms || 200
57
61
  )
58
62
 
59
63
  selector = build_selector(config, inner)
@@ -9,11 +9,14 @@ module Pinot
9
9
  MAX_POOL_SIZE = 5
10
10
  KEEP_ALIVE_TIMEOUT = 30
11
11
 
12
+ PoolEntry = Struct.new(:http, :checked_in_at)
13
+
12
14
  def initialize(timeout: nil, tls_config: nil)
13
15
  @timeout = timeout
14
16
  @tls_config = tls_config
15
17
  @pool = {}
16
18
  @pool_mutex = Mutex.new
19
+ @reaper = start_reaper
17
20
  end
18
21
 
19
22
  def post(url, body:, headers: {})
@@ -52,20 +55,61 @@ module Pinot
52
55
  end
53
56
 
54
57
  def checkout(key, uri)
55
- @pool_mutex.synchronize { @pool[key]&.pop } || new_connection(uri)
58
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
59
+ http = @pool_mutex.synchronize do
60
+ entries = @pool[key] ||= []
61
+ fresh = nil
62
+ while (entry = entries.pop)
63
+ if now - entry.checked_in_at < KEEP_ALIVE_TIMEOUT
64
+ fresh = entry.http
65
+ break
66
+ else
67
+ entry.http.finish rescue nil
68
+ end
69
+ end
70
+ fresh
71
+ end
72
+ http || new_connection(uri)
56
73
  end
57
74
 
58
75
  def checkin(key, http)
59
76
  @pool_mutex.synchronize do
60
77
  pool_for_key = @pool[key] ||= []
61
78
  if pool_for_key.size < MAX_POOL_SIZE
62
- pool_for_key.push(http)
79
+ pool_for_key.push(PoolEntry.new(http, Process.clock_gettime(Process::CLOCK_MONOTONIC)))
63
80
  else
64
81
  http.finish rescue nil
65
82
  end
66
83
  end
67
84
  end
68
85
 
86
+ def start_reaper
87
+ t = Thread.new do
88
+ loop do
89
+ sleep KEEP_ALIVE_TIMEOUT / 2.0
90
+ reap_stale_connections
91
+ end
92
+ end
93
+ t.abort_on_exception = false
94
+ t
95
+ end
96
+
97
+ def reap_stale_connections
98
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
99
+ @pool_mutex.synchronize do
100
+ @pool.each_value do |entries|
101
+ entries.reject! do |entry|
102
+ if now - entry.checked_in_at >= KEEP_ALIVE_TIMEOUT
103
+ entry.http.finish rescue nil
104
+ true
105
+ else
106
+ false
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+
69
113
  def new_connection(uri)
70
114
  http = Net::HTTP.new(uri.host, uri.port)
71
115
  configure_ssl(http, uri)
@@ -108,33 +152,60 @@ module Pinot
108
152
  "Content-Type" => "application/json; charset=utf-8"
109
153
  }.freeze
110
154
 
111
- def initialize(http_client:, extra_headers: {}, timeout_ms: nil, logger: nil)
155
+ RETRYABLE_ERRORS = [
156
+ Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
157
+ Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout
158
+ ].freeze
159
+
160
+ def initialize(http_client:, extra_headers: {}, timeout_ms: nil, logger: nil,
161
+ max_retries: 0, retry_interval_ms: 200)
112
162
  @http_client = http_client
113
163
  @extra_headers = extra_headers
114
164
  @timeout_ms = timeout_ms
115
165
  @logger = logger
166
+ @max_retries = max_retries
167
+ @retry_interval_ms = retry_interval_ms
116
168
  end
117
169
 
118
170
  def execute(broker_address, request)
119
171
  logger.debug "Pinot query to #{broker_address}: #{request.query}"
120
172
 
121
- url = build_url(broker_address, request.query_format)
122
- body = build_body(request)
123
- headers = DEFAULT_HEADERS
124
- .merge(@extra_headers)
125
- .merge("X-Correlation-Id" => SecureRandom.uuid)
173
+ attempts = 0
174
+ max_attempts = (@max_retries || 0) + 1
126
175
 
127
- resp = @http_client.post(url, body: body, headers: headers)
176
+ begin
177
+ attempts += 1
128
178
 
129
- unless resp.code.to_i == 200
130
- logger.error "Pinot broker returned HTTP #{resp.code}"
131
- raise TransportError, "http exception with HTTP status code #{resp.code}"
132
- end
179
+ url = build_url(broker_address, request.query_format)
180
+ body = build_body(request)
181
+ headers = DEFAULT_HEADERS
182
+ .merge(@extra_headers)
183
+ .merge("X-Correlation-Id" => SecureRandom.uuid)
133
184
 
134
- begin
135
- BrokerResponse.from_json(resp.body)
136
- rescue JSON::ParserError => e
137
- raise e.message
185
+ resp = @http_client.post(url, body: body, headers: headers)
186
+
187
+ if resp.code == "503"
188
+ logger.error "Pinot broker returned HTTP #{resp.code}"
189
+ raise TransportError, "http exception with HTTP status code #{resp.code}"
190
+ end
191
+
192
+ unless resp.code.to_i == 200
193
+ logger.error "Pinot broker returned HTTP #{resp.code}"
194
+ raise TransportError, "http exception with HTTP status code #{resp.code}"
195
+ end
196
+
197
+ begin
198
+ BrokerResponse.from_json(resp.body)
199
+ rescue JSON::ParserError => e
200
+ raise e.message
201
+ end
202
+ rescue TransportError, *RETRYABLE_ERRORS => e
203
+ if attempts < max_attempts
204
+ sleep_ms = (@retry_interval_ms || 200) * (2 ** (attempts - 1))
205
+ sleep(sleep_ms / 1000.0)
206
+ retry
207
+ end
208
+ raise
138
209
  end
139
210
  end
140
211
 
data/lib/pinot/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pinot
2
- VERSION = "1.7.0"
2
+ VERSION = "1.10.0"
3
3
  end
data/lib/pinot.rb CHANGED
@@ -22,9 +22,9 @@ require_relative "pinot/connection"
22
22
  require_relative "pinot/prepared_statement"
23
23
  require_relative "pinot/connection_factory"
24
24
 
25
+ require_relative "pinot/grpc_config"
25
26
  begin
26
27
  require_relative "pinot/grpc_transport"
27
- require_relative "pinot/grpc_config"
28
28
  rescue LoadError
29
29
  # grpc gem not available; GrpcTransport disabled
30
30
  end
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.7.0
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xiang Fu