pinot-client 1.0.2 → 1.1.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: 1859bffb975457a69f7eb417283f2e4671c5f53b1f7e12b9509ea0704f89c74f
4
- data.tar.gz: ed8f59fe262df5a4a1133b8dfcac0765066c20528d6f7c9e2fd0326524bf0999
3
+ metadata.gz: 2f2e2377c6f9af1cb73b472bd5b7c9ad2bfa512ec0594c67d3298c884bee5f0a
4
+ data.tar.gz: 608b9a1f070dd22d71a793ad1e4817dcdbe4100b14e3aa4033e71643fc2de9a8
5
5
  SHA512:
6
- metadata.gz: 9e64c60e9cff0130ae77c31fe7898223d9dfe4c95015fb6a40e8a54b1afb3de601c1b58fbde3940e132c07365ebd19cbf56e35746a50d15e3b76913a187111d5
7
- data.tar.gz: 0653ae3c8241944ceaa4fc982c45e277ca057c4a938754c4b5873d1cf75257c720cd8adeeabb0315ca2401e2268270bad3fc394f9ae6eebab868fdfa8f257490
6
+ metadata.gz: bd247b8088c5be2b20d78e95e0cc16effd8b8c24e136810101f5a136d630dcbca002127ace5ea56f98a9c9308a0d7ae42c20d16a32d9ebba8ad3f11656ceff08
7
+ data.tar.gz: 02ecaf1df43b2117a2ad86cf481ec241412668c3e96a49fc046e2a730fd92e6d44edad99f34f9ab68eac44d457cd88d5acda636226241ab7bbe82206ae99ea0c
data/README.md CHANGED
@@ -106,6 +106,16 @@ client.use_multistage_engine = true
106
106
  resp = client.execute_sql("baseballStats", "SELECT teamID, count(*) FROM baseballStats GROUP BY teamID")
107
107
  ```
108
108
 
109
+ Or enable it upfront via `ClientConfig`:
110
+
111
+ ```ruby
112
+ config = Pinot::ClientConfig.new(
113
+ broker_list: ["localhost:8000"],
114
+ use_multistage_engine: true
115
+ )
116
+ client = Pinot.from_config(config)
117
+ ```
118
+
109
119
  ### Trace
110
120
 
111
121
  ```ruby
data/lib/pinot/config.rb CHANGED
@@ -11,20 +11,24 @@ module Pinot
11
11
 
12
12
  class ClientConfig
13
13
  attr_accessor :broker_list, :http_timeout, :extra_http_header,
14
- :use_multistage_engine, :controller_config
14
+ :use_multistage_engine, :controller_config, :logger, :tls_config
15
15
 
16
16
  def initialize(
17
17
  broker_list: [],
18
18
  http_timeout: nil,
19
19
  extra_http_header: {},
20
20
  use_multistage_engine: false,
21
- controller_config: nil
21
+ controller_config: nil,
22
+ logger: nil,
23
+ tls_config: nil
22
24
  )
23
25
  @broker_list = broker_list
24
26
  @http_timeout = http_timeout
25
27
  @extra_http_header = extra_http_header
26
28
  @use_multistage_engine = use_multistage_engine
27
29
  @controller_config = controller_config
30
+ @logger = logger
31
+ @tls_config = tls_config
28
32
  end
29
33
  end
30
34
  end
@@ -2,11 +2,12 @@ require "bigdecimal"
2
2
 
3
3
  module Pinot
4
4
  class Connection
5
- def initialize(transport:, broker_selector:, use_multistage_engine: false)
5
+ def initialize(transport:, broker_selector:, use_multistage_engine: false, logger: nil)
6
6
  @transport = transport
7
7
  @broker_selector = broker_selector
8
8
  @use_multistage_engine = use_multistage_engine
9
9
  @trace = false
10
+ @logger = logger
10
11
  end
11
12
 
12
13
  def use_multistage_engine=(val)
@@ -22,6 +23,7 @@ module Pinot
22
23
  end
23
24
 
24
25
  def execute_sql(table, query)
26
+ logger.debug "Executing SQL on table=#{table}: #{query}"
25
27
  broker = @broker_selector.select_broker(table)
