pinot-client 1.29.0 → 1.30.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/lib/pinot/railtie.rb +105 -0
- data/lib/pinot/response.rb +4 -10
- data/lib/pinot/schema_client.rb +122 -0
- data/lib/pinot/version.rb +1 -1
- data/lib/pinot.rb +3 -0
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf947863b8d33ffa5c029bdd62e51f3451b76320a0b5f46c1d48ba8994827767
|
|
4
|
+
data.tar.gz: 433c4df6222017a3e32069410e550146ffa249ba6b34c9cb12bbd49b89933191
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ada0f4344c5482daafefdf4cb062e80601723119b2732df5d5e116d1247931076db9eed6f3c393e6af3befcde14dedaac66a7a1fa90e7a0a6e8a65ce81e4eab0
|
|
7
|
+
data.tar.gz: fe0699c5d2fe75b1cb0aeeb832dcb57eb8b7fe44e33801171c76a7006e99cee08f34476134d1e87e2dccce8695a8dfc4b7b642436c5faebbe60246c3cbebd627
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require "pinot"
|
|
2
|
+
require "pinot/active_support_notifications"
|
|
3
|
+
|
|
4
|
+
module Pinot
|
|
5
|
+
# Rails integration via Railtie — zero configuration required.
|
|
6
|
+
#
|
|
7
|
+
# Automatically activated when the gem is required inside a Rails app.
|
|
8
|
+
# Just add `gem "pinot-client"` to your Gemfile.
|
|
9
|
+
#
|
|
10
|
+
# == What it does
|
|
11
|
+
#
|
|
12
|
+
# 1. **ActiveSupport::Notifications bridge** — every Pinot query fires a
|
|
13
|
+
# "sql.pinot" event on the AS::N bus, picked up by Rails log subscribers,
|
|
14
|
+
# Skylight, Scout APM, etc.
|
|
15
|
+
#
|
|
16
|
+
# 2. **OpenTelemetry bridge** — when the opentelemetry-api gem is present,
|
|
17
|
+
# creates "pinot.query" spans and injects W3C trace-context headers into
|
|
18
|
+
# every outbound broker request.
|
|
19
|
+
#
|
|
20
|
+
# 3. **X-Request-Id propagation** — inserts Pinot::RequestIdMiddleware into
|
|
21
|
+
# the Rack stack. The current request's X-Request-Id is forwarded as an
|
|
22
|
+
# HTTP header on every broker call so Pinot broker logs can be correlated
|
|
23
|
+
# with application request logs.
|
|
24
|
+
#
|
|
25
|
+
# == Opting out of individual features
|
|
26
|
+
#
|
|
27
|
+
# # config/initializers/pinot.rb
|
|
28
|
+
# Rails.application.config.pinot.notifications = false
|
|
29
|
+
# Rails.application.config.pinot.open_telemetry = false
|
|
30
|
+
# Rails.application.config.pinot.request_id = false
|
|
31
|
+
#
|
|
32
|
+
# == Manual setup (non-Rails / opt-out of Railtie entirely)
|
|
33
|
+
#
|
|
34
|
+
# require "pinot/active_support_notifications"
|
|
35
|
+
# Pinot::ActiveSupportNotifications.install!
|
|
36
|
+
#
|
|
37
|
+
# require "pinot/open_telemetry"
|
|
38
|
+
# Pinot::OpenTelemetry.install!
|
|
39
|
+
#
|
|
40
|
+
# # In your Rack middleware stack:
|
|
41
|
+
# use Pinot::RequestIdMiddleware
|
|
42
|
+
class Railtie < ::Rails::Railtie
|
|
43
|
+
initializer "pinot.install_notifications" do |app|
|
|
44
|
+
opts = app.config.pinot
|
|
45
|
+
next if opts[:notifications] == false
|
|
46
|
+
|
|
47
|
+
require "pinot/active_support_notifications"
|
|
48
|
+
ActiveSupportNotifications.install!
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
initializer "pinot.install_open_telemetry" do |app|
|
|
52
|
+
opts = app.config.pinot
|
|
53
|
+
next if opts[:open_telemetry] == false
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
require "opentelemetry"
|
|
57
|
+
require "pinot/open_telemetry"
|
|
58
|
+
OpenTelemetry.install!
|
|
59
|
+
rescue LoadError
|
|
60
|
+
# opentelemetry-api gem not present — skip silently
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
initializer "pinot.request_id_propagation" do |app|
|
|
65
|
+
opts = app.config.pinot
|
|
66
|
+
next if opts[:request_id] == false
|
|
67
|
+
|
|
68
|
+
app.config.middleware.use(RequestIdMiddleware)
|
|
69
|
+
Connection.prepend(RequestIdInjector)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Rack middleware that captures X-Request-Id from the inbound HTTP request
|
|
74
|
+
# and stores it in a thread-local for the duration of the request.
|
|
75
|
+
#
|
|
76
|
+
# Inserted automatically by Pinot::Railtie. For non-Rails Rack apps:
|
|
77
|
+
#
|
|
78
|
+
# use Pinot::RequestIdMiddleware
|
|
79
|
+
class RequestIdMiddleware
|
|
80
|
+
RACK_HEADER = "HTTP_X_REQUEST_ID".freeze
|
|
81
|
+
|
|
82
|
+
def initialize(app)
|
|
83
|
+
@app = app
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def call(env)
|
|
87
|
+
Thread.current[:pinot_request_id] = env[RACK_HEADER]
|
|
88
|
+
@app.call(env)
|
|
89
|
+
ensure
|
|
90
|
+
Thread.current[:pinot_request_id] = nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Prepended into Connection when the Railtie is active.
|
|
95
|
+
# Automatically merges the current request's X-Request-Id into every
|
|
96
|
+
# outbound Pinot query as an HTTP header, without requiring callers to
|
|
97
|
+
# pass it explicitly.
|
|
98
|
+
module RequestIdInjector
|
|
99
|
+
def execute_sql(table, query, query_timeout_ms: nil, headers: {})
|
|
100
|
+
rid = Thread.current[:pinot_request_id]
|
|
101
|
+
merged = rid && !headers.key?("X-Request-Id") ? headers.merge("X-Request-Id" => rid) : headers
|
|
102
|
+
super(table, query, query_timeout_ms: query_timeout_ms, headers: merged)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/pinot/response.rb
CHANGED
|
@@ -110,11 +110,8 @@ module Pinot
|
|
|
110
110
|
if raw.include?(".") || raw.include?("e") || raw.include?("E")
|
|
111
111
|
# Floating point string — check if it's a whole number
|
|
112
112
|
bd = BigDecimal(raw)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
rescue StandardError
|
|
116
|
-
return 0
|
|
117
|
-
end
|
|
113
|
+
return 0 if bd.infinite? || bd.nan?
|
|
114
|
+
|
|
118
115
|
int_val = bd.to_i
|
|
119
116
|
return 0 unless bd == BigDecimal(int_val.to_s)
|
|
120
117
|
return 0 if int_val > INT32_MAX || int_val < INT32_MIN
|
|
@@ -139,11 +136,8 @@ module Pinot
|
|
|
139
136
|
begin
|
|
140
137
|
if raw.include?(".") || raw.include?("e") || raw.include?("E")
|
|
141
138
|
bd = BigDecimal(raw)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
rescue StandardError
|
|
145
|
-
return 0
|
|
146
|
-
end
|
|
139
|
+
return 0 if bd.infinite? || bd.nan?
|
|
140
|
+
|
|
147
141
|
int_val = bd.to_i
|
|
148
142
|
return 0 unless bd == BigDecimal(int_val.to_s)
|
|
149
143
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Pinot
|
|
6
|
+
# Thin client for the Pinot Controller REST API — table listing and schema
|
|
7
|
+
# introspection. Useful for tooling, migrations, and debugging column types.
|
|
8
|
+
#
|
|
9
|
+
# == Usage
|
|
10
|
+
#
|
|
11
|
+
# client = Pinot::SchemaClient.new("http://controller:9000")
|
|
12
|
+
#
|
|
13
|
+
# client.list_tables # => ["baseballStats", "orders", ...]
|
|
14
|
+
# client.get_schema("baseballStats") # => Hash (raw schema JSON)
|
|
15
|
+
# client.get_table_config("baseballStats")# => Hash (raw tableConfig JSON)
|
|
16
|
+
# client.table_exists?("orders") # => true / false
|
|
17
|
+
#
|
|
18
|
+
# == Authentication / extra headers
|
|
19
|
+
#
|
|
20
|
+
# client = Pinot::SchemaClient.new(
|
|
21
|
+
# "https://controller:9000",
|
|
22
|
+
# headers: { "Authorization" => "Bearer <token>" }
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# The client is intentionally stateless and lightweight — it uses a shared
|
|
26
|
+
# HttpClient (connection pool) but does not perform background polling.
|
|
27
|
+
class SchemaClient
|
|
28
|
+
TABLES_PATH = "/tables".freeze
|
|
29
|
+
SCHEMA_PATH = "/schemas/%<table>s".freeze
|
|
30
|
+
TABLE_CONFIG_PATH = "/tables/%<table>s".freeze
|
|
31
|
+
|
|
32
|
+
# @param controller_address [String] base URL e.g. "controller:9000" or
|
|
33
|
+
# "http://controller:9000" or "https://controller:9000"
|
|
34
|
+
# @param headers [Hash] extra HTTP headers for every request
|
|
35
|
+
# @param http_client [HttpClient, nil] optional pre-configured client
|
|
36
|
+
def initialize(controller_address, headers: {}, http_client: nil)
|
|
37
|
+
@base = normalize_address(controller_address)
|
|
38
|
+
@headers = { "Accept" => "application/json" }.merge(headers)
|
|
39
|
+
@http = http_client || HttpClient.new
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns an array of all table names known to the controller.
|
|
43
|
+
#
|
|
44
|
+
# @return [Array<String>]
|
|
45
|
+
def list_tables
|
|
46
|
+
body = get_json(TABLES_PATH)
|
|
47
|
+
body["tables"] || []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the schema for a table as a Hash.
|
|
51
|
+
#
|
|
52
|
+
# @param table [String] table name (without _OFFLINE / _REALTIME suffix)
|
|
53
|
+
# @return [Hash] raw schema JSON
|
|
54
|
+
# @raise [TableNotFoundError] if the table or schema does not exist (404)
|
|
55
|
+
# @raise [TransportError] on other non-200 responses
|
|
56
|
+
def get_schema(table)
|
|
57
|
+
get_json(format(SCHEMA_PATH, table: table))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns the full table config (including segmentsConfig, tableIndexConfig,
|
|
61
|
+
# tenants, etc.) for a table as a Hash.
|
|
62
|
+
#
|
|
63
|
+
# @param table [String] table name (without _OFFLINE / _REALTIME suffix)
|
|
64
|
+
# @return [Hash] raw tableConfig JSON
|
|
65
|
+
# @raise [TableNotFoundError] if the table does not exist (404)
|
|
66
|
+
# @raise [TransportError] on other non-200 responses
|
|
67
|
+
def get_table_config(table)
|
|
68
|
+
get_json(format(TABLE_CONFIG_PATH, table: table))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Returns true when the controller knows about the given table.
|
|
72
|
+
#
|
|
73
|
+
# @param table [String]
|
|
74
|
+
# @return [Boolean]
|
|
75
|
+
def table_exists?(table)
|
|
76
|
+
get_table_config(table)
|
|
77
|
+
true
|
|
78
|
+
rescue TableNotFoundError
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Returns the column names and their data types for a table.
|
|
83
|
+
#
|
|
84
|
+
# Convenience wrapper around get_schema that returns a flat Hash:
|
|
85
|
+
# { "playerId" => "INT", "playerName" => "STRING", ... }
|
|
86
|
+
#
|
|
87
|
+
# @param table [String]
|
|
88
|
+
# @return [Hash{String => String}]
|
|
89
|
+
def column_types(table)
|
|
90
|
+
schema = get_schema(table)
|
|
91
|
+
dims = schema["dimensionFieldSpecs"] || []
|
|
92
|
+
metrics = schema["metricFieldSpecs"] || []
|
|
93
|
+
date_time = schema["dateTimeFieldSpecs"] || []
|
|
94
|
+
(dims + metrics + date_time).to_h do |spec|
|
|
95
|
+
[spec["name"], spec["dataType"]]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def normalize_address(address)
|
|
102
|
+
addr = address.to_s.chomp("/")
|
|
103
|
+
addr.start_with?("http://", "https://") ? addr : "http://#{addr}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def get_json(path)
|
|
107
|
+
url = "#{@base}#{path}"
|
|
108
|
+
resp = @http.get(url, headers: @headers)
|
|
109
|
+
|
|
110
|
+
case resp.code.to_i
|
|
111
|
+
when 200
|
|
112
|
+
JSON.parse(resp.body)
|
|
113
|
+
when 404
|
|
114
|
+
raise TableNotFoundError, "not found: #{path}"
|
|
115
|
+
else
|
|
116
|
+
raise TransportError, "controller returned HTTP #{resp.code} for #{path}"
|
|
117
|
+
end
|
|
118
|
+
rescue JSON::ParserError => e
|
|
119
|
+
raise TransportError, "invalid JSON from controller: #{e.message}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/pinot/version.rb
CHANGED
data/lib/pinot.rb
CHANGED
|
@@ -24,6 +24,7 @@ require_relative "pinot/paginator"
|
|
|
24
24
|
require_relative "pinot/connection"
|
|
25
25
|
require_relative "pinot/prepared_statement"
|
|
26
26
|
require_relative "pinot/connection_factory"
|
|
27
|
+
require_relative "pinot/schema_client"
|
|
27
28
|
|
|
28
29
|
require_relative "pinot/grpc_config"
|
|
29
30
|
begin
|
|
@@ -36,3 +37,5 @@ begin
|
|
|
36
37
|
require_relative "pinot/zookeeper_broker_selector"
|
|
37
38
|
rescue LoadError # rubocop:disable Lint/SuppressedException
|
|
38
39
|
end
|
|
40
|
+
|
|
41
|
+
require_relative "pinot/railtie" if defined?(Rails::Railtie)
|
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.
|
|
4
|
+
version: 1.30.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Xiang Fu
|
|
@@ -122,6 +122,20 @@ dependencies:
|
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
124
|
version: '3.0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: bundler-audit
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0.9'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0.9'
|
|
125
139
|
description: A Ruby client for Apache Pinot, mirroring the Go client API
|
|
126
140
|
email:
|
|
127
141
|
executables: []
|
|
@@ -151,8 +165,10 @@ files:
|
|
|
151
165
|
- lib/pinot/proto/broker_service_pb.rb
|
|
152
166
|
- lib/pinot/proto/broker_service_services_pb.rb
|
|
153
167
|
- lib/pinot/query_result.rb
|
|
168
|
+
- lib/pinot/railtie.rb
|
|
154
169
|
- lib/pinot/request.rb
|
|
155
170
|
- lib/pinot/response.rb
|
|
171
|
+
- lib/pinot/schema_client.rb
|
|
156
172
|
- lib/pinot/simple_broker_selector.rb
|
|
157
173
|
- lib/pinot/table_aware_broker_selector.rb
|
|
158
174
|
- lib/pinot/tls_config.rb
|