pinot-client 1.24.0 → 1.26.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: b893ea9b2cefc1aca8cf86940399d36be6e071e88adc8023affb660d94ad8ce7
4
- data.tar.gz: 539a393551125f7169975b0e641f4d1b60fd0cc6390fc1c9c10b4f86c4036db0
3
+ metadata.gz: 0c6d47938639a210b77881492e1155d26728be12f987b9721712f16503183ec3
4
+ data.tar.gz: 9e14c467edbba3ad7fff1e20f6b288ac810895c5069549cd33f08e2972b8d739
5
5
  SHA512:
6
- metadata.gz: 13d9d34e4a86ac6d62b15db000f4220e28cd5498e78cfdb42b2c9edcfac5645c68332a43048d96bc16089381fc4ea9c3d0ce429a95480c481766d1ebd9dfd6ca
7
- data.tar.gz: 679bde8940819c309cbc351712cf2c4d4f3cfa1601e81ae023e5bf03a944404bd907b1f0a6fa44c2d5925909f3b8ce6533adf2a91cfea5aa284be7ab86afc9cf
6
+ metadata.gz: 73ec7a9ae84e96f71618f430ed542527fb9f9e16f64b1d0d78a1a8bcb0ba1950f6d7117072f0318ffcc08fdb0df2143be268667d6b96e4914d52fc4f0448c0fa
7
+ data.tar.gz: 9fe83c991b5ef549ce915dbe4af55b50143d17d6d9660915d5fd6308798541f81068e557317b8b88af167b55a63ea7781ef14dfff428cd51b671df7d2a0267b2
@@ -50,6 +50,46 @@ module Pinot
50
50
  raise e
51
51
  end
52
52
 
53
+ def execute_many(queries, max_concurrency: nil)
54
+ return [] if queries.empty?
55
+
56
+ results = Array.new(queries.size)
57
+ # Queue acts as a counting semaphore: pre-filled with N tokens.
58
+ sem = max_concurrency ? build_semaphore(max_concurrency) : nil
59
+
60
+ threads = queries.each_with_index.map do |item, idx|
61
+ table = item[:table] || item["table"] || ""
62
+ query = item[:query] || item["query"] || ""
63
+ timeout_ms = item[:query_timeout_ms] || item["query_timeout_ms"]
64
+
65
+ Thread.new do
66
+ sem&.pop # acquire
67
+ begin
68
+ resp = execute_sql(table, query, query_timeout_ms: timeout_ms)
69
+ results[idx] = QueryResult.new(table: table, query: query, response: resp, error: nil)
70
+ rescue => e
71
+ results[idx] = QueryResult.new(table: table, query: query, response: nil, error: e)
72
+ ensure
73
+ sem&.push(:token) # release
74
+ end
75
+ end
76
+ end
77
+
78
+ threads.each(&:join)
79
+ results
80
+ end
81
+
82
+ def paginate(query, page_size: Paginator::DEFAULT_PAGE_SIZE, table: nil, extra_headers: {})
83
+ broker = @broker_selector.select_broker(table || "")
84
+ Paginator.new(
85
+ @transport.http_client,
86
+ broker,
87
+ query,
88
+ page_size: page_size,
89
+ extra_headers: extra_headers
90
+ )
91
+ end
92
+
53
93
  def prepare(table, query_template)
54
94
  raise ArgumentError, "table name cannot be empty" if table.nil? || table.strip.empty?
55
95
  raise ArgumentError, "query template cannot be empty" if query_template.nil? || query_template.strip.empty?
@@ -101,6 +141,12 @@ module Pinot
101
141
 
102
142
  private
103
143
 
144
+ def build_semaphore(n)
145
+ q = SizedQueue.new(n)
146
+ n.times { q.push(:token) }
147
+ q
148
+ end
149
+
104
150
  def run_with_circuit_breaker(broker, &block)
105
151
  return yield unless @circuit_breaker_registry
106
152
  @circuit_breaker_registry.for(broker).call(broker, &block)
