questdb 0.1.0-aarch64-linux-gnu
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 +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE.txt +201 -0
- data/README.md +170 -0
- data/SECURITY.md +19 -0
- data/THIRD_PARTY_NOTICES.md +242 -0
- data/docs/arrow.md +89 -0
- data/docs/configuration.md +61 -0
- data/docs/design.md +46 -0
- data/docs/ingestion.md +96 -0
- data/docs/performance.md +36 -0
- data/docs/python-parity.md +34 -0
- data/docs/querying.md +54 -0
- data/docs/release-infrastructure.md +70 -0
- data/docs/releasing.md +47 -0
- data/docs/troubleshooting.md +41 -0
- data/examples/basic.rb +32 -0
- data/examples/callbacks.rb +46 -0
- data/examples/columnar.rb +41 -0
- data/examples/independent_buffer.rb +39 -0
- data/examples/query.rb +28 -0
- data/examples/standalone.rb +33 -0
- data/examples/tls_auth.rb +33 -0
- data/lib/questdb/arrow.rb +250 -0
- data/lib/questdb/client.rb +308 -0
- data/lib/questdb/column_vector.rb +91 -0
- data/lib/questdb/config.rb +161 -0
- data/lib/questdb/connection_event.rb +60 -0
- data/lib/questdb/direct_ingestion.rb +440 -0
- data/lib/questdb/event_inbox.rb +60 -0
- data/lib/questdb/ingress/buffer.rb +104 -0
- data/lib/questdb/ingress/error.rb +43 -0
- data/lib/questdb/ingress/float64_array.rb +60 -0
- data/lib/questdb/ingress/native.rb +40 -0
- data/lib/questdb/ingress/timestamp.rb +53 -0
- data/lib/questdb/ingress.rb +77 -0
- data/lib/questdb/native/3.1/questdb_ingress.so +0 -0
- data/lib/questdb/native/3.2/questdb_ingress.so +0 -0
- data/lib/questdb/native/3.3/questdb_ingress.so +0 -0
- data/lib/questdb/native/3.4/questdb_ingress.so +0 -0
- data/lib/questdb/pooled_sender.rb +215 -0
- data/lib/questdb/query.rb +429 -0
- data/lib/questdb/sender.rb +436 -0
- data/lib/questdb/sender_error.rb +49 -0
- data/lib/questdb/types.rb +198 -0
- data/lib/questdb/version.rb +27 -0
- data/lib/questdb.rb +22 -0
- data/questdb.gemspec +69 -0
- data/sig/questdb-arrow.rbs +46 -0
- data/sig/questdb.rbs +337 -0
- metadata +188 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ___ _ ____ ____
|
|
4
|
+
# / _ \ _ _ ___ ___| |_| _ \| __ )
|
|
5
|
+
# | | | | | | |/ _ \/ __| __| | | | _ \
|
|
6
|
+
# | |_| | |_| | __/\__ \ |_| |_| | |_) |
|
|
7
|
+
# \__\_\\__,_|\___||___/\__|____/|____/
|
|
8
|
+
#
|
|
9
|
+
# Copyright (c) 2014-2019 Appsicle
|
|
10
|
+
# Copyright (c) 2019-2026 QuestDB
|
|
11
|
+
#
|
|
12
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
# you may not use this file except in compliance with the License.
|
|
14
|
+
# You may obtain a copy of the License at
|
|
15
|
+
#
|
|
16
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
#
|
|
18
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
# See the License for the specific language governing permissions and
|
|
22
|
+
# limitations under the License.
|
|
23
|
+
#
|
|
24
|
+
require "weakref"
|
|
25
|
+
|
|
26
|
+
module QuestDB
|
|
27
|
+
# A connection-level QWP/WebSocket sender. Prefer Client#sender for pooled
|
|
28
|
+
# production ingestion; standalone senders are useful for manual progress
|
|
29
|
+
# and explicit buffer control.
|
|
30
|
+
class Sender
|
|
31
|
+
PROTOCOLS = %i[ws wss].freeze
|
|
32
|
+
ACK_LEVELS = { ok: 0, durable: 1 }.freeze
|
|
33
|
+
EMPTY_HASH = {}.freeze
|
|
34
|
+
private_constant :EMPTY_HASH
|
|
35
|
+
|
|
36
|
+
attr_reader :protocol, :init_buf_size, :auto_flush_rows, :auto_flush_bytes, :auto_flush_interval
|
|
37
|
+
|
|
38
|
+
def initialize(protocol:, host:, port: nil, connection_listener: nil, error_handler: nil,
|
|
39
|
+
connection_event_inbox_capacity: 64, error_event_inbox_capacity: 64, **settings)
|
|
40
|
+
@protocol = protocol.to_sym
|
|
41
|
+
raise ArgumentError, "protocol must be :ws or :wss" unless PROTOCOLS.include?(@protocol)
|
|
42
|
+
host = Config.normalize_host(host)
|
|
43
|
+
port = Integer(port || 9000)
|
|
44
|
+
raise RangeError, "port must be between 1 and 65535" unless port.between?(1, 65_535)
|
|
45
|
+
validate_setting_keys!(settings)
|
|
46
|
+
effective = { "addr" => "#{host}:#{port}" }.merge(
|
|
47
|
+
settings.to_h { |key, value| [key.to_s, Config.setting_value(value, key)] }
|
|
48
|
+
)
|
|
49
|
+
native = effective.reject { |key, _| Config::STANDALONE_WRAPPER_KEYS.include?(key) }
|
|
50
|
+
@configuration = "#{@protocol}::#{native.map { |key, value| "#{key}=#{Config.encode(value, key.to_sym)};" }.join}".freeze
|
|
51
|
+
setup_wrapper(effective, connection_listener: connection_listener, error_handler: error_handler,
|
|
52
|
+
connection_event_inbox_capacity: connection_event_inbox_capacity,
|
|
53
|
+
error_event_inbox_capacity: error_event_inbox_capacity)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.from_conf(conf, connection_listener: nil, error_handler: nil,
|
|
57
|
+
connection_event_inbox_capacity: 64, error_event_inbox_capacity: 64, **overrides)
|
|
58
|
+
configuration, effective, protocol = merge_conf(conf, overrides)
|
|
59
|
+
object = allocate
|
|
60
|
+
object.send(:initialize_from_conf, configuration, effective, protocol,
|
|
61
|
+
connection_listener: connection_listener, error_handler: error_handler,
|
|
62
|
+
connection_event_inbox_capacity: connection_event_inbox_capacity,
|
|
63
|
+
error_event_inbox_capacity: error_event_inbox_capacity)
|
|
64
|
+
object
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.from_env(**overrides)
|
|
68
|
+
conf = ENV["QDB_CLIENT_CONF"]
|
|
69
|
+
raise Ingress::IngressError.new("QDB_CLIENT_CONF is not set", code: :config_error) unless conf && !conf.empty?
|
|
70
|
+
from_conf(conf, **overrides)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def establish
|
|
74
|
+
ensure_thread!
|
|
75
|
+
raise Ingress::IngressError.new("standalone sender is closed", code: :invalid_api_call) if @closed
|
|
76
|
+
return self if @native
|
|
77
|
+
@native = Ingress::Native::StandaloneSender.new(
|
|
78
|
+
@configuration, !@connection_listener.nil?, @connection_event_inbox_capacity,
|
|
79
|
+
!@error_handler.nil?, @error_event_inbox_capacity
|
|
80
|
+
)
|
|
81
|
+
@buffer = Ingress::Buffer.send(:from_native, @native.new_buffer(@init_buf_size))
|
|
82
|
+
@batch_started = nil
|
|
83
|
+
start_dispatcher(:connection, @connection_listener, @connection_inbox) if @connection_listener
|
|
84
|
+
start_dispatcher(:error, @error_handler, @error_inbox) if @error_handler
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def open
|
|
89
|
+
raise ArgumentError, "a block is required" unless block_given?
|
|
90
|
+
establish
|
|
91
|
+
begin
|
|
92
|
+
result = yield self
|
|
93
|
+
close
|
|
94
|
+
result
|
|
95
|
+
rescue Exception
|
|
96
|
+
close(flush: false, drain: false)
|
|
97
|
+
raise
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def new_buffer
|
|
102
|
+
ensure_open!
|
|
103
|
+
Ingress::Buffer.send(:from_native, @native.new_buffer(@init_buf_size))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def row(table, symbols: EMPTY_HASH, columns: EMPTY_HASH, at:)
|
|
107
|
+
ensure_open!
|
|
108
|
+
appended = @buffer.__send__(:append, table, symbols, columns, at)
|
|
109
|
+
return self unless appended
|
|
110
|
+
@batch_started ||= monotonic
|
|
111
|
+
flush if should_auto_flush?
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# One-off poolless direct columnar load using this sender's auth/TLS
|
|
116
|
+
# configuration. It uses a separate connection and has no ordering
|
|
117
|
+
# relationship with the buffered row stream.
|
|
118
|
+
def ingest_columns(table, columns:, symbols: EMPTY_HASH, at:, max_rows_per_batch: 16_384, ack: :ok)
|
|
119
|
+
prepared = DirectIngestion.prepare(
|
|
120
|
+
table, columns: columns, symbols: symbols, at: at,
|
|
121
|
+
max_rows_per_batch: max_rows_per_batch, ack: ack
|
|
122
|
+
)
|
|
123
|
+
ensure_ws!
|
|
124
|
+
return self if prepared[4].zero?
|
|
125
|
+
@native.ingest_columns(@configuration, *prepared)
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
alias write_columns ingest_columns
|
|
129
|
+
|
|
130
|
+
def row_count = (@buffer&.row_count || 0)
|
|
131
|
+
alias length row_count
|
|
132
|
+
def empty? = row_count.zero?
|
|
133
|
+
def closed? = @closed || (@native && @native.closed?)
|
|
134
|
+
def established? = !@native.nil? && !closed?
|
|
135
|
+
def auto_flush? = @auto_flush_enabled
|
|
136
|
+
alias auto_flush auto_flush?
|
|
137
|
+
|
|
138
|
+
def flush(buffer: nil, clear: true, wait: false, ack: :ok)
|
|
139
|
+
ensure_open!
|
|
140
|
+
target = buffer || @buffer
|
|
141
|
+
raise TypeError, "buffer must be a QuestDB QWP buffer" unless target.is_a?(Ingress::Buffer)
|
|
142
|
+
raise ArgumentError, "clear: false requires an explicit buffer" if target.equal?(@buffer) && !clear
|
|
143
|
+
level = normalize_ack(ack)
|
|
144
|
+
raise ArgumentError, "ack: :durable requires wait: true" if level == ACK_LEVELS[:durable] && !wait
|
|
145
|
+
return self if target.empty? && !wait
|
|
146
|
+
@native.flush(target.send(:native), !clear)
|
|
147
|
+
@native.wait(level, 0) if wait
|
|
148
|
+
@batch_started = nil if target.equal?(@buffer) && target.empty?
|
|
149
|
+
self
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def wait(timeout_millis: 0, ack: :ok)
|
|
153
|
+
ensure_ws!
|
|
154
|
+
@native.wait(normalize_ack(ack), non_negative_integer(timeout_millis, "timeout_millis"))
|
|
155
|
+
self
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def flush_and_get_fsn
|
|
159
|
+
ensure_ws!
|
|
160
|
+
value = @native.flush_fsn(@buffer.send(:native), false)
|
|
161
|
+
@batch_started = nil if @buffer.empty?
|
|
162
|
+
value
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def flush_and_keep_and_get_fsn(buffer = nil)
|
|
166
|
+
ensure_ws!
|
|
167
|
+
target = buffer || @buffer
|
|
168
|
+
@native.flush_fsn(target.send(:native), true)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def published_fsn = (ensure_ws!; @native.published_fsn)
|
|
172
|
+
def acked_fsn = (ensure_ws!; @native.acked_fsn)
|
|
173
|
+
|
|
174
|
+
def await_acked_fsn(fsn, timeout_millis: 0)
|
|
175
|
+
ensure_ws!
|
|
176
|
+
target = non_negative_integer(fsn, "fsn")
|
|
177
|
+
current = @native.acked_fsn
|
|
178
|
+
return true if current && current >= target
|
|
179
|
+
begin
|
|
180
|
+
@native.wait(ACK_LEVELS[:ok], non_negative_integer(timeout_millis, "timeout_millis"))
|
|
181
|
+
rescue Ingress::IngressError => error
|
|
182
|
+
raise unless error.code == :failover_retry
|
|
183
|
+
end
|
|
184
|
+
current = @native.acked_fsn
|
|
185
|
+
!!(current && current >= target)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def drive_once = (ensure_ws!; @native.drive_once)
|
|
189
|
+
|
|
190
|
+
def close_drain
|
|
191
|
+
ensure_ws!
|
|
192
|
+
@native.close_drain
|
|
193
|
+
self
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def poll_error
|
|
197
|
+
ensure_ws!
|
|
198
|
+
values = @native.poll_error
|
|
199
|
+
values && SenderError.from_native(values)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def poll_connection_event = @connection_inbox&.poll
|
|
203
|
+
def poll_callback_error = @error_inbox&.poll
|
|
204
|
+
|
|
205
|
+
def error_events_delivered
|
|
206
|
+
ensure_ws!
|
|
207
|
+
@native.error_events_delivered + (@error_inbox&.delivered || 0)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def error_events_dropped
|
|
211
|
+
ensure_ws!
|
|
212
|
+
@native.error_events_dropped + (@error_inbox&.dropped || 0)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def connection_events_delivered
|
|
216
|
+
ensure_open!
|
|
217
|
+
@native.connection_events_delivered + (@connection_inbox&.delivered || 0)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def connection_events_dropped
|
|
221
|
+
ensure_open!
|
|
222
|
+
@native.connection_events_dropped + (@connection_inbox&.dropped || 0)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def close(flush: true, wait: false, drain: nil)
|
|
226
|
+
return self if @closed
|
|
227
|
+
ensure_thread!
|
|
228
|
+
drain = flush if drain.nil?
|
|
229
|
+
begin
|
|
230
|
+
if @native && flush && @buffer && !@buffer.empty?
|
|
231
|
+
self.flush(wait: wait)
|
|
232
|
+
elsif @native && wait
|
|
233
|
+
self.wait
|
|
234
|
+
end
|
|
235
|
+
ensure
|
|
236
|
+
primary = $!
|
|
237
|
+
@buffer&.clear
|
|
238
|
+
begin
|
|
239
|
+
@native&.close(!!drain)
|
|
240
|
+
rescue Exception
|
|
241
|
+
raise unless primary
|
|
242
|
+
ensure
|
|
243
|
+
@closed = true
|
|
244
|
+
threads = @dispatcher_threads
|
|
245
|
+
@dispatcher_threads = []
|
|
246
|
+
current = Thread.current
|
|
247
|
+
threads.each { |thread| thread.join unless thread.equal?(current) }
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
self
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def initialize_copy(*) = raise(TypeError, "QuestDB standalone senders cannot be copied")
|
|
254
|
+
|
|
255
|
+
class << self
|
|
256
|
+
private
|
|
257
|
+
|
|
258
|
+
def dispatcher_proc(owner, native, kind, handler, inbox)
|
|
259
|
+
method = kind == :connection ? :next_connection_event : :next_error_event
|
|
260
|
+
converter = kind == :connection ? ConnectionEvent : SenderError
|
|
261
|
+
proc do
|
|
262
|
+
Thread.current.name = "questdb-standalone-#{kind}-events" if Thread.current.respond_to?(:name=)
|
|
263
|
+
loop do
|
|
264
|
+
break unless owner.weakref_alive?
|
|
265
|
+
values = native.public_send(method, false)
|
|
266
|
+
unless values
|
|
267
|
+
break if native.closed?
|
|
268
|
+
sleep(0.01)
|
|
269
|
+
next
|
|
270
|
+
end
|
|
271
|
+
event = converter.from_native(values)
|
|
272
|
+
inbox.push(event)
|
|
273
|
+
begin
|
|
274
|
+
handler.call(event)
|
|
275
|
+
rescue Exception => error
|
|
276
|
+
Kernel.warn("QuestDB standalone #{kind} callback failed: #{error.class}: #{error.message}")
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def merge_conf(conf, overrides)
|
|
283
|
+
conf = String(conf)
|
|
284
|
+
schema, separator, rest = conf.partition("::")
|
|
285
|
+
protocol = schema.to_sym
|
|
286
|
+
unless separator == "::" && PROTOCOLS.include?(protocol)
|
|
287
|
+
raise Ingress::IngressError.new("configuration must start with ws:: or wss::", code: :config_error)
|
|
288
|
+
end
|
|
289
|
+
overrides = overrides.transform_keys(&:to_sym)
|
|
290
|
+
overrides.each_key do |key|
|
|
291
|
+
raise ArgumentError, "invalid setting #{key.inspect}" unless key.to_s.match?(/\A[a-z_][a-z0-9_]*\z/)
|
|
292
|
+
end
|
|
293
|
+
parts = Config.split_parts(rest)
|
|
294
|
+
keys = parts.filter_map do |part|
|
|
295
|
+
key, value = part.split("=", 2)
|
|
296
|
+
key if value && key.match?(/\A[a-z_][a-z0-9_]*\z/)
|
|
297
|
+
end
|
|
298
|
+
duplicates = keys.tally.select { |_key, count| count > 1 }.keys
|
|
299
|
+
unless duplicates.empty?
|
|
300
|
+
raise Ingress::IngressError.new("duplicate configuration keys: #{duplicates.join(', ')}", code: :config_error)
|
|
301
|
+
end
|
|
302
|
+
values = Config.parse(conf)
|
|
303
|
+
conflicts = overrides.keys.select { |key| values.key?(key.to_s) }
|
|
304
|
+
raise ArgumentError, "settings already present in configuration: #{conflicts.join(', ')}" unless conflicts.empty?
|
|
305
|
+
effective = values.merge(overrides.to_h { |key, value| [key.to_s, Config.setting_value(value, key)] })
|
|
306
|
+
native_parts = parts.reject { |part| Config::STANDALONE_WRAPPER_KEYS.include?(part.split("=", 2).first) }
|
|
307
|
+
keyword_parts = overrides.reject { |key, _| Config::STANDALONE_WRAPPER_KEYS.include?(key.to_s) }
|
|
308
|
+
.map { |key, value| "#{key}=#{Config.encode(value, key)};" }.join
|
|
309
|
+
rebuilt = native_parts.map do |part|
|
|
310
|
+
key, value = part.split("=", 2)
|
|
311
|
+
value ? "#{key}=#{Config.encode(value)};" : "#{Config.encode(part)};"
|
|
312
|
+
end.join
|
|
313
|
+
["#{schema}::#{keyword_parts}#{rebuilt}".freeze, effective, protocol]
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
private
|
|
318
|
+
|
|
319
|
+
def initialize_from_conf(configuration, effective, protocol, connection_listener:, error_handler:,
|
|
320
|
+
connection_event_inbox_capacity:, error_event_inbox_capacity:)
|
|
321
|
+
@configuration = configuration
|
|
322
|
+
@protocol = protocol
|
|
323
|
+
setup_wrapper(effective, connection_listener: connection_listener, error_handler: error_handler,
|
|
324
|
+
connection_event_inbox_capacity: connection_event_inbox_capacity,
|
|
325
|
+
error_event_inbox_capacity: error_event_inbox_capacity)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def setup_wrapper(settings, connection_listener:, error_handler:,
|
|
329
|
+
connection_event_inbox_capacity:, error_event_inbox_capacity:)
|
|
330
|
+
@owner_thread = Thread.current
|
|
331
|
+
validate_handler!(connection_listener, "connection_listener")
|
|
332
|
+
validate_handler!(error_handler, "error_handler")
|
|
333
|
+
@connection_event_inbox_capacity = validate_capacity!(connection_event_inbox_capacity, "connection_event_inbox_capacity")
|
|
334
|
+
@error_event_inbox_capacity = validate_capacity!(error_event_inbox_capacity, "error_event_inbox_capacity")
|
|
335
|
+
@connection_listener = connection_listener
|
|
336
|
+
@error_handler = error_handler
|
|
337
|
+
@connection_inbox = connection_listener && EventInbox.new(@connection_event_inbox_capacity)
|
|
338
|
+
@error_inbox = error_handler && EventInbox.new(@error_event_inbox_capacity)
|
|
339
|
+
@dispatcher_threads = []
|
|
340
|
+
@native = @buffer = nil
|
|
341
|
+
@closed = false
|
|
342
|
+
@init_buf_size = Integer(settings.fetch("init_buf_size", 65_536))
|
|
343
|
+
raise ArgumentError, "init_buf_size must be nonnegative" if @init_buf_size.negative?
|
|
344
|
+
raise RangeError, "init_buf_size exceeds 100 MiB" if @init_buf_size > Ingress::Buffer::MAX_PREALLOCATE_BYTES
|
|
345
|
+
@auto_flush_rows = threshold(settings.fetch("auto_flush_rows", 600), "auto_flush_rows")
|
|
346
|
+
@auto_flush_bytes = settings.key?("auto_flush_bytes") ? threshold(settings["auto_flush_bytes"], "auto_flush_bytes") : nil
|
|
347
|
+
@auto_flush_interval = threshold(settings.fetch("auto_flush_interval", 1_000), "auto_flush_interval")
|
|
348
|
+
enabled = if settings.key?("auto_flush")
|
|
349
|
+
switch(settings["auto_flush"], "auto_flush")
|
|
350
|
+
else
|
|
351
|
+
[@auto_flush_rows, @auto_flush_bytes, @auto_flush_interval].any?
|
|
352
|
+
end
|
|
353
|
+
unless enabled
|
|
354
|
+
@auto_flush_rows = @auto_flush_bytes = @auto_flush_interval = nil
|
|
355
|
+
end
|
|
356
|
+
if enabled && [@auto_flush_rows, @auto_flush_bytes, @auto_flush_interval].none?
|
|
357
|
+
raise ArgumentError, "auto_flush cannot be enabled when every threshold is off"
|
|
358
|
+
end
|
|
359
|
+
@auto_flush_enabled = enabled
|
|
360
|
+
@batch_started = nil
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def start_dispatcher(kind, handler, inbox)
|
|
364
|
+
thread = Thread.new(&self.class.send(:dispatcher_proc, WeakRef.new(self), @native, kind, handler, inbox))
|
|
365
|
+
thread.report_on_exception = false if thread.respond_to?(:report_on_exception=)
|
|
366
|
+
@dispatcher_threads << thread
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def validate_handler!(handler, name)
|
|
370
|
+
return if handler.nil? || handler.respond_to?(:call)
|
|
371
|
+
raise TypeError, "#{name} must respond to #call"
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def validate_capacity!(value, name)
|
|
375
|
+
capacity = Integer(value)
|
|
376
|
+
raise ArgumentError, "#{name} must be nonnegative" if capacity.negative?
|
|
377
|
+
capacity
|
|
378
|
+
rescue ArgumentError, TypeError
|
|
379
|
+
raise ArgumentError, "#{name} must be a nonnegative integer"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def ensure_thread!
|
|
383
|
+
return if Thread.current.equal?(@owner_thread)
|
|
384
|
+
raise Ingress::IngressError.new("standalone sender must be used on its creating thread", code: :invalid_api_call)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def ensure_open!
|
|
388
|
+
ensure_thread!
|
|
389
|
+
raise Ingress::IngressError.new("standalone sender is not established", code: :invalid_api_call) unless established?
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def ensure_ws!
|
|
393
|
+
ensure_open!
|
|
394
|
+
true
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def normalize_ack(value)
|
|
398
|
+
value = value.to_sym if value.respond_to?(:to_sym)
|
|
399
|
+
ACK_LEVELS.fetch(value) { raise ArgumentError, "ack must be :ok or :durable" }
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def threshold(value, name)
|
|
403
|
+
return nil if %w[off false].include?(value.to_s)
|
|
404
|
+
number = Integer(value)
|
|
405
|
+
raise ArgumentError, "#{name} must be positive or off" unless number.positive?
|
|
406
|
+
number
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def switch(value, name)
|
|
410
|
+
return true if %w[on true].include?(value.to_s)
|
|
411
|
+
return false if %w[off false].include?(value.to_s)
|
|
412
|
+
raise ArgumentError, "#{name} must be on or off"
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def validate_setting_keys!(settings)
|
|
416
|
+
settings.each_key do |key|
|
|
417
|
+
raise ArgumentError, "invalid setting #{key.inspect}" unless key.to_s.match?(/\A[a-z_][a-z0-9_]*\z/)
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def non_negative_integer(value, name)
|
|
422
|
+
value = Integer(value)
|
|
423
|
+
raise ArgumentError, "#{name} must be nonnegative" if value.negative?
|
|
424
|
+
value
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def should_auto_flush?
|
|
428
|
+
return false unless @batch_started
|
|
429
|
+
return true if @auto_flush_rows && row_count >= @auto_flush_rows
|
|
430
|
+
return true if @auto_flush_bytes && @buffer.bytesize >= @auto_flush_bytes
|
|
431
|
+
@auto_flush_interval && monotonic - @batch_started >= @auto_flush_interval
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ___ _ ____ ____
|
|
4
|
+
# / _ \ _ _ ___ ___| |_| _ \| __ )
|
|
5
|
+
# | | | | | | |/ _ \/ __| __| | | | _ \
|
|
6
|
+
# | |_| | |_| | __/\__ \ |_| |_| | |_) |
|
|
7
|
+
# \__\_\\__,_|\___||___/\__|____/|____/
|
|
8
|
+
#
|
|
9
|
+
# Copyright (c) 2014-2019 Appsicle
|
|
10
|
+
# Copyright (c) 2019-2026 QuestDB
|
|
11
|
+
#
|
|
12
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
# you may not use this file except in compliance with the License.
|
|
14
|
+
# You may obtain a copy of the License at
|
|
15
|
+
#
|
|
16
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
#
|
|
18
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
# See the License for the specific language governing permissions and
|
|
22
|
+
# limitations under the License.
|
|
23
|
+
#
|
|
24
|
+
|
|
25
|
+
module QuestDB
|
|
26
|
+
class SenderError
|
|
27
|
+
ATTRIBUTES = %i[category applied_policy status message message_sequence from_fsn to_fsn].freeze
|
|
28
|
+
attr_reader(*ATTRIBUTES)
|
|
29
|
+
|
|
30
|
+
def initialize(category:, applied_policy:, status:, message:, message_sequence:, from_fsn:, to_fsn:)
|
|
31
|
+
@category = category.to_sym
|
|
32
|
+
@applied_policy = applied_policy.to_sym
|
|
33
|
+
@status = status
|
|
34
|
+
@message = String(message).dup.freeze
|
|
35
|
+
@message_sequence = message_sequence
|
|
36
|
+
@from_fsn = Integer(from_fsn)
|
|
37
|
+
@to_fsn = Integer(to_fsn)
|
|
38
|
+
freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def terminal? = applied_policy == :terminal
|
|
42
|
+
def retriable? = %i[retriable retriable_other].include?(applied_policy)
|
|
43
|
+
|
|
44
|
+
def self.from_native(values)
|
|
45
|
+
new(category: values[0], applied_policy: values[1], status: values[2],
|
|
46
|
+
message: values[3], message_sequence: values[4], from_fsn: values[5], to_fsn: values[6])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ___ _ ____ ____
|
|
4
|
+
# / _ \ _ _ ___ ___| |_| _ \| __ )
|
|
5
|
+
# | | | | | | |/ _ \/ __| __| | | | _ \
|
|
6
|
+
# | |_| | |_| | __/\__ \ |_| |_| | |_) |
|
|
7
|
+
# \__\_\\__,_|\___||___/\__|____/|____/
|
|
8
|
+
#
|
|
9
|
+
# Copyright (c) 2014-2019 Appsicle
|
|
10
|
+
# Copyright (c) 2019-2026 QuestDB
|
|
11
|
+
#
|
|
12
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
# you may not use this file except in compliance with the License.
|
|
14
|
+
# You may obtain a copy of the License at
|
|
15
|
+
#
|
|
16
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
#
|
|
18
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
# See the License for the specific language governing permissions and
|
|
22
|
+
# limitations under the License.
|
|
23
|
+
#
|
|
24
|
+
require "ipaddr"
|
|
25
|
+
|
|
26
|
+
module QuestDB
|
|
27
|
+
class DateMillis
|
|
28
|
+
attr_reader :value
|
|
29
|
+
def initialize(value)
|
|
30
|
+
@value = Integer(value)
|
|
31
|
+
raise RangeError, "date milliseconds are outside signed 64-bit range" unless @value.between?(-(1 << 63), (1 << 63) - 1)
|
|
32
|
+
freeze
|
|
33
|
+
end
|
|
34
|
+
def to_time = Time.at(Rational(value, 1_000)).utc
|
|
35
|
+
def ==(other) = other.is_a?(DateMillis) && value == other.value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Char
|
|
39
|
+
attr_reader :value
|
|
40
|
+
def initialize(value)
|
|
41
|
+
value = value.ord if value.is_a?(String) && value.length == 1
|
|
42
|
+
@value = Integer(value)
|
|
43
|
+
raise RangeError, "CHAR must be one UTF-16 code unit" unless @value.between?(0, 0xffff)
|
|
44
|
+
freeze
|
|
45
|
+
end
|
|
46
|
+
def to_s
|
|
47
|
+
return "\uFFFD" if value.between?(0xd800, 0xdfff)
|
|
48
|
+
[value].pack("U")
|
|
49
|
+
end
|
|
50
|
+
def ==(other) = other.is_a?(Char) && value == other.value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class UUID
|
|
54
|
+
PATTERN = /\A([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{12})\z/
|
|
55
|
+
attr_reader :bytes
|
|
56
|
+
def initialize(value)
|
|
57
|
+
if value.is_a?(UUID)
|
|
58
|
+
@bytes = value.bytes
|
|
59
|
+
else
|
|
60
|
+
match = PATTERN.match(String(value)) or raise ArgumentError, "invalid UUID"
|
|
61
|
+
@bytes = match.captures.join.then { |hex| [hex].pack("H*") }.freeze
|
|
62
|
+
end
|
|
63
|
+
freeze
|
|
64
|
+
end
|
|
65
|
+
def self.from_bytes(value)
|
|
66
|
+
bytes = String(value).b
|
|
67
|
+
raise ArgumentError, "UUID requires exactly 16 bytes" unless bytes.bytesize == 16
|
|
68
|
+
allocate.tap { |uuid| uuid.instance_variable_set(:@bytes, bytes.dup.freeze); uuid.freeze }
|
|
69
|
+
end
|
|
70
|
+
def to_s
|
|
71
|
+
hex = bytes.unpack1("H*")
|
|
72
|
+
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
73
|
+
end
|
|
74
|
+
alias inspect to_s
|
|
75
|
+
def ==(other) = other.is_a?(UUID) && bytes == other.bytes
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class Binary
|
|
79
|
+
attr_reader :bytes
|
|
80
|
+
def initialize(bytes)
|
|
81
|
+
@bytes = String(bytes).b.dup.freeze
|
|
82
|
+
freeze
|
|
83
|
+
end
|
|
84
|
+
def ==(other) = other.is_a?(Binary) && bytes == other.bytes
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class IPv4
|
|
88
|
+
attr_reader :value
|
|
89
|
+
def initialize(value)
|
|
90
|
+
@value = case value
|
|
91
|
+
when IPv4 then value.value
|
|
92
|
+
when IPAddr
|
|
93
|
+
raise ArgumentError, "IPv4 value must not be IPv6" unless value.ipv4?
|
|
94
|
+
value.to_i
|
|
95
|
+
when String
|
|
96
|
+
address = IPAddr.new(value)
|
|
97
|
+
raise ArgumentError, "IPv4 value must not be IPv6" unless address.ipv4?
|
|
98
|
+
address.to_i
|
|
99
|
+
else Integer(value)
|
|
100
|
+
end
|
|
101
|
+
raise RangeError, "IPv4 value must fit unsigned 32-bit" unless @value.between?(0, 0xffff_ffff)
|
|
102
|
+
freeze
|
|
103
|
+
end
|
|
104
|
+
def to_s = [24, 16, 8, 0].map { |shift| (value >> shift) & 0xff }.join(".")
|
|
105
|
+
def to_ipaddr = IPAddr.new(value, Socket::AF_INET)
|
|
106
|
+
def ==(other) = other.is_a?(IPv4) && value == other.value
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class Geohash
|
|
110
|
+
attr_reader :value, :precision_bits
|
|
111
|
+
def self.from_native(value, precision_bits) = new(value, precision_bits: precision_bits)
|
|
112
|
+
def initialize(value, precision_bits:)
|
|
113
|
+
@value = Integer(value)
|
|
114
|
+
@precision_bits = Integer(precision_bits)
|
|
115
|
+
raise RangeError, "geohash precision must be between 1 and 60 bits" unless @precision_bits.between?(1, 60)
|
|
116
|
+
raise RangeError, "geohash value exceeds its precision" unless @value.between?(0, (1 << @precision_bits) - 1)
|
|
117
|
+
freeze
|
|
118
|
+
end
|
|
119
|
+
def ==(other) = other.is_a?(Geohash) && value == other.value && precision_bits == other.precision_bits
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class Long256
|
|
123
|
+
LIMIT = 1 << 256
|
|
124
|
+
attr_reader :value
|
|
125
|
+
def initialize(value)
|
|
126
|
+
@value = Integer(value)
|
|
127
|
+
raise RangeError, "LONG256 must be between 0 and 2^256-1" unless @value.between?(0, LIMIT - 1)
|
|
128
|
+
freeze
|
|
129
|
+
end
|
|
130
|
+
def bytes = [value.to_s(16).rjust(64, "0")].pack("H*").reverse
|
|
131
|
+
def self.from_bytes(bytes)
|
|
132
|
+
bytes = String(bytes).b
|
|
133
|
+
raise ArgumentError, "LONG256 requires 32 bytes" unless bytes.bytesize == 32
|
|
134
|
+
new(bytes.reverse.unpack1("H*").to_i(16))
|
|
135
|
+
end
|
|
136
|
+
def ==(other) = other.is_a?(Long256) && value == other.value
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
class DecimalValue
|
|
140
|
+
attr_reader :mantissa, :scale
|
|
141
|
+
def self.from_native(mantissa, scale) = new(mantissa, scale: scale)
|
|
142
|
+
def initialize(mantissa, scale:)
|
|
143
|
+
@mantissa = Integer(mantissa)
|
|
144
|
+
@scale = Integer(scale)
|
|
145
|
+
raise RangeError, "decimal scale must fit signed 8-bit" unless @scale.between?(-128, 127)
|
|
146
|
+
freeze
|
|
147
|
+
end
|
|
148
|
+
def self.from_bigdecimal(value)
|
|
149
|
+
sign, digits, _base, exponent = value.split
|
|
150
|
+
mantissa = (sign < 0 ? -1 : 1) * digits.to_i
|
|
151
|
+
scale = digits.length - exponent
|
|
152
|
+
while scale.positive? && (mantissa % 10).zero?
|
|
153
|
+
mantissa /= 10
|
|
154
|
+
scale -= 1
|
|
155
|
+
end
|
|
156
|
+
new(mantissa, scale: scale)
|
|
157
|
+
end
|
|
158
|
+
def self.from_twos_complement(bytes, scale)
|
|
159
|
+
bytes = String(bytes).b
|
|
160
|
+
bits = bytes.bytesize * 8
|
|
161
|
+
unsigned = bytes.reverse.unpack1("H*").to_i(16)
|
|
162
|
+
unsigned -= 1 << bits if (unsigned & (1 << (bits - 1))) != 0
|
|
163
|
+
new(unsigned, scale: scale)
|
|
164
|
+
end
|
|
165
|
+
def to_d = BigDecimal(mantissa) * (BigDecimal(10) ** -scale)
|
|
166
|
+
def to_s = to_d.to_s("F")
|
|
167
|
+
def ==(other) = other.is_a?(DecimalValue) && mantissa == other.mantissa && scale == other.scale
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
class TypedNull
|
|
171
|
+
KINDS = %i[boolean byte short int long float double symbol timestamp date uuid long256 geohash varchar timestamp_nanos decimal64 decimal128 decimal256 char binary ipv4].freeze
|
|
172
|
+
attr_reader :kind, :scale, :precision_bits
|
|
173
|
+
def initialize(kind, scale: nil, precision_bits: nil)
|
|
174
|
+
@kind = kind.to_sym
|
|
175
|
+
raise ArgumentError, "unsupported typed NULL kind #{@kind.inspect}" unless KINDS.include?(@kind)
|
|
176
|
+
@scale = scale.nil? ? nil : Integer(scale)
|
|
177
|
+
@precision_bits = precision_bits.nil? ? nil : Integer(precision_bits)
|
|
178
|
+
if %i[decimal64 decimal128 decimal256].include?(@kind)
|
|
179
|
+
@scale ||= 0
|
|
180
|
+
raise RangeError, "decimal scale must fit signed 8-bit" unless @scale.between?(-128, 127)
|
|
181
|
+
end
|
|
182
|
+
if @kind == :geohash
|
|
183
|
+
raise ArgumentError, "geohash NULL requires precision_bits:" unless @precision_bits
|
|
184
|
+
raise RangeError, "geohash precision must be between 1 and 60 bits" unless @precision_bits.between?(1, 60)
|
|
185
|
+
end
|
|
186
|
+
freeze
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
class OpaqueValue
|
|
191
|
+
attr_reader :kind, :bytes
|
|
192
|
+
def initialize(kind, bytes = nil)
|
|
193
|
+
@kind = kind
|
|
194
|
+
@bytes = bytes&.b&.dup&.freeze
|
|
195
|
+
freeze
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|