26
28
  @transport.execute(broker, build_request(query))
27
29
  rescue => e
@@ -87,6 +89,10 @@ module Pinot
87
89
 
88
90
  private
89
91
 
92
+ def logger
93
+ @logger || Pinot::Logging.logger
94
+ end
95
+
90
96
  def build_request(query)
91
97
  Request.new("sql", query, @trace, @use_multistage_engine)
92
98
  end
@@ -12,20 +12,22 @@ module Pinot
12
12
  end
13
13
 
14
14
  def self.from_config(config, http_client: nil)
15
- inner = http_client || HttpClient.new(timeout: config.http_timeout)
15
+ inner = http_client || HttpClient.new(timeout: config.http_timeout, tls_config: config.tls_config)
16
16
 
17
17
  transport = JsonHttpTransport.new(
18
18
  http_client: inner,
19
- extra_headers: config.extra_http_header || {}
19
+ extra_headers: config.extra_http_header || {},
20
+ logger: config.logger
20
21
  )
21
22
 
22
23
  selector = build_selector(config, inner)
23
- raise ArgumentError, "must specify broker_list or controller_config" unless selector
24
+ raise ConfigurationError, "must specify broker_list or controller_config" unless selector
24
25
 
25
26
  conn = Connection.new(
26
27
  transport: transport,
27
28
  broker_selector: selector,
28
- use_multistage_engine: config.use_multistage_engine || false
29
+ use_multistage_engine: config.use_multistage_engine || false,
30
+ logger: config.logger
29
31
  )
30
32
 
31
33
  selector.init
@@ -36,7 +38,7 @@ module Pinot
36
38
  if config.broker_list && !config.broker_list.empty?
37
39
  SimpleBrokerSelector.new(config.broker_list)
38
40
  elsif config.controller_config
39
- ControllerBasedBrokerSelector.new(config.controller_config, http_client)
41
+ ControllerBasedBrokerSelector.new(config.controller_config, http_client, logger: config.logger)
40
42
  end
41
43
  end
42
44
  private_class_method :build_selector
@@ -7,16 +7,18 @@ module Pinot
7
7
  CONTROLLER_API_PATH = "/v2/brokers/tables?state=ONLINE"
8
8
  DEFAULT_UPDATE_FREQ_MS = 1000
9
9
 
10
- def initialize(config, http_client = nil)
10
+ def initialize(config, http_client = nil, logger: nil)
11
11
  super()
12
12
  @config = config
13
13
  @internal_http = http_client || HttpClient.new
14
+ @logger = logger
14
15
  end
15
16
 
16
17
  def init
17
18
  @config.update_freq_ms ||= DEFAULT_UPDATE_FREQ_MS
18
19
  @controller_url = build_controller_url(@config.controller_address)
19
20
  fetch_and_update
21
+ logger.info "ControllerBasedBrokerSelector initialized with #{@all_broker_list.size} brokers"
20
22
  start_background_refresh
21
23
  nil
22
24
  end
@@ -26,7 +28,7 @@ module Pinot
26
28
  if addr.include?("://")
27
29
  scheme = addr.split("://").first
28
30
  unless %w[http https].include?(scheme)
29
- raise "unsupported controller URL scheme: #{scheme}"
31
+ raise ConfigurationError, "unsupported controller URL scheme: #{scheme}"
30
32
  end
31
33
  addr.chomp("/") + CONTROLLER_API_PATH
32
34
  else
@@ -43,20 +45,24 @@ module Pinot
43
45
  resp = @internal_http.get(@controller_url, headers: headers)
44
46
 
45
47
  unless resp.code.to_i == 200
46
- raise "controller API returned HTTP status code #{resp.code}"
48
+ raise TransportError, "controller API returned HTTP status code #{resp.code}"
47
49
  end
48
50
 
49
51
  body = resp.body
50
52
  begin
51
53
  raw = JSON.parse(body)
52
54
  rescue JSON::ParserError => e
53
- raise "error decoding controller API response: #{e.message}"
55
+ raise ConfigurationError, "error decoding controller API response: #{e.message}"
54
56
  end
55
57
 
56
58
  cr = ControllerResponse.new(raw)
57
59
  update_broker_data(cr.extract_broker_list, cr.extract_table_to_broker_map)
58
60
  end
59
61
 
62
+ def logger
63
+ @logger || Pinot::Logging.logger
64
+ end
65
+
60
66
  def start_background_refresh
61
67
  interval = @config.update_freq_ms / 1000.0
62
68
  Thread.new do
@@ -65,7 +71,7 @@ module Pinot
65
71
  begin
66
72
  fetch_and_update
67
73
  rescue => e
68
- warn "Pinot: error refreshing broker data: #{e.message}"
74
+ logger.warn "Pinot controller refresh failed: #{e.message}"
69
75
  end
70
76
  end
71
77
  end
@@ -0,0 +1,8 @@
1
+ module Pinot
2
+ class Error < StandardError; end
3
+ class BrokerNotFoundError < Error; end
4
+ class TableNotFoundError < Error; end
5
+ class TransportError < Error; end
6
+ class PreparedStatementClosedError < Error; end
7
+ class ConfigurationError < Error; end
8
+ end
@@ -0,0 +1,17 @@
1
+ require "logger"
2
+
3
+ module Pinot
4
+ module Logging
5
+ def self.logger
6
+ @logger ||= begin
7
+ l = Logger.new($stdout)
8
+ l.level = Logger::WARN
9
+ l
10
+ end
11
+ end
12
+
13
+ def self.logger=(logger)
14
+ @logger = logger
15
+ end
16
+ end
17
+ end
@@ -51,7 +51,7 @@ module Pinot
51
51
 
52
52
  def set(index, value)
53
53
  @mutex.synchronize do
54
- raise "prepared statement is closed" if @closed
54
+ raise PreparedStatementClosedError, "prepared statement is closed" if @closed
55
55
  unless index >= 1 && index <= @param_count
56
56
  raise "parameter index #{index} is out of range [1, #{@param_count}]"
57
57
  end
@@ -62,7 +62,7 @@ module Pinot
62
62
 
63
63
  def execute
64
64
  @mutex.synchronize do
65
- raise "prepared statement is closed" if @closed
65
+ raise PreparedStatementClosedError, "prepared statement is closed" if @closed
66
66
  @parameters.each_with_index do |p, i|
67
67
  raise "parameter at index #{i + 1} is not set" if p.nil?
68
68
  end
@@ -76,7 +76,7 @@ module Pinot
76
76
  end
77
77
 
78
78
  def execute_with_params(*params)
79
- @mutex.synchronize { raise "prepared statement is closed" if @closed }
79
+ @mutex.synchronize { raise PreparedStatementClosedError, "prepared statement is closed" if @closed }
80
80
  if params.length != @param_count
81
81
  raise "expected #{@param_count} parameters, got #{params.length}"
82
82
  end
@@ -90,7 +90,7 @@ module Pinot
90
90
 
91
91
  def clear_parameters
92
92
  @mutex.synchronize do
93
- raise "prepared statement is closed" if @closed
93
+ raise PreparedStatementClosedError, "prepared statement is closed" if @closed
94
94
  @parameters.fill(nil)
95
95
  end
96
96
  nil
@@ -7,11 +7,11 @@ module Pinot
7
7
  end
8
8
 
9
9
  def init
10
- raise "no pre-configured broker lists" if @broker_list.empty?
10
+ raise BrokerNotFoundError, "no pre-configured broker lists" if @broker_list.empty?
11
11
  end
12
12
 
13
13
  def select_broker(_table)
14
- raise "no pre-configured broker lists" if @broker_list.empty?
14
+ raise BrokerNotFoundError, "no pre-configured broker lists" if @broker_list.empty?
15
15
  @broker_list.sample
16
16
  end
17
17
  end
@@ -19,12 +19,12 @@ module Pinot
19
19
  table_name = extract_table_name(table.to_s)
20
20
  @mutex.synchronize do
21
21
  if table_name.empty?
22
- raise "no available broker" if @all_broker_list.empty?
22
+ raise BrokerNotFoundError, "no available broker" if @all_broker_list.empty?
23
23
  return @all_broker_list.sample
24
24
  end
25
25
  brokers = @table_broker_map[table_name]