@@ -0,0 +1,123 @@
1
+ module Pinot
2
+ # Implements Pinot's server-side cursor API.
3
+ #
4
+ # The broker stores the full result set and returns slices on demand.
5
+ # All fetch requests after the first must go to the same broker that
6
+ # owns the cursor state (brokerHost:brokerPort from the initial response).
7
+ #
8
+ # Usage:
9
+ # paginator = conn.paginate("SELECT * FROM t LIMIT 10000", page_size: 100)
10
+ # paginator.each_page { |resp| process(resp.result_table) }
11
+ # paginator.each_row { |row| puts row.map(&:to_s).join(", ") }
12
+ # paginator.delete # optional early cleanup; also called automatically on exhaustion
13
+ class Paginator
14
+ include Enumerable
15
+
16
+ DEFAULT_PAGE_SIZE = 1000
17
+
18
+ def initialize(http_client, broker_address, query, page_size:, extra_headers: {})
19
+ raise ArgumentError, "page_size must be a positive integer" unless page_size.is_a?(Integer) && page_size > 0
20
+
21
+ @http_client = http_client
22
+ @broker_address = broker_address
23
+ @query = query
24
+ @page_size = page_size
25
+ @extra_headers = extra_headers
26
+
27
+ @request_id = nil
28
+ @cursor_base = nil # "http://host:port" — set after first response
29
+ @exhausted = false
30
+ end
31
+
32
+ # Yields each page as a BrokerResponse. Returns an Enumerator without a block.
33
+ def each_page
34
+ return enum_for(:each_page) unless block_given?
35
+
36
+ # Submit the query and get the first page + cursor metadata
37
+ first = submit_cursor
38
+ return if first.result_table.nil? || first.result_table.rows.empty?
39
+
40
+ yield first
41
+
42
+ fetched = first.num_rows || first.result_table.rows.size
43
+ total = first.num_rows_result_set || 0
44
+
45
+ while fetched < total
46
+ page = fetch_page(fetched)
47
+ rows = page.result_table&.rows || []
48
+ break if rows.empty?
49
+
50
+ yield page
51
+
52
+ fetched += rows.size
53
+ break if rows.size < @page_size
54
+ end
55
+
56
+ delete
57
+ end
58
+
59
+ # Yields each row Array across all pages. Returns an Enumerator without a block.
60
+ # Aliased as #each so Enumerable methods (.map, .select, .to_a, etc.) work.
61
+ def each(&block)
62
+ return enum_for(:each) unless block_given?
63
+
64
+ each_page do |response|
65
+ response.result_table.rows.each(&block)
66
+ end
67
+ end
68
+
69
+ alias each_row each
70
+
71
+ # Delete the cursor from the broker early (also called automatically after exhaustion).
72
+ def delete
73
+ return unless @request_id && @cursor_base
74
+
75
+ url = "#{@cursor_base}/responseStore/#{@request_id}"
76
+ @http_client.delete(url, headers: json_headers)
77
+ @request_id = nil
78
+ rescue StandardError
79
+ # best-effort; cursor will expire naturally
80
+ end
81
+
82
+ private
83
+
84
+ def submit_cursor
85
+ base = broker_base(@broker_address)
86
+ url = "#{base}/query/sql?getCursor=true&numRows=#{@page_size}"
87
+ body = JSON.generate("sql" => @query)
88
+ resp = @http_client.post(url, body: body, headers: json_headers)
89
+
90
+ raise TransportError, "cursor submit returned HTTP #{resp.code}" unless resp.code.to_i == 200
91
+
92
+ parsed = BrokerResponse.from_json(resp.body)
93
+
94
+ @request_id = parsed.request_id
95
+ @cursor_base = broker_base_from_response(parsed) || base
96
+
97
+ parsed
98
+ end
99
+
100
+ def fetch_page(offset)
101
+ url = "#{@cursor_base}/responseStore/#{@request_id}/results?offset=#{offset}&numRows=#{@page_size}"
102
+ resp = @http_client.get(url, headers: json_headers)
103
+
104
+ raise TransportError, "cursor fetch returned HTTP #{resp.code}" unless resp.code.to_i == 200
105
+
106
+ BrokerResponse.from_json(resp.body)
107
+ end
108
+
109
+ def broker_base(address)
110
+ return address if address.start_with?("http://", "https://")
111
+ "http://#{address}"
112
+ end
113
+
114
+ def broker_base_from_response(resp)
115
+ return nil unless resp.broker_host && resp.broker_port
116
+ "http://#{resp.broker_host}:#{resp.broker_port}"
117
+ end
118
+
119
+ def json_headers
120
+ { "Content-Type" => "application/json; charset=utf-8" }.merge(@extra_headers)
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,11 @@
1
+ module Pinot
2
+ QueryResult = Struct.new(:table, :query, :response, :error, keyword_init: true) do
3
+ def success?
4
+ error.nil?
5
+ end
6
+
7
+ def error?
8
+ !error.nil?
9
+ end
10
+ end
11
+ end
@@ -188,7 +188,11 @@ module Pinot
188
188
  :num_docs_scanned, :num_entries_scanned_in_filter,
