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 +4 -4
- data/README.md +66 -0
- data/lib/pinot/config.rb +8 -2
- data/lib/pinot/connection_factory.rb +6 -2
- data/lib/pinot/transport.rb +88 -17
- data/lib/pinot/version.rb +1 -1
- data/lib/pinot.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ddb501e91483838437691807ff09f98be16584f5b5e9e25c32982581db9fc226
|
|
4
|
+
data.tar.gz: 69f4df0c9fae29d42388b5fa6e1f55a1c722f70673c91b5ad43339762ad052fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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)
|
data/lib/pinot/transport.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
176
|
+
begin
|
|
177
|
+
attempts += 1
|
|
128
178
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
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
|