26
- raise "unable to find table: #{table}" unless brokers
27
- raise "no available broker for table: #{table}" if brokers.empty?
26
+ raise TableNotFoundError, "unable to find table: #{table}" unless brokers
27
+ raise BrokerNotFoundError, "no available broker for table: #{table}" if brokers.empty?
28
28
  brokers.sample
29
29
  end
30
30
  end
@@ -0,0 +1,15 @@
1
+ module Pinot
2
+ class TlsConfig
3
+ attr_accessor :ca_cert_file, # path to CA cert PEM file (String, optional)
4
+ :client_cert_file, # path to client cert PEM file (String, optional)
5
+ :client_key_file, # path to client key PEM file (String, optional)
6
+ :insecure_skip_verify # boolean, skip server cert verification (default: false)
7
+
8
+ def initialize(ca_cert_file: nil, client_cert_file: nil, client_key_file: nil, insecure_skip_verify: false)
9
+ @ca_cert_file = ca_cert_file
10
+ @client_cert_file = client_cert_file
11
+ @client_key_file = client_key_file
12
+ @insecure_skip_verify = insecure_skip_verify
13
+ end
14
+ end
15
+ end
@@ -2,38 +2,104 @@ require "net/http"
2
2
  require "uri"
3
3
  require "json"
4
4
  require "securerandom"
5
+ require "openssl"
5
6
 
6
7
  module Pinot
7
8
  class HttpClient
8
- def initialize(timeout: nil)
9
+ MAX_POOL_SIZE = 5
10
+ KEEP_ALIVE_TIMEOUT = 30
11
+
12
+ def initialize(timeout: nil, tls_config: nil)
9
13
  @timeout = timeout
14
+ @tls_config = tls_config
15
+ @pool = {}
16
+ @pool_mutex = Mutex.new
10
17
  end
11
18
 
12
19
  def post(url, body:, headers: {})
13
20
  uri = URI.parse(url)
14
- http = Net::HTTP.new(uri.host, uri.port)
15
- http.use_ssl = uri.scheme == "https"
16
- if @timeout
17
- http.open_timeout = @timeout
18
- http.read_timeout = @timeout
21
+ with_connection(url) do |http|
22
+ req = Net::HTTP::Post.new(uri.request_uri)
23
+ headers.each { |k, v| req[k] = v }
24
+ req.body = body
25
+ http.request(req)
19
26
  end
20
- req = Net::HTTP::Post.new(uri.request_uri)
21
- headers.each { |k, v| req[k] = v }
22
- req.body = body
23
- http.request(req)
24
27
  end
25
28
 
26
29
  def get(url, headers: {})
27
30
  uri = URI.parse(url)
31
+ with_connection(url) do |http|
32
+ req = Net::HTTP::Get.new(uri.request_uri)
33
+ headers.each { |k, v| req[k] = v }
34
+ http.request(req)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def with_connection(url)
41
+ uri = URI.parse(url)
42
+ key = "#{uri.host}:#{uri.port}"
43
+ http = checkout(key, uri)
44
+ begin
45
+ result = yield http
46
+ checkin(key, http)
47
+ result
48
+ rescue => e
49
+ http.finish rescue nil
50
+ raise e
51
+ end
52
+ end
53
+
54
+ def checkout(key, uri)
55
+ @pool_mutex.synchronize { @pool[key]&.pop } || new_connection(uri)
56
+ end
57
+
58
+ def checkin(key, http)
59
+ @pool_mutex.synchronize do
60
+ pool_for_key = @pool[key] ||= []
61
+ if pool_for_key.size < MAX_POOL_SIZE
62
+ pool_for_key.push(http)
63
+ else
64
+ http.finish rescue nil
65
+ end
66
+ end
67
+ end
68
+
69
+ def new_connection(uri)
28
70
  http = Net::HTTP.new(uri.host, uri.port)
29
- http.use_ssl = uri.scheme == "https"
71
+ configure_ssl(http, uri)
30
72
  if @timeout
31
73
  http.open_timeout = @timeout
32
74
  http.read_timeout = @timeout
