questdb 0.1.0 → 0.1.1
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/.cargo/audit.toml +17 -9
- data/CHANGELOG.md +7 -0
- data/README.md +1 -1
- data/THIRD_PARTY_NOTICES.md +2 -2
- data/docs/configuration.md +3 -2
- data/docs/design.md +1 -1
- data/docs/python-parity.md +1 -1
- data/docs/releasing.md +1 -1
- data/ext/questdb_ingress/extconf.rb +2 -0
- data/ext/questdb_ingress/questdb_ingress.c +2 -1
- data/ext/questdb_ingress/unlink_build_ruby.rb +58 -0
- data/lib/questdb/arrow.rb +2 -0
- data/lib/questdb/client.rb +6 -0
- data/lib/questdb/config.rb +19 -0
- data/lib/questdb/pooled_sender.rb +4 -1
- data/lib/questdb/sender.rb +10 -1
- data/lib/questdb/version.rb +2 -2
- data/script/check-native-linkage +115 -0
- data/test/client_test.rb +30 -0
- data/test/extconf_link_flags_test.rb +160 -0
- data/vendor/c-questdb-client/.bumpversion.toml +1 -1
- data/vendor/c-questdb-client/CMakeLists.txt +1 -1
- data/vendor/c-questdb-client/cpp_test/qwp_mock_server.hpp +10 -1
- data/vendor/c-questdb-client/cpp_test/test_arrow_c.c +39 -0
- data/vendor/c-questdb-client/cpp_test/test_reader_mock.cpp +428 -0
- data/vendor/c-questdb-client/doc/COMPATIBILITY.md +1 -1
- data/vendor/c-questdb-client/doc/CONSIDERATIONS.md +6 -3
- data/vendor/c-questdb-client/doc/RELEASE_RUNBOOK.md +7 -7
- data/vendor/c-questdb-client/doc/RELEASING.md +6 -6
- data/vendor/c-questdb-client/include/questdb/egress/qwp_reader.h +48 -1
- data/vendor/c-questdb-client/include/questdb/egress/qwp_reader.hpp +52 -0
- data/vendor/c-questdb-client/include/questdb/ingress/line_sender.h +46 -4
- data/vendor/c-questdb-client/include/questdb/ingress/line_sender.hpp +7 -4
- data/vendor/c-questdb-client/include/questdb/ingress/line_sender_core.hpp +1 -0
- data/vendor/c-questdb-client/questdb-rs/Cargo.toml +1 -1
- data/vendor/c-questdb-client/questdb-rs/README.md +13 -13
- data/vendor/c-questdb-client/questdb-rs/src/egress/config.rs +78 -0
- data/vendor/c-questdb-client/questdb-rs/src/egress/mod.rs +2 -2
- data/vendor/c-questdb-client/questdb-rs/src/egress/query_request.rs +174 -1
- data/vendor/c-questdb-client/questdb-rs/src/egress/reader.rs +264 -14
- data/vendor/c-questdb-client/questdb-rs/src/egress/wire/capabilities.rs +69 -0
- data/vendor/c-questdb-client/questdb-rs/src/egress/wire/msg_kind.rs +33 -0
- data/vendor/c-questdb-client/questdb-rs/src/error.rs +33 -5
- data/vendor/c-questdb-client/questdb-rs/src/ingress/buffer/qwp.rs +96 -11
- data/vendor/c-questdb-client/questdb-rs/src/ingress/buffer.rs +6 -3
- data/vendor/c-questdb-client/questdb-rs/src/ingress/column_sender/sender.rs +1 -16
- data/vendor/c-questdb-client/questdb-rs/src/ingress/sender/qwp_ws.rs +19 -13
- data/vendor/c-questdb-client/questdb-rs/src/ingress/sender/qwp_ws_driver.rs +2 -9
- data/vendor/c-questdb-client/questdb-rs/src/ingress/sender/qwp_ws_publisher.rs +3 -3
- data/vendor/c-questdb-client/questdb-rs/src/ingress/sender.rs +101 -17
- data/vendor/c-questdb-client/questdb-rs/src/ingress/tests.rs +32 -0
- data/vendor/c-questdb-client/questdb-rs/src/ingress.rs +3 -2
- data/vendor/c-questdb-client/questdb-rs/src/tests/qwp_ws.rs +96 -16
- data/vendor/c-questdb-client/questdb-rs/tests/egress_failover.rs +219 -1
- data/vendor/c-questdb-client/questdb-rs/tests/egress_live_server.rs +34 -0
- data/vendor/c-questdb-client/questdb-rs-ffi/Cargo.lock +2 -2
- data/vendor/c-questdb-client/questdb-rs-ffi/Cargo.toml +1 -1
- data/vendor/c-questdb-client/questdb-rs-ffi/src/egress.rs +31 -0
- data/vendor/c-questdb-client/questdb-rs-ffi/src/lib.rs +70 -3
- data/vendor/c-questdb-client/system_test/arrow_ffi.py +1 -0
- data/vendor/c-questdb-client/system_test/enterprise_e2e/tests/test_failover.py +12 -6
- data/vendor/c-questdb-client/system_test/enterprise_e2e/tests/test_orphan_drainer_bindings.py +12 -4
- data/vendor/c-questdb-client/system_test/failover_clients/Cargo.lock +1 -1
- data/vendor/c-questdb-client/system_test/soak/workload_rs/Cargo.lock +1 -1
- data/vendor/c-questdb-client.version +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ee4c4fb9e29b76cf567494acba3f14416fd364efef44e9f2d2c7d19ea0c25fb
|
|
4
|
+
data.tar.gz: 5abe1a191ded582c1cd6f9cb5353eaaa6cca4b4d73b985b78c9b3900c2352ed4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d824b5d90da424043ffd261e185e827495afac15e76dde355f268fe1dfa80b1de6d9a1282f3ab514e8b2b5b65d793ce5871b2c9bbda8b40838de97df8aeef2c
|
|
7
|
+
data.tar.gz: 4648bc7748d704d652706b94744d4a739714351641c20b08a2228309246dca38a194fff11f10dc93d4fcc3cc919520015195647b7776fb86e1c128e13492f69b
|
data/.cargo/audit.toml
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
# Temporary exceptions for the 0.1.
|
|
2
|
-
# 7.1.0
|
|
3
|
-
#
|
|
4
|
-
#
|
|
1
|
+
# Temporary exceptions for the 0.1.x releases, which pin c-questdb-client
|
|
2
|
+
# 7.1.0 and 7.2.0. Both lockfiles have rustls 0.23.41, rustls-webpki
|
|
3
|
+
# 0.103.13 and webpki-roots 1.0.8; the fixes need rustls 0.23.46,
|
|
4
|
+
# rustls-webpki 0.103.16 and webpki-roots 1.0.10 or newer. Remove each entry
|
|
5
|
+
# when a c-questdb-client patch release ships it. See docs/releasing.md.
|
|
5
6
|
[advisories]
|
|
6
7
|
ignore = [
|
|
7
|
-
# TLS 1.3 handshake messages accepted across encryption level
|
|
8
|
+
# rustls: TLS 1.3 handshake messages accepted across encryption level
|
|
9
|
+
# boundaries.
|
|
8
10
|
"RUSTSEC-2026-0285",
|
|
9
|
-
# `Debug` impls for client session values expose TLS secrets.
|
|
11
|
+
# rustls: `Debug` impls for client session values expose TLS secrets.
|
|
10
12
|
"RUSTSEC-2026-0346",
|
|
11
|
-
# Brotli certificate decompression can allocate up to 1 GiB; the
|
|
12
|
-
# feature is not enabled in this build.
|
|
13
|
+
# rustls: Brotli certificate decompression can allocate up to 1 GiB; the
|
|
14
|
+
# brotli feature is not enabled in this build.
|
|
13
15
|
"RUSTSEC-2026-0348",
|
|
14
|
-
#
|
|
16
|
+
# rustls: unauthenticated ServerHello can downgrade later connections to
|
|
15
17
|
# classical key exchange.
|
|
16
18
|
"RUSTSEC-2026-0349",
|
|
19
|
+
# webpki-roots: bundled roots allow trust in distrusted emSign
|
|
20
|
+
# intermediates.
|
|
21
|
+
"RUSTSEC-2026-0381",
|
|
22
|
+
# rustls-webpki: certificate verification may accept distrusted
|
|
23
|
+
# intermediates.
|
|
24
|
+
"RUSTSEC-2026-0382",
|
|
17
25
|
]
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1 (2026-10-21)
|
|
4
|
+
|
|
5
|
+
- Fixed precompiled gems that installed but failed on `require "questdb"`. The 0.1.0 native extensions were linked against the Ruby on the build machine: macOS gems referenced that machine's `libruby` path (or, for Ruby 3.1 on ARM64, symbols in its `ruby` executable), and Linux gems required `libruby.so`, so they failed with Rubies built without a shared library. Extensions now resolve Ruby's C API from the Ruby that loads them. The release pipeline checks every extension's linkage, because install tests on the build machines cannot detect this.
|
|
6
|
+
- Fixed `ack: :durable` without `request_durable_ack` enabled: `ingest_columns`, `ingest_arrow`, `flush(wait: true)`, and `wait` now raise `:invalid_api_call` before sending anything. Previously direct ingestion committed its rows and then raised an in-doubt error.
|
|
7
|
+
- Updated to `c-questdb-client` 7.2.0. The `query_timeout_ms` connect-string key sets a client-side timeout for queries on servers that support it; an expired query raises `QuestDB::Error` with code `:query_timeout`. Standalone WebSocket flushes now check the full frame-size limit before writing anything.
|
|
8
|
+
- Known issue: `c-questdb-client` 7.2.0 still locks rustls 0.23.41, rustls-webpki 0.103.13 and webpki-roots 1.0.8. In addition to the four rustls advisories listed for 0.1.0, RUSTSEC-2026-0381 (webpki-roots) and RUSTSEC-2026-0382 (rustls-webpki), both about trusting distrusted certificate intermediates, are temporarily ignored in `cargo audit` (`.cargo/audit.toml`) until a `c-questdb-client` patch release updates these crates.
|
|
9
|
+
|
|
3
10
|
## 0.1.0 (2026-10-19)
|
|
4
11
|
|
|
5
12
|
Initial pre-1.0 release for early adopters. Public APIs may evolve before 1.0; delivery and acknowledgement semantics are intentional compatibility commitments and changes will be documented here.
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# QuestDB Ruby client
|
|
2
2
|
|
|
3
|
-
`questdb` is an MRI Ruby client for QuestDB's native **QuestDB Wire Protocol (QWP)** over WebSocket. It is backed by the official `c-questdb-client` 7.
|
|
3
|
+
`questdb` is an MRI Ruby client for QuestDB's native **QuestDB Wire Protocol (QWP)** over WebSocket. It is backed by the official `c-questdb-client` 7.2.0 C ABI and does not implement the protocol, networking, TLS, authentication, failover, or store-and-forward in Ruby.
|
|
4
4
|
|
|
5
5
|
The client provides a deployment-level connection pool, pooled row and direct columnar ingestion, streaming SQL queries, safe Ruby dispatch for lifecycle/rejection callbacks, and standalone QWP WebSocket senders. Query batches and callback values are copied into ordinary Ruby objects before borrowed native data expires. Legacy ILP transports are intentionally not part of this client.
|
|
6
6
|
|
data/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -130,8 +130,8 @@ powerfmt | 0.2.0 | MIT OR Apache-2.0
|
|
|
130
130
|
ppv-lite86 | 0.2.21 | MIT OR Apache-2.0
|
|
131
131
|
proc-macro2 | 1.0.106 | MIT OR Apache-2.0
|
|
132
132
|
questdb-confstr | 0.1.1 | Apache-2.0
|
|
133
|
-
questdb-rs | 7.
|
|
134
|
-
questdb-rs-ffi | 7.
|
|
133
|
+
questdb-rs | 7.2.0 | Apache-2.0
|
|
134
|
+
questdb-rs-ffi | 7.2.0 | Apache-2.0 (upstream workspace license)
|
|
135
135
|
quote | 1.0.46 | MIT OR Apache-2.0
|
|
136
136
|
r-efi | 5.3.0 | MIT OR Apache-2.0 OR LGPL-2.1-or-later
|
|
137
137
|
r-efi | 6.0.0 | MIT OR Apache-2.0 OR LGPL-2.1-or-later
|
data/docs/configuration.md
CHANGED
|
@@ -20,15 +20,16 @@ The Ruby-only keywords `connection_listener`, `error_handler`, `connection_event
|
|
|
20
20
|
|
|
21
21
|
## Native settings
|
|
22
22
|
|
|
23
|
-
Settings are passed directly to `c-questdb-client` 7.
|
|
23
|
+
Settings are passed directly to `c-questdb-client` 7.2.0 so new native settings do not require a Ruby release. Important groups include:
|
|
24
24
|
|
|
25
25
|
- pool: `sender_pool_min`, `sender_pool_max`, `query_pool_min`, `query_pool_max`, `acquire_timeout_ms`, `idle_timeout_ms`, `pool_reap`, `lazy_connect`;
|
|
26
26
|
- reconnect/failover: `addr` (comma-separated endpoints), `target`, `zone`, `reconnect_initial_backoff_millis`, `reconnect_max_backoff_millis`, `reconnect_max_duration_millis`, `initial_connect_retry`;
|
|
27
27
|
- store-and-forward: `sf_dir`, `sender_id`, `sf_max_segment_bytes`, `sf_max_total_bytes`, `sf_append_deadline_millis`, `sf_durability`, `sf_sync_interval_millis`, `drain_orphans`, `max_background_drainers`;
|
|
28
28
|
- delivery: `request_durable_ack`, `close_flush_timeout_millis`, `max_buf_size`, `max_name_len`, `error_inbox_capacity`;
|
|
29
|
+
- query: `query_timeout_ms` (expiry raises `:query_timeout`; servers without per-query timeout support reject queries with `:unsupported_server`);
|
|
29
30
|
- authentication/TLS: `username`, `password`, `token`, `auth_timeout_ms`, `tls_verify`, `tls_ca`, `tls_roots`, `tls_roots_password`.
|
|
30
31
|
|
|
31
|
-
Consult the matching upstream 7.
|
|
32
|
+
Consult the matching upstream 7.2.0 connect-string reference for exact constraints. Unknown or invalid settings are rejected by the native parser.
|
|
32
33
|
|
|
33
34
|
Direct column ingestion borrows from the native direct sender pool and uses `acquire_timeout_ms`; initial direct connection retry is bounded by `reconnect_max_duration_millis`. `max_rows_per_batch` is a method argument rather than a connection setting. Direct writes do not use `sf_dir`, do not produce FSNs, and are unaffected by row auto-publish thresholds. `ack: :durable` still requires both `request_durable_ack=on` and server support.
|
|
34
35
|
|
data/docs/design.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Native boundary
|
|
4
4
|
|
|
5
|
-
The gem links statically to official `c-questdb-client` 7.
|
|
5
|
+
The gem links statically to official `c-questdb-client` 7.2.0. Ruby owns thin typed-data wrappers for:
|
|
6
6
|
|
|
7
7
|
- one ref-counted `questdb_db` pool state;
|
|
8
8
|
- short-lived borrowed `qwp_sender` leases;
|
data/docs/python-parity.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Python QWP parity
|
|
2
2
|
|
|
3
|
-
This client tracks the QWP surface of the QuestDB Python 5.0 client at the `c-questdb-client` 7.
|
|
3
|
+
This client tracks the QWP surface of the QuestDB Python 5.0 client at the `c-questdb-client` 7.2.0 ABI boundary. Parity means equivalent delivery, lifecycle, type, and error semantics expressed idiomatically in Ruby; it does not mean retaining Python's legacy ILP APIs.
|
|
4
4
|
|
|
5
5
|
| Python QWP capability | Ruby API | Status |
|
|
6
6
|
| --- | --- | --- |
|
data/docs/releasing.md
CHANGED
|
@@ -17,7 +17,7 @@ The versioned layout is required because MRI's native ABI and `libruby` soname m
|
|
|
17
17
|
|
|
18
18
|
1. Update `QuestDB::Ingress::VERSION` and change the changelog heading from `unreleased` to the intended release date.
|
|
19
19
|
2. Build from a clean candidate commit with recursively initialized submodules on approved QuestDB infrastructure.
|
|
20
|
-
3. Require Ruby and Rust dependency audits to pass without ignored advisories. Temporary exception for 0.1.0: the rustls advisories listed in `.cargo/audit.toml` are ignored until a patch release moves `c-questdb-client` to rustls 0.23.46 or newer; remove each ignore as soon as its fix ships. Enterprise-only durable-ACK qualification remains optional, matching the Python client's policy.
|
|
20
|
+
3. Require Ruby and Rust dependency audits to pass without ignored advisories. Temporary exception for 0.1.x on `c-questdb-client` 7.1.0 and 7.2.0: the rustls, rustls-webpki and webpki-roots advisories listed in `.cargo/audit.toml` are ignored until a patch release moves `c-questdb-client` to rustls 0.23.46, rustls-webpki 0.103.16 and webpki-roots 1.0.10 or newer; remove each ignore as soon as its fix ships. Enterprise-only durable-ACK qualification remains optional, matching the Python client's policy.
|
|
21
21
|
4. Run unit, integration, Arrow, native-memory, package, and bounded fault qualification. Shared-host benchmark results remain informational.
|
|
22
22
|
5. Produce all eight gems, then run `script/verify-gem` on each final file and inspect native linkage. Clean-install each native gem on MRI 3.1, 3.2, 3.3, and 3.4 on its matching platform.
|
|
23
23
|
6. Confirm the source gem installs with `CARGO_NET_OFFLINE=true` in a clean `GEM_HOME` and contains no generated extension or nested QuestDB server source.
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
require "mkmf"
|
|
25
25
|
require "rbconfig"
|
|
26
26
|
require "fileutils"
|
|
27
|
+
require_relative "unlink_build_ruby"
|
|
27
28
|
|
|
28
29
|
root = File.expand_path("../..", __dir__)
|
|
29
30
|
client = File.join(root, "vendor", "c-questdb-client")
|
|
@@ -73,5 +74,6 @@ when /mswin|mingw/
|
|
|
73
74
|
else
|
|
74
75
|
$LOCAL_LIBS << " -ldl -lpthread -lm -lrt -lutil"
|
|
75
76
|
end
|
|
77
|
+
QuestDBExtconf.unlink_build_ruby!(host_os)
|
|
76
78
|
$CFLAGS << " -std=c11 -Wall -Wextra"
|
|
77
79
|
create_makefile("questdb_ingress")
|
|
@@ -509,6 +509,7 @@ static VALUE code_symbol(line_sender_error_code code)
|
|
|
509
509
|
case line_sender_error_batch_too_large: name = "batch_too_large"; break;
|
|
510
510
|
case line_sender_error_store_resend_required: name = "store_resend_required"; break;
|
|
511
511
|
case line_sender_error_symbol_dict_full: name = "symbol_dict_full"; break;
|
|
512
|
+
case line_sender_error_query_timeout: name = "query_timeout"; break;
|
|
512
513
|
default: name = "unknown_error"; break;
|
|
513
514
|
}
|
|
514
515
|
return ID2SYM(rb_intern(name));
|
|
@@ -4380,5 +4381,5 @@ void Init_questdb_ingress(void)
|
|
|
4380
4381
|
rb_define_method(cNativeStandalone, "closed?", standalone_closed, 0);
|
|
4381
4382
|
rb_define_method(cNativeStandalone, "close", standalone_close, 1);
|
|
4382
4383
|
|
|
4383
|
-
rb_define_const(mNative, "C_CLIENT_VERSION", rb_str_new_cstr("7.
|
|
4384
|
+
rb_define_const(mNative, "C_CLIENT_VERSION", rb_str_new_cstr("7.2.0"));
|
|
4384
4385
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
# Link flags for a native extension that must load in any Ruby, not just the
|
|
25
|
+
# one that built it. Precompiled gems would otherwise depend on the build
|
|
26
|
+
# Ruby's libruby path (or, for a static build Ruby, on symbols in its
|
|
27
|
+
# executable). Leaving the Ruby C API unresolved lets it bind to whichever
|
|
28
|
+
# Ruby loads the extension, static or shared.
|
|
29
|
+
module QuestDBExtconf
|
|
30
|
+
BUNDLE_LOADER = /\s*(?:-bundle_loader\s+(?:'[^']*'|"[^"]*"|\S+)|-Wl,-bundle_loader,\S+)/
|
|
31
|
+
# Covers every form libruby reaches a link line in: the mkmf variables (for
|
|
32
|
+
# example EXTDLDFLAGS = "$(LIBRUBYARG_SHARED)" in Ruby 3.1 and 3.2 on macOS)
|
|
33
|
+
# and their expansions.
|
|
34
|
+
LIBRUBY = /\s*(?:\$\(LIBRUBYARG(?:_SHARED|_STATIC)?\)|-l\$\(RUBY_SO_NAME\)|-lruby[\w.-]*)/
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# Call after `require "mkmf"` and before create_makefile.
|
|
39
|
+
def unlink_build_ruby!(host_os)
|
|
40
|
+
return if host_os.match?(/mswin|mingw/) # Windows extensions must import the Ruby DLL.
|
|
41
|
+
|
|
42
|
+
$LIBRUBYARG = ""
|
|
43
|
+
$LIBRUBYARG_SHARED = ""
|
|
44
|
+
return unless host_os.match?(/darwin/)
|
|
45
|
+
|
|
46
|
+
$DLDFLAGS = strip_build_ruby(String($DLDFLAGS))
|
|
47
|
+
%w[DLDFLAGS EXTDLDFLAGS].each do |key|
|
|
48
|
+
[RbConfig::MAKEFILE_CONFIG, RbConfig::CONFIG].each do |config|
|
|
49
|
+
config[key] = strip_build_ruby(config[key]) if config[key]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
$DLDFLAGS << " -Wl,-undefined,dynamic_lookup" unless $DLDFLAGS.include?("dynamic_lookup")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def strip_build_ruby(flags)
|
|
56
|
+
flags.gsub(BUNDLE_LOADER, "").gsub(LIBRUBY, "")
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/questdb/arrow.rb
CHANGED
|
@@ -170,6 +170,7 @@ module QuestDB
|
|
|
170
170
|
checkpoint_batches = Integer(checkpoint_batches)
|
|
171
171
|
raise ArgumentError, "checkpoint_batches must be positive" unless checkpoint_batches.positive?
|
|
172
172
|
@mutex.synchronize { ensure_open! }
|
|
173
|
+
ensure_durable_ack!(ack_level)
|
|
173
174
|
Arrow.with_stream(source) do |capsule|
|
|
174
175
|
@native.ingest_arrow(
|
|
175
176
|
capsule, table, at_kind, at_value, overrides, ack_level, checkpoint_batches
|
|
@@ -201,6 +202,7 @@ module QuestDB
|
|
|
201
202
|
checkpoint_batches = Integer(checkpoint_batches)
|
|
202
203
|
raise ArgumentError, "checkpoint_batches must be positive" unless checkpoint_batches.positive?
|
|
203
204
|
ensure_ws!
|
|
205
|
+
ensure_durable_ack!(ack_level)
|
|
204
206
|
Arrow.with_stream(source) do |capsule|
|
|
205
207
|
@native.ingest_arrow(
|
|
206
208
|
@configuration, capsule, table, at_kind, at_value,
|
data/lib/questdb/client.rb
CHANGED
|
@@ -84,6 +84,7 @@ module QuestDB
|
|
|
84
84
|
error_capacity = validate_capacity!(error_event_inbox_capacity, "error_event_inbox_capacity")
|
|
85
85
|
parsed_settings = effective_settings || Config.parse(configuration)
|
|
86
86
|
@auto_flush = parse_auto_flush(parsed_settings)
|
|
87
|
+
@durable_ack_requested = Config.durable_ack_requested?(parsed_settings)
|
|
87
88
|
@mutex = Mutex.new
|
|
88
89
|
@native = Ingress::Native::Client.new(
|
|
89
90
|
configuration, !connection_listener.nil?, connection_capacity,
|
|
@@ -160,6 +161,7 @@ module QuestDB
|
|
|
160
161
|
ack: ack
|
|
161
162
|
)
|
|
162
163
|
@mutex.synchronize { ensure_open! }
|
|
164
|
+
ensure_durable_ack!(prepared[6])
|
|
163
165
|
return self if prepared[4].zero?
|
|
164
166
|
|
|
165
167
|
@native.ingest_columns(*prepared)
|
|
@@ -249,6 +251,10 @@ module QuestDB
|
|
|
249
251
|
end
|
|
250
252
|
end
|
|
251
253
|
|
|
254
|
+
def ensure_durable_ack!(ack_level)
|
|
255
|
+
Config.ensure_durable_ack!(@durable_ack_requested, ack_level, PooledSender::ACK_LEVELS[:durable])
|
|
256
|
+
end
|
|
257
|
+
|
|
252
258
|
def ensure_open!
|
|
253
259
|
raise Ingress::IngressError.new("client is closed", code: :invalid_api_call) if closed?
|
|
254
260
|
end
|
data/lib/questdb/config.rb
CHANGED
|
@@ -117,6 +117,25 @@ module QuestDB
|
|
|
117
117
|
parts
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
+
# Whether request_durable_ack asks for durable ACKs. Only the absent and
|
|
121
|
+
# explicit off values are recognised, so future tier values pass through
|
|
122
|
+
# to the native client unchanged.
|
|
123
|
+
def durable_ack_requested?(settings)
|
|
124
|
+
value = settings["request_durable_ack"]
|
|
125
|
+
!value.nil? && !value.to_s.strip.match?(/\A(?:off|false)\z/i)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Fails before anything is sent when ack: :durable can never succeed,
|
|
129
|
+
# instead of after a direct batch or a row frame was already published.
|
|
130
|
+
def ensure_durable_ack!(requested, ack_level, durable_level)
|
|
131
|
+
return if requested || ack_level != durable_level
|
|
132
|
+
|
|
133
|
+
raise Ingress::IngressError.new(
|
|
134
|
+
"ack: :durable requires request_durable_ack=on in the configuration; nothing was sent",
|
|
135
|
+
code: :invalid_api_call
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
120
139
|
def setting_value(value, key = nil)
|
|
121
140
|
if key&.to_sym == :tls_verify && value == false
|
|
122
141
|
"unsafe_off"
|
|
@@ -61,6 +61,7 @@ module QuestDB
|
|
|
61
61
|
ensure_open!
|
|
62
62
|
level = normalize_ack(ack)
|
|
63
63
|
raise ArgumentError, "ack: :durable requires wait: true" if level == ACK_LEVELS[:durable] && !wait
|
|
64
|
+
@client.send(:ensure_durable_ack!, level)
|
|
64
65
|
return self if empty? && !wait
|
|
65
66
|
@native.flush(@buffer.send(:native), !!wait, level)
|
|
66
67
|
@batch_started = nil if @buffer.empty?
|
|
@@ -70,7 +71,9 @@ module QuestDB
|
|
|
70
71
|
def wait(timeout_millis: 0, ack: :ok)
|
|
71
72
|
ensure_open!
|
|
72
73
|
timeout = non_negative_integer(timeout_millis, "timeout_millis")
|
|
73
|
-
|
|
74
|
+
level = normalize_ack(ack)
|
|
75
|
+
@client.send(:ensure_durable_ack!, level)
|
|
76
|
+
@native.wait(level, timeout)
|
|
74
77
|
self
|
|
75
78
|
end
|
|
76
79
|
|
data/lib/questdb/sender.rb
CHANGED
|
@@ -121,6 +121,7 @@ module QuestDB
|
|
|
121
121
|
max_rows_per_batch: max_rows_per_batch, ack: ack
|
|
122
122
|
)
|
|
123
123
|
ensure_ws!
|
|
124
|
+
ensure_durable_ack!(prepared[6])
|
|
124
125
|
return self if prepared[4].zero?
|
|
125
126
|
@native.ingest_columns(@configuration, *prepared)
|
|
126
127
|
self
|
|
@@ -142,6 +143,7 @@ module QuestDB
|
|
|
142
143
|
raise ArgumentError, "clear: false requires an explicit buffer" if target.equal?(@buffer) && !clear
|
|
143
144
|
level = normalize_ack(ack)
|
|
144
145
|
raise ArgumentError, "ack: :durable requires wait: true" if level == ACK_LEVELS[:durable] && !wait
|
|
146
|
+
ensure_durable_ack!(level)
|
|
145
147
|
return self if target.empty? && !wait
|
|
146
148
|
@native.flush(target.send(:native), !clear)
|
|
147
149
|
@native.wait(level, 0) if wait
|
|
@@ -151,7 +153,9 @@ module QuestDB
|
|
|
151
153
|
|
|
152
154
|
def wait(timeout_millis: 0, ack: :ok)
|
|
153
155
|
ensure_ws!
|
|
154
|
-
|
|
156
|
+
level = normalize_ack(ack)
|
|
157
|
+
ensure_durable_ack!(level)
|
|
158
|
+
@native.wait(level, non_negative_integer(timeout_millis, "timeout_millis"))
|
|
155
159
|
self
|
|
156
160
|
end
|
|
157
161
|
|
|
@@ -339,6 +343,7 @@ module QuestDB
|
|
|
339
343
|
@dispatcher_threads = []
|
|
340
344
|
@native = @buffer = nil
|
|
341
345
|
@closed = false
|
|
346
|
+
@durable_ack_requested = Config.durable_ack_requested?(settings)
|
|
342
347
|
@init_buf_size = Integer(settings.fetch("init_buf_size", 65_536))
|
|
343
348
|
raise ArgumentError, "init_buf_size must be nonnegative" if @init_buf_size.negative?
|
|
344
349
|
raise RangeError, "init_buf_size exceeds 100 MiB" if @init_buf_size > Ingress::Buffer::MAX_PREALLOCATE_BYTES
|
|
@@ -394,6 +399,10 @@ module QuestDB
|
|
|
394
399
|
true
|
|
395
400
|
end
|
|
396
401
|
|
|
402
|
+
def ensure_durable_ack!(ack_level)
|
|
403
|
+
Config.ensure_durable_ack!(@durable_ack_requested, ack_level, ACK_LEVELS[:durable])
|
|
404
|
+
end
|
|
405
|
+
|
|
397
406
|
def normalize_ack(value)
|
|
398
407
|
value = value.to_sym if value.respond_to?(:to_sym)
|
|
399
408
|
ACK_LEVELS.fetch(value) { raise ArgumentError, "ack must be :ok or :durable" }
|
data/lib/questdb/version.rb
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ___ _ ____ ____
|
|
5
|
+
# / _ \ _ _ ___ ___| |_| _ \| __ )
|
|
6
|
+
# | | | | | | |/ _ \/ __| __| | | | _ \
|
|
7
|
+
# | |_| | |_| | __/\__ \ |_| |_| | |_) |
|
|
8
|
+
# \__\_\\__,_|\___||___/\__|____/|____/
|
|
9
|
+
#
|
|
10
|
+
# Copyright (c) 2014-2019 Appsicle
|
|
11
|
+
# Copyright (c) 2019-2026 QuestDB
|
|
12
|
+
#
|
|
13
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
14
|
+
# you may not use this file except in compliance with the License.
|
|
15
|
+
# You may obtain a copy of the License at
|
|
16
|
+
#
|
|
17
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
18
|
+
#
|
|
19
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
20
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
21
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
22
|
+
# See the License for the specific language governing permissions and
|
|
23
|
+
# limitations under the License.
|
|
24
|
+
#
|
|
25
|
+
|
|
26
|
+
# Fails when a native extension is linked to the Ruby that built it. Such an
|
|
27
|
+
# extension loads on the build machine but not on a user's Ruby, so an install
|
|
28
|
+
# test on the build machine cannot catch it. Reads ELF and Mach-O headers
|
|
29
|
+
# directly; PE (Windows) extensions must import the Ruby DLL and are skipped.
|
|
30
|
+
#
|
|
31
|
+
# script/check-native-linkage lib/questdb/native/*/questdb_ingress.{so,bundle}
|
|
32
|
+
|
|
33
|
+
def elf_needed(bytes)
|
|
34
|
+
raise "32-bit ELF is not supported" unless bytes.getbyte(4) == 2
|
|
35
|
+
le = bytes.getbyte(5) == 1
|
|
36
|
+
q = le ? "Q<" : "Q>"
|
|
37
|
+
l = le ? "L<" : "L>"
|
|
38
|
+
s = le ? "S<" : "S>"
|
|
39
|
+
shoff = bytes.unpack1(q, offset: 0x28)
|
|
40
|
+
shentsize, shnum = bytes.unpack("#{s}#{s}", offset: 0x3a)
|
|
41
|
+
sections = Array.new(shnum) do |i|
|
|
42
|
+
base = shoff + i * shentsize
|
|
43
|
+
{ type: bytes.unpack1(l, offset: base + 4), offset: bytes.unpack1(q, offset: base + 0x18),
|
|
44
|
+
size: bytes.unpack1(q, offset: base + 0x20), link: bytes.unpack1(l, offset: base + 0x28) }
|
|
45
|
+
end
|
|
46
|
+
dynamic = sections.find { |sec| sec[:type] == 6 } or return []
|
|
47
|
+
strtab = sections.fetch(dynamic[:link])
|
|
48
|
+
(0...dynamic[:size] / 16).filter_map do |i|
|
|
49
|
+
tag, val = bytes.unpack("#{q}#{q}", offset: dynamic[:offset] + i * 16)
|
|
50
|
+
next unless tag == 1 # DT_NEEDED
|
|
51
|
+
|
|
52
|
+
bytes.byteslice(strtab[:offset] + val, 256).unpack1("Z*")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def macho(bytes)
|
|
57
|
+
raise "only 64-bit little-endian Mach-O is supported" unless bytes.unpack1("L<") == 0xfeedfacf
|
|
58
|
+
|
|
59
|
+
ncmds = bytes.unpack1("L<", offset: 16)
|
|
60
|
+
offset = 32
|
|
61
|
+
dylibs = []
|
|
62
|
+
symtab = nil
|
|
63
|
+
ncmds.times do
|
|
64
|
+
cmd, size = bytes.unpack("L<L<", offset: offset)
|
|
65
|
+
case cmd
|
|
66
|
+
when 0xc, 0x80000018, 0x8000001f # LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB, LC_REEXPORT_DYLIB
|
|
67
|
+
dylibs << bytes.byteslice(offset + bytes.unpack1("L<", offset: offset + 8), size).unpack1("Z*")
|
|
68
|
+
when 0x2 # LC_SYMTAB
|
|
69
|
+
symtab = bytes.unpack("L<L<L<", offset: offset + 8)
|
|
70
|
+
end
|
|
71
|
+
offset += size
|
|
72
|
+
end
|
|
73
|
+
bound_to_executable = []
|
|
74
|
+
if symtab
|
|
75
|
+
symoff, nsyms, stroff = symtab
|
|
76
|
+
nsyms.times do |i|
|
|
77
|
+
strx, type, _sect, desc = bytes.unpack("L<CCS<", offset: symoff + i * 16)
|
|
78
|
+
next unless (type & 0x0e).zero? && (type & 1) == 1 # undefined external
|
|
79
|
+
|
|
80
|
+
name = bytes.byteslice(stroff + strx, 256).unpack1("Z*")
|
|
81
|
+
bound_to_executable << name if ((desc >> 8) & 0xff) == 0xff # EXECUTABLE_ORDINAL
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
[dylibs, bound_to_executable]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
abort "usage: #{$PROGRAM_NAME} EXTENSION..." if ARGV.empty?
|
|
88
|
+
|
|
89
|
+
failures = []
|
|
90
|
+
ARGV.each do |path|
|
|
91
|
+
bytes = File.binread(path)
|
|
92
|
+
if bytes.start_with?("\x7FELF".b)
|
|
93
|
+
libruby = elf_needed(bytes).grep(/libruby/)
|
|
94
|
+
failures << "#{path}: links #{libruby.join(', ')}" unless libruby.empty?
|
|
95
|
+
elsif bytes.unpack1("L<") == 0xfeedfacf
|
|
96
|
+
dylibs, executable = macho(bytes)
|
|
97
|
+
pinned = dylibs.grep(%r{libruby|/Users/|hostedtoolcache})
|
|
98
|
+
failures << "#{path}: links #{pinned.join(', ')}" unless pinned.empty?
|
|
99
|
+
unless executable.empty?
|
|
100
|
+
failures << "#{path}: #{executable.size} symbols bound to the build Ruby executable " \
|
|
101
|
+
"(e.g. #{executable.first}); link with -undefined dynamic_lookup"
|
|
102
|
+
end
|
|
103
|
+
elsif bytes.start_with?("MZ".b)
|
|
104
|
+
next
|
|
105
|
+
else
|
|
106
|
+
failures << "#{path}: unrecognized binary format"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if failures.empty?
|
|
111
|
+
puts "native linkage ok: #{ARGV.size} extension(s) resolve Ruby's C API at load time"
|
|
112
|
+
else
|
|
113
|
+
warn failures
|
|
114
|
+
exit 1
|
|
115
|
+
end
|
data/test/client_test.rb
CHANGED
|
@@ -35,6 +35,36 @@ class ClientTest < Minitest::Test
|
|
|
35
35
|
assert_raises(RangeError) { QuestDB.connect(host: "localhost", port: 0) }
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# ack: :durable without request_durable_ack can never succeed. It must fail
|
|
39
|
+
# before anything is sent: the unreachable address makes any borrow or
|
|
40
|
+
# connect attempt raise :socket_error instead.
|
|
41
|
+
def test_durable_ack_without_opt_in_fails_before_sending
|
|
42
|
+
fast = LAZY_CONF + "reconnect_max_duration_millis=1;"
|
|
43
|
+
[fast, fast + "request_durable_ack=off;"].each do |conf|
|
|
44
|
+
client = QuestDB.connect(conf)
|
|
45
|
+
[
|
|
46
|
+
-> { client.ingest_columns("t", columns: { v: [1] }, at: QuestDB::SERVER_TIMESTAMP, ack: :durable) },
|
|
47
|
+
-> { client.sender { |s| s.row("t", columns: { v: 1 }, at: QuestDB::SERVER_TIMESTAMP); s.flush(wait: true, ack: :durable) } },
|
|
48
|
+
-> { client.sender { |s| s.wait(ack: :durable) } }
|
|
49
|
+
].each do |call|
|
|
50
|
+
error = assert_raises(IngressError) { call.call }
|
|
51
|
+
assert_equal :invalid_api_call, error.code, error.message
|
|
52
|
+
refute error.in_doubt?, error.message
|
|
53
|
+
assert_match(/request_durable_ack/, error.message)
|
|
54
|
+
end
|
|
55
|
+
client.close
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_durable_ack_opt_in_is_not_rejected_up_front
|
|
60
|
+
client = QuestDB.connect(LAZY_CONF + "reconnect_max_duration_millis=1;request_durable_ack=on;")
|
|
61
|
+
error = assert_raises(IngressError) do
|
|
62
|
+
client.ingest_columns("t", columns: { v: [1] }, at: QuestDB::SERVER_TIMESTAMP, ack: :durable)
|
|
63
|
+
end
|
|
64
|
+
refute_match(/request_durable_ack/, error.message, "an opted-in client must reach the network")
|
|
65
|
+
client.close
|
|
66
|
+
end
|
|
67
|
+
|
|
38
68
|
def test_config_builder_ipv6_escaping_and_duplicates
|
|
39
69
|
conf, = QuestDB::Config.build(host: "::1", port: 9000, tls: true,
|
|
40
70
|
settings: { username: "a;b", lazy_connect: true })
|