189
189
  :num_entries_scanned_post_filter, :total_docs,
190
190
  :time_used_ms, :min_consuming_freshness_time_ms,
191
- :num_groups_limit_reached
191
+ :num_groups_limit_reached,
192
+ # cursor fields — only present when getCursor=true
193
+ :request_id, :num_rows_result_set, :offset, :num_rows,
194
+ :broker_host, :broker_port,
195
+ :submission_time_ms, :expiration_time_ms
192
196
 
193
197
  def self.from_json(json_str)
194
198
  hash = JSON.parse(json_str)
@@ -215,6 +219,19 @@ module Pinot
215
219
  @total_docs = hash["totalDocs"] || 0
216
220
  @time_used_ms = hash["timeUsedMs"] || 0
217
221
  @min_consuming_freshness_time_ms = hash["minConsumingFreshnessTimeMs"] || 0
222
+
223
+ @request_id = hash["requestId"]
224
+ @num_rows_result_set = hash["numRowsResultSet"]
225
+ @offset = hash["offset"]
226
+ @num_rows = hash["numRows"]
227
+ @broker_host = hash["brokerHost"]
228
+ @broker_port = hash["brokerPort"]
229
+ @submission_time_ms = hash["submissionTimeMs"]
230
+ @expiration_time_ms = hash["expirationTimeMs"]
231
+ end
232
+
233
+ def cursor?
234
+ !@request_id.nil?
218
235
  end
219
236
  end
220
237
  end
@@ -40,6 +40,15 @@ module Pinot
40
40
  end
41
41
  end
42
42
 
43
+ def delete(url, headers: {})
44
+ uri = URI.parse(url)
45
+ with_connection(url) do |http|
46
+ req = Net::HTTP::Delete.new(uri.request_uri)
47
+ headers.each { |k, v| req[k] = v }
48
+ http.request(req)
49
+ end
50
+ end
51
+
43
52
  def close
44
53
  @reaper.kill rescue nil
45
54
  @pool_mutex.synchronize do
@@ -183,6 +192,8 @@ module Pinot
183
192
  # 250 = ExecutionTimeoutError (server-side), 400 = BrokerTimeoutError.
184
193
  TIMEOUT_ERROR_CODES = [250, 400].freeze
185
194
 
195
+ attr_reader :http_client
196
+
186
197
  def initialize(http_client:, extra_headers: {}, timeout_ms: nil, logger: nil,
187
198
  max_retries: 0, retry_interval_ms: 200)
188
199
  @http_client = http_client
data/lib/pinot/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pinot
2
- VERSION = "1.24.0"
2
+ VERSION = "1.26.0"
3
3
  end
data/lib/pinot.rb CHANGED
@@ -19,6 +19,8 @@ require_relative "pinot/controller_response"
19
19
  require_relative "pinot/controller_based_broker_selector"
20
20
  require_relative "pinot/transport"
21
21
  require_relative "pinot/circuit_breaker"
22
+ require_relative "pinot/query_result"
23
+ require_relative "pinot/paginator"
22
24
  require_relative "pinot/connection"
23
25
  require_relative "pinot/prepared_statement"
24
26
  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.24.0
4
+ version: 1.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xiang Fu
@@ -115,10 +115,12 @@ files:
115
115
  - lib/pinot/grpc_transport.rb
116
116
  - lib/pinot/instrumentation.rb
117
117
  - lib/pinot/logger.rb
118
+ - lib/pinot/paginator.rb
118
119
  - lib/pinot/prepared_statement.rb
119
120
  - lib/pinot/proto/broker_service.proto
120
121
  - lib/pinot/proto/broker_service_pb.rb
121
122
  - lib/pinot/proto/broker_service_services_pb.rb
123
+ - lib/pinot/query_result.rb
122
124
  - lib/pinot/request.rb
123
125
  - lib/pinot/response.rb
124
126
  - lib/pinot/simple_broker_selector.rb