75
+ http.write_timeout = @timeout
76
+ end
77
+ http.start
78
+ http
79
+ end
80
+
81
+ def configure_ssl(http, uri)
82
+ if uri.scheme == "https"
83
+ http.use_ssl = true
84
+ if @tls_config
85
+ if @tls_config.ca_cert_file
86
+ store = OpenSSL::X509::Store.new
87
+ store.add_file(@tls_config.ca_cert_file)
88
+ http.cert_store = store
89
+ end
90
+ if @tls_config.client_cert_file && @tls_config.client_key_file
91
+ http.cert = OpenSSL::X509::Certificate.new(File.read(@tls_config.client_cert_file))
92
+ http.key = OpenSSL::PKey.read(File.read(@tls_config.client_key_file))
93
+ end
94
+ if @tls_config.insecure_skip_verify
95
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
96
+ else
97
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
98
+ end
99
+ end
100
+ else
101
+ http.use_ssl = false
33
102
  end
34
- req = Net::HTTP::Get.new(uri.request_uri)
35
- headers.each { |k, v| req[k] = v }
36
- http.request(req)
37
103
  end
38
104
  end
39
105
 
@@ -42,13 +108,16 @@ module Pinot
42
108
  "Content-Type" => "application/json; charset=utf-8"
43
109
  }.freeze
44
110
 
45
- def initialize(http_client:, extra_headers: {}, timeout_ms: nil)
111
+ def initialize(http_client:, extra_headers: {}, timeout_ms: nil, logger: nil)
46
112
  @http_client = http_client
47
113
  @extra_headers = extra_headers
48
114
  @timeout_ms = timeout_ms
115
+ @logger = logger
49
116
  end
50
117
 
51
118
  def execute(broker_address, request)
119
+ logger.debug "Pinot query to #{broker_address}: #{request.query}"
120
+
52
121
  url = build_url(broker_address, request.query_format)
53
122
  body = build_body(request)
54
123
  headers = DEFAULT_HEADERS
@@ -58,7 +127,8 @@ module Pinot
58
127
  resp = @http_client.post(url, body: body, headers: headers)
59
128
 
60
129
  unless resp.code.to_i == 200
61
- raise "http exception with HTTP status code #{resp.code}"
130
+ logger.error "Pinot broker returned HTTP #{resp.code}"
131
+ raise TransportError, "http exception with HTTP status code #{resp.code}"
62
132
  end
63
133
 
64
134
  begin
@@ -70,6 +140,10 @@ module Pinot
70
140
 
71
141
  private
72
142
 
143
+ def logger
144
+ @logger || Pinot::Logging.logger
145
+ end
146
+
73
147
  def build_url(broker_address, query_format)
74
148
  base = if broker_address.start_with?("http://", "https://")
75
149
  broker_address
data/lib/pinot/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pinot
2
- VERSION = "1.0.2"
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/pinot.rb CHANGED
@@ -4,8 +4,11 @@ require "json"
4
4
  require "bigdecimal"
5
5
  require "securerandom"
6
6
 
7
+ require_relative "pinot/errors"
7
8
  require_relative "pinot/version"
9
+ require_relative "pinot/logger"
8
10
  require_relative "pinot/config"
11
+ require_relative "pinot/tls_config"
9
12
  require_relative "pinot/request"
10
13
  require_relative "pinot/response"
11
14
  require_relative "pinot/broker_selector"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pinot-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xiang Fu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-18 00:00:00.000000000 Z
11
+ date: 2026-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logger
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rspec
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -67,11 +81,14 @@ files:
67
81
  - lib/pinot/connection_factory.rb
68
82
  - lib/pinot/controller_based_broker_selector.rb
69
83
  - lib/pinot/controller_response.rb
84
+ - lib/pinot/errors.rb
85
+ - lib/pinot/logger.rb
70
86
  - lib/pinot/prepared_statement.rb
71
87
  - lib/pinot/request.rb
72
88
  - lib/pinot/response.rb
73
89
  - lib/pinot/simple_broker_selector.rb
74
90
  - lib/pinot/table_aware_broker_selector.rb
91
+ - lib/pinot/tls_config.rb
75
92
  - lib/pinot/transport.rb
76
93
  - lib/pinot/version.rb
77
94
  homepage: https://github.com/startreedata/ruby-pinot-client