pinot-client 1.6.0 → 1.8.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: 2ea9db0f69b7fb8d963190553d6a92201f4d63d5d6a07a975f862d00e43aa30a
4
- data.tar.gz: c3b968737e6b7650c57bb79ef42153eb6870df22521939f885e5332fe4a2b7eb
3
+ metadata.gz: 819824203883477127807cc1a86d8535f7721883dfaec85423c5b333dd453b6e
4
+ data.tar.gz: dff26b09b035ad4d217a63b2b661677d4b47922760f2f140a6cda942d9383a20
5
5
  SHA512:
6
- metadata.gz: b5809f8a9245ff2b13cd6a487499aff37ad9f879d4c5537570f61b1c34da6fe702817bb3ced5d6c812f8b38b9283bafb1bb3a16f2ae1f9279436e9cf86f274ae
7
- data.tar.gz: 78c2573048ced129a60d9186092acba394456304cc343f1fca56cce1fa0bc8e3417c8748b3bd50a97f6dcd5f05f32caaf2c0f2a12e153e5685876e06aa033f31
6
+ metadata.gz: 606903f919bd310a8b1c208206db2c4ac25b5522ee26079435966f95f171573b0800266970ca3643c29528f506cf2a2c392b7161f7f2c3a174c313732a53721e
7
+ data.tar.gz: bc99e8c6ba493de6a534009e557915f9e72a57298129837750c49d8e1026dc8785822b079c63d05c42842a5b800930c44ac415bdf487e622fe1518f5cbbbf867
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, :query_timeout_ms
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: [],
@@ -35,7 +37,8 @@ module Pinot
35
37
  tls_config: nil,
36
38
  grpc_config: nil,
37
39
  zookeeper_config: nil,
38
- query_timeout_ms: nil
40
+ max_retries: 0,
41
+ retry_interval_ms: 200
39
42
  )
40
43
  @broker_list = broker_list
41
44
  @http_timeout = http_timeout
@@ -48,6 +51,8 @@ module Pinot
48
51
  @grpc_config = grpc_config
49
52
  @zookeeper_config = zookeeper_config
50
53
  @query_timeout_ms = query_timeout_ms
54
+ @max_retries = max_retries
55
+ @retry_interval_ms = retry_interval_ms
51
56
  end
52
57
 
53
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)
@@ -108,33 +108,60 @@ module Pinot
108
108
  "Content-Type" => "application/json; charset=utf-8"
109
109
  }.freeze
110
110
 
111
- def initialize(http_client:, extra_headers: {}, timeout_ms: nil, logger: nil)
111
+ RETRYABLE_ERRORS = [
112
+ Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
113
+ Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout
114
+ ].freeze
115
+
116
+ def initialize(http_client:, extra_headers: {}, timeout_ms: nil, logger: nil,
117
+ max_retries: 0, retry_interval_ms: 200)
112
118
  @http_client = http_client
113
119
  @extra_headers = extra_headers
114
120
  @timeout_ms = timeout_ms
115
121
  @logger = logger
122
+ @max_retries = max_retries
123
+ @retry_interval_ms = retry_interval_ms
116
124
  end
117
125
 
118
126
  def execute(broker_address, request)
119
127
  logger.debug "Pinot query to #{broker_address}: #{request.query}"
120
128
 
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)
129
+ attempts = 0
130
+ max_attempts = (@max_retries || 0) + 1
126
131
 
127
- resp = @http_client.post(url, body: body, headers: headers)
132
+ begin
133
+ attempts += 1
128
134
 
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
135
+ url = build_url(broker_address, request.query_format)
136
+ body = build_body(request)
137
+ headers = DEFAULT_HEADERS
138
+ .merge(@extra_headers)
139
+ .merge("X-Correlation-Id" => SecureRandom.uuid)
133
140
 
134
- begin
135
- BrokerResponse.from_json(resp.body)
136
- rescue JSON::ParserError => e
137
- raise e.message
141
+ resp = @http_client.post(url, body: body, headers: headers)
142
+
143
+ if resp.code == "503"
144
+ logger.error "Pinot broker returned HTTP #{resp.code}"
145
+ raise TransportError, "http exception with HTTP status code #{resp.code}"
146
+ end
147
+
148
+ unless resp.code.to_i == 200
149
+ logger.error "Pinot broker returned HTTP #{resp.code}"
150
+ raise TransportError, "http exception with HTTP status code #{resp.code}"
151
+ end
152
+
153
+ begin
154
+ BrokerResponse.from_json(resp.body)
155
+ rescue JSON::ParserError => e
156
+ raise e.message
157
+ end
158
+ rescue TransportError, *RETRYABLE_ERRORS => e
159
+ if attempts < max_attempts
160
+ sleep_ms = (@retry_interval_ms || 200) * (2 ** (attempts - 1))
161
+ sleep(sleep_ms / 1000.0)
162
+ retry
163
+ end
164
+ raise
138
165
  end
139
166
  end
140
167
 
data/lib/pinot/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pinot
2
- VERSION = "1.6.0"
2
+ VERSION = "1.8.0"
3
3
  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.6.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xiang Fu