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
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
//! bind_count: varint
|
|
36
36
|
//! binds: per egress::binds
|
|
37
37
|
//! query_flags: varint optional trailer; omitted when 0
|
|
38
|
+
//! timeout_ms: varint present iff query_flags & QUERY_FLAG_TIMEOUT
|
|
38
39
|
//! ```
|
|
39
40
|
|
|
40
41
|
use std::net::Ipv4Addr;
|
|
@@ -54,6 +55,14 @@ pub const MAX_BINDS: usize = 1024;
|
|
|
54
55
|
/// (query-scoped dict). Only honoured by servers advertising `CAP_QUERY_FLAGS`.
|
|
55
56
|
pub const QUERY_FLAG_RESET_DICT: u64 = 0x01;
|
|
56
57
|
|
|
58
|
+
/// `query_flags` bit: a `timeout_ms: varint` field follows the `query_flags`
|
|
59
|
+
/// varint. Only honoured by servers advertising `CAP_QUERY_TIMEOUT`.
|
|
60
|
+
///
|
|
61
|
+
/// The server rejects a missing, truncated, zero or negative `timeout_ms`
|
|
62
|
+
/// with `STATUS_PARSE_ERROR`, so "no timeout" is expressed by leaving this
|
|
63
|
+
/// bit clear rather than by sending `0`.
|
|
64
|
+
pub const QUERY_FLAG_TIMEOUT: u64 = 0x02;
|
|
65
|
+
|
|
57
66
|
/// A complete, validated `QUERY_REQUEST` ready for serialization.
|
|
58
67
|
#[derive(Debug, Clone)]
|
|
59
68
|
pub struct QueryRequest {
|
|
@@ -62,6 +71,10 @@ pub struct QueryRequest {
|
|
|
62
71
|
initial_credit: u64,
|
|
63
72
|
binds: Vec<Bind>,
|
|
64
73
|
query_flags: u64,
|
|
74
|
+
/// Per-query timeout in milliseconds. Serialized iff
|
|
75
|
+
/// [`QUERY_FLAG_TIMEOUT`] is set in `query_flags`; `build` keeps the
|
|
76
|
+
/// two in step.
|
|
77
|
+
timeout_ms: u64,
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
/// Byte offset of the 8-byte little-endian `request_id` field inside
|
|
@@ -88,6 +101,7 @@ impl QueryRequest {
|
|
|
88
101
|
initial_credit: 0,
|
|
89
102
|
binds: Vec::new(),
|
|
90
103
|
query_flags: 0,
|
|
104
|
+
timeout_ms: 0,
|
|
91
105
|
}
|
|
92
106
|
}
|
|
93
107
|
|
|
@@ -112,6 +126,12 @@ impl QueryRequest {
|
|
|
112
126
|
}
|
|
113
127
|
if self.query_flags != 0 {
|
|
114
128
|
varint::encode_u64(self.query_flags, out);
|
|
129
|
+
// Appended after the flags word, not after the binds: the
|
|
130
|
+
// server's decoder reads the flags varint and then this field
|
|
131
|
+
// only when the bit is set. `build` guarantees the pairing.
|
|
132
|
+
if self.query_flags & QUERY_FLAG_TIMEOUT != 0 {
|
|
133
|
+
varint::encode_u64(self.timeout_ms, out);
|
|
134
|
+
}
|
|
115
135
|
}
|
|
116
136
|
Ok(())
|
|
117
137
|
}
|
|
@@ -129,6 +149,7 @@ pub struct QueryRequestBuilder {
|
|
|
129
149
|
initial_credit: u64,
|
|
130
150
|
binds: Vec<Bind>,
|
|
131
151
|
query_flags: u64,
|
|
152
|
+
timeout_ms: u64,
|
|
132
153
|
}
|
|
133
154
|
|
|
134
155
|
impl QueryRequestBuilder {
|
|
@@ -145,12 +166,27 @@ impl QueryRequestBuilder {
|
|
|
145
166
|
}
|
|
146
167
|
|
|
147
168
|
/// Set the `query_flags` trailer (`0` = omit it). See
|
|
148
|
-
/// [`QUERY_FLAG_RESET_DICT`]. Default `0`.
|
|
169
|
+
/// [`QUERY_FLAG_RESET_DICT`] and [`QUERY_FLAG_TIMEOUT`]. Default `0`.
|
|
149
170
|
pub fn query_flags(mut self, flags: u64) -> Self {
|
|
150
171
|
self.query_flags = flags;
|
|
151
172
|
self
|
|
152
173
|
}
|
|
153
174
|
|
|
175
|
+
/// Set the per-query `timeout_ms` field. Default `0` (field absent).
|
|
176
|
+
///
|
|
177
|
+
/// Setting a non-zero value does **not** by itself put the field on the
|
|
178
|
+
/// wire: [`QUERY_FLAG_TIMEOUT`] must also be set in
|
|
179
|
+
/// [`query_flags`](Self::query_flags), which is what cap-gates the
|
|
180
|
+
/// field against servers that would reject it. [`build`](Self::build)
|
|
181
|
+
/// rejects the two being out of step.
|
|
182
|
+
///
|
|
183
|
+
/// Capped at `i64::MAX`: the server decodes the field as a signed
|
|
184
|
+
/// long, so anything larger would arrive negative and be rejected.
|
|
185
|
+
pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
|
|
186
|
+
self.timeout_ms = timeout_ms.min(i64::MAX as u64);
|
|
187
|
+
self
|
|
188
|
+
}
|
|
189
|
+
|
|
154
190
|
/// Append a typed bind parameter at the next position.
|
|
155
191
|
pub fn bind(mut self, value: Bind) -> Self {
|
|
156
192
|
self.binds.push(value);
|
|
@@ -268,12 +304,29 @@ impl QueryRequestBuilder {
|
|
|
268
304
|
check_bindable(bind.kind())
|
|
269
305
|
.map_err(|e| fmt!(InvalidBind, "bind ${}: {}", i + 1, e.msg()))?;
|
|
270
306
|
}
|
|
307
|
+
// Keep the flag bit and the field in step. Either half alone
|
|
308
|
+
// produces a frame the server reads wrong: the bit without a
|
|
309
|
+
// positive value is a `STATUS_PARSE_ERROR` (it rejects zero), and
|
|
310
|
+
// a value without the bit silently drops the timeout while the
|
|
311
|
+
// caller believes it was applied. Caught here rather than on the
|
|
312
|
+
// wire so the mistake cannot leave the process.
|
|
313
|
+
let flag_set = self.query_flags & QUERY_FLAG_TIMEOUT != 0;
|
|
314
|
+
if flag_set != (self.timeout_ms > 0) {
|
|
315
|
+
return Err(fmt!(
|
|
316
|
+
InvalidApiCall,
|
|
317
|
+
"QUERY_FLAG_TIMEOUT and timeout_ms disagree: flag {}, timeout_ms {} \
|
|
318
|
+
(set both, or neither)",
|
|
319
|
+
if flag_set { "set" } else { "clear" },
|
|
320
|
+
self.timeout_ms,
|
|
321
|
+
));
|
|
322
|
+
}
|
|
271
323
|
Ok(QueryRequest {
|
|
272
324
|
request_id: self.request_id,
|
|
273
325
|
sql: self.sql,
|
|
274
326
|
initial_credit: self.initial_credit,
|
|
275
327
|
binds: self.binds,
|
|
276
328
|
query_flags: self.query_flags,
|
|
329
|
+
timeout_ms: self.timeout_ms,
|
|
277
330
|
})
|
|
278
331
|
}
|
|
279
332
|
}
|
|
@@ -424,6 +477,126 @@ mod tests {
|
|
|
424
477
|
assert_eq!(*with_flag.last().unwrap(), QUERY_FLAG_RESET_DICT as u8);
|
|
425
478
|
}
|
|
426
479
|
|
|
480
|
+
#[test]
|
|
481
|
+
fn timeout_trailer_follows_flags() {
|
|
482
|
+
// No timeout -> byte-identical to the bindless baseline, which is
|
|
483
|
+
// what keeps an older server seeing the layout it has always seen.
|
|
484
|
+
let mut baseline = Vec::new();
|
|
485
|
+
QueryRequest::builder("X")
|
|
486
|
+
.build()
|
|
487
|
+
.unwrap()
|
|
488
|
+
.encode(&mut baseline)
|
|
489
|
+
.unwrap();
|
|
490
|
+
|
|
491
|
+
let mut with_timeout = Vec::new();
|
|
492
|
+
QueryRequest::builder("X")
|
|
493
|
+
.query_flags(QUERY_FLAG_TIMEOUT)
|
|
494
|
+
.timeout_ms(1_000)
|
|
495
|
+
.build()
|
|
496
|
+
.unwrap()
|
|
497
|
+
.encode(&mut with_timeout)
|
|
498
|
+
.unwrap();
|
|
499
|
+
|
|
500
|
+
// flags varint (0x02) then timeout varint (1000 = 0xE8 0x07).
|
|
501
|
+
assert_eq!(&with_timeout[..baseline.len()], &baseline[..]);
|
|
502
|
+
assert_eq!(&with_timeout[baseline.len()..], &[0x02, 0xE8, 0x07]);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
#[test]
|
|
506
|
+
fn timeout_trailer_combines_with_reset_dict() {
|
|
507
|
+
// Both bits in one word, timeout field last. The server decodes
|
|
508
|
+
// the flags varint first, so a client that emitted the field
|
|
509
|
+
// before the flags would be read as a bad flags word.
|
|
510
|
+
let mut buf = Vec::new();
|
|
511
|
+
QueryRequest::builder("X")
|
|
512
|
+
.bind_i64(7)
|
|
513
|
+
.query_flags(QUERY_FLAG_RESET_DICT | QUERY_FLAG_TIMEOUT)
|
|
514
|
+
.timeout_ms(1)
|
|
515
|
+
.build()
|
|
516
|
+
.unwrap()
|
|
517
|
+
.encode(&mut buf)
|
|
518
|
+
.unwrap();
|
|
519
|
+
assert_eq!(&buf[buf.len() - 2..], &[0x03, 0x01]);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
#[test]
|
|
523
|
+
fn timeout_ms_caps_at_i64_max() {
|
|
524
|
+
let mut buf = Vec::new();
|
|
525
|
+
QueryRequest::builder("X")
|
|
526
|
+
.query_flags(QUERY_FLAG_TIMEOUT)
|
|
527
|
+
.timeout_ms(u64::MAX)
|
|
528
|
+
.build()
|
|
529
|
+
.unwrap()
|
|
530
|
+
.encode(&mut buf)
|
|
531
|
+
.unwrap();
|
|
532
|
+
let mut expected = vec![0xFF; 8];
|
|
533
|
+
expected.push(0x7F);
|
|
534
|
+
assert_eq!(&buf[buf.len() - 9..], &expected[..]);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
#[test]
|
|
538
|
+
fn timeout_ms_without_flag_rejected() {
|
|
539
|
+
// A value with no bit would silently not reach the server.
|
|
540
|
+
let err = QueryRequest::builder("X")
|
|
541
|
+
.timeout_ms(1_000)
|
|
542
|
+
.build()
|
|
543
|
+
.unwrap_err();
|
|
544
|
+
assert_eq!(err.code(), ErrorCode::InvalidApiCall);
|
|
545
|
+
assert!(err.msg().contains("timeout_ms"), "msg: {}", err.msg());
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
#[test]
|
|
549
|
+
fn timeout_flag_without_value_rejected() {
|
|
550
|
+
// The bit with no value is `STATUS_PARSE_ERROR` at the server,
|
|
551
|
+
// which rejects a zero timeout.
|
|
552
|
+
let err = QueryRequest::builder("X")
|
|
553
|
+
.query_flags(QUERY_FLAG_TIMEOUT)
|
|
554
|
+
.build()
|
|
555
|
+
.unwrap_err();
|
|
556
|
+
assert_eq!(err.code(), ErrorCode::InvalidApiCall);
|
|
557
|
+
assert!(err.msg().contains("timeout_ms"), "msg: {}", err.msg());
|
|
558
|
+
|
|
559
|
+
// An explicit zero alongside the bit is the same mistake.
|
|
560
|
+
let err = QueryRequest::builder("X")
|
|
561
|
+
.query_flags(QUERY_FLAG_TIMEOUT)
|
|
562
|
+
.timeout_ms(0)
|
|
563
|
+
.build()
|
|
564
|
+
.unwrap_err();
|
|
565
|
+
assert_eq!(err.code(), ErrorCode::InvalidApiCall);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
#[test]
|
|
569
|
+
fn query_flag_bits_are_distinct() {
|
|
570
|
+
assert_eq!(QUERY_FLAG_RESET_DICT, 0x01);
|
|
571
|
+
assert_eq!(QUERY_FLAG_TIMEOUT, 0x02);
|
|
572
|
+
assert_eq!(QUERY_FLAG_RESET_DICT & QUERY_FLAG_TIMEOUT, 0);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
#[test]
|
|
576
|
+
fn timeout_does_not_move_the_request_id() {
|
|
577
|
+
// The failover-replay path patches `[REQUEST_ID_OFFSET..+8]` of the
|
|
578
|
+
// encoded buffer. The timeout field is a trailer, so it must not
|
|
579
|
+
// shift that span.
|
|
580
|
+
const SENTINEL: i64 = 0x0123_4567_89AB_CDEF;
|
|
581
|
+
let mut buf = Vec::new();
|
|
582
|
+
QueryRequest::builder("S")
|
|
583
|
+
.request_id(SENTINEL)
|
|
584
|
+
.query_flags(QUERY_FLAG_TIMEOUT)
|
|
585
|
+
.timeout_ms(5_000)
|
|
586
|
+
.build()
|
|
587
|
+
.unwrap()
|
|
588
|
+
.encode(&mut buf)
|
|
589
|
+
.unwrap();
|
|
590
|
+
assert_eq!(
|
|
591
|
+
i64::from_le_bytes(
|
|
592
|
+
buf[REQUEST_ID_OFFSET..REQUEST_ID_OFFSET + 8]
|
|
593
|
+
.try_into()
|
|
594
|
+
.unwrap()
|
|
595
|
+
),
|
|
596
|
+
SENTINEL
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
427
600
|
#[test]
|
|
428
601
|
fn sql_too_long_rejected() {
|
|
429
602
|
let big = "a".repeat(MAX_SQL_BYTES + 1);
|
|
@@ -52,7 +52,7 @@ use crate::egress::config::{Endpoint, ReaderConfig, Target};
|
|
|
52
52
|
use crate::egress::decoder::DecodedBatch;
|
|
53
53
|
use crate::egress::decoder::ZstdScratch;
|
|
54
54
|
use crate::egress::query_request::{
|
|
55
|
-
QUERY_FLAG_RESET_DICT, QueryRequest, QueryRequestBuilder, REQUEST_ID_OFFSET,
|
|
55
|
+
QUERY_FLAG_RESET_DICT, QUERY_FLAG_TIMEOUT, QueryRequest, QueryRequestBuilder, REQUEST_ID_OFFSET,
|
|
56
56
|
};
|
|
57
57
|
use crate::egress::schema::Schema;
|
|
58
58
|
use crate::egress::server_event::UpgradeReject;
|
|
@@ -60,7 +60,7 @@ use crate::egress::server_event::{ServerEvent, ServerInfo, ServerRole, decode_fr
|
|
|
60
60
|
use crate::egress::symbol_dict::SymbolDict;
|
|
61
61
|
use crate::egress::tracker::HostHealthTracker;
|
|
62
62
|
use crate::egress::transport::{CLOSE_TIMEOUT, WRITE_TIMEOUT, WsTransport};
|
|
63
|
-
use crate::egress::wire::capabilities::has_query_flags;
|
|
63
|
+
use crate::egress::wire::capabilities::{has_query_flags, has_query_timeout};
|
|
64
64
|
use crate::egress::wire::header::HEADER_LEN;
|
|
65
65
|
use crate::egress::wire::msg_kind::MsgKind;
|
|
66
66
|
use crate::egress::wire::varint;
|
|
@@ -696,10 +696,12 @@ impl Reader {
|
|
|
696
696
|
/// exclusively borrows the reader; only one in-flight cursor at a
|
|
697
697
|
/// time. Append binds in placeholder order, then call `.execute()`.
|
|
698
698
|
pub fn prepare<S: Into<String>>(&mut self, sql: S) -> ReaderQuery<'_> {
|
|
699
|
+
let timeout_ms = self.cfg.query_timeout_ms;
|
|
699
700
|
ReaderQuery {
|
|
700
701
|
reader: self,
|
|
701
702
|
builder: QueryRequest::builder(sql),
|
|
702
703
|
reset_symbol_dict: false,
|
|
704
|
+
timeout_ms,
|
|
703
705
|
on_failover_reset: None,
|
|
704
706
|
on_failover_progress: None,
|
|
705
707
|
}
|
|
@@ -876,6 +878,12 @@ pub struct ReaderQuery<'r> {
|
|
|
876
878
|
/// `query_flags` trailer at [`Self::execute`] iff the server advertised
|
|
877
879
|
/// `CAP_QUERY_FLAGS`.
|
|
878
880
|
reset_symbol_dict: bool,
|
|
881
|
+
/// Per-query timeout in milliseconds; `0` = none. Seeded from
|
|
882
|
+
/// [`ReaderConfig::query_timeout_ms`] by [`Reader::prepare`] and
|
|
883
|
+
/// overridden by [`Self::timeout`]. Turned into a `timeout_ms` field at
|
|
884
|
+
/// [`Self::execute`], which also rejects a non-zero value against a
|
|
885
|
+
/// server that does not advertise `CAP_QUERY_TIMEOUT`.
|
|
886
|
+
timeout_ms: u64,
|
|
879
887
|
/// Optional handler called every time the cursor reconnects after a
|
|
880
888
|
/// transport-level failure (see [`FailoverResetEvent`]).
|
|
881
889
|
on_failover_reset: Option<FailoverResetCallback<'r>>,
|
|
@@ -912,6 +920,55 @@ impl<'r> ReaderQuery<'r> {
|
|
|
912
920
|
self
|
|
913
921
|
}
|
|
914
922
|
|
|
923
|
+
/// Run this query under its own timeout instead of the server-wide
|
|
924
|
+
/// `query.timeout`, overriding the connection's
|
|
925
|
+
/// [`ReaderConfig::query_timeout_ms`] default.
|
|
926
|
+
///
|
|
927
|
+
/// [`Duration::ZERO`] means "no client timeout" and clears a default
|
|
928
|
+
/// inherited from the connect string. The server sets no ceiling, so a
|
|
929
|
+
/// value above `query.timeout` is honoured — which is the point for a
|
|
930
|
+
/// long transform. A sub-millisecond non-zero duration is rounded *up*
|
|
931
|
+
/// to 1 ms: truncating it to zero would silently mean "no timeout".
|
|
932
|
+
///
|
|
933
|
+
/// # Errors
|
|
934
|
+
///
|
|
935
|
+
/// [`Self::execute`] fails with [`crate::ErrorCode::UnsupportedServer`]
|
|
936
|
+
/// when a non-zero timeout is set and the server does not advertise
|
|
937
|
+
/// `CAP_QUERY_TIMEOUT`, rather than running the query under the server
|
|
938
|
+
/// default and letting the caller believe its timeout applied. The
|
|
939
|
+
/// refusal happens before anything is written, so the connection is
|
|
940
|
+
/// untouched. Raise the server's `query.timeout` instead, or clear the
|
|
941
|
+
/// timeout here. A mid-query failover onto such a server fails the same
|
|
942
|
+
/// way instead of replaying the query. The code is deliberately not
|
|
943
|
+
/// [`crate::ErrorCode::QueryTimeout`]: that one means the budget ran
|
|
944
|
+
/// out, and retrying it with a larger budget is a reasonable reaction
|
|
945
|
+
/// that would never succeed here.
|
|
946
|
+
///
|
|
947
|
+
/// # Behaviour on expiry
|
|
948
|
+
///
|
|
949
|
+
/// The query ends with [`crate::ErrorCode::QueryTimeout`] and the
|
|
950
|
+
/// connection stays open and authenticated, so the next query runs on
|
|
951
|
+
/// it without reconnecting. **Do not retry a write on that error:** a
|
|
952
|
+
/// statement that finishes past its timeout is reported as `EXEC_DONE`
|
|
953
|
+
/// instead (so a retry cannot apply it twice), and a DDL / `INSERT` /
|
|
954
|
+
/// `UPDATE` that timed out waiting for the table writer may still be
|
|
955
|
+
/// applied afterwards.
|
|
956
|
+
///
|
|
957
|
+
/// The server enforces it per request, so a failover replay re-sends
|
|
958
|
+
/// the full timeout rather than the remainder. Nothing bounds the
|
|
959
|
+
/// query's end-to-end wall-clock:
|
|
960
|
+
/// [`ReaderConfig::failover_max_duration_ms`] only limits when a new
|
|
961
|
+
/// failover round may start.
|
|
962
|
+
pub fn timeout(mut self, timeout: Duration) -> Self {
|
|
963
|
+
let ms = timeout.as_millis();
|
|
964
|
+
self.timeout_ms = if ms == 0 && !timeout.is_zero() {
|
|
965
|
+
1
|
|
966
|
+
} else {
|
|
967
|
+
ms.min(u64::MAX as u128) as u64
|
|
968
|
+
};
|
|
969
|
+
self
|
|
970
|
+
}
|
|
971
|
+
|
|
915
972
|
/// Install a callback fired every time the cursor's underlying
|
|
916
973
|
/// connection is replaced via mid-query failover. The closure
|
|
917
974
|
/// receives a [`FailoverResetEvent`] describing the new endpoint and
|
|
@@ -1144,20 +1201,18 @@ impl<'r> ReaderQuery<'r> {
|
|
|
1144
1201
|
// Cap-gate the query_flags trailer: only emit it when the server
|
|
1145
1202
|
// advertised CAP_QUERY_FLAGS, so an older server sees the baseline
|
|
1146
1203
|
// QUERY_REQUEST layout and the reset request silently degrades.
|
|
1147
|
-
let
|
|
1204
|
+
let capabilities = self
|
|
1148
1205
|
.reader
|
|
1149
1206
|
.server_info()
|
|
1150
|
-
.map(|info|
|
|
1151
|
-
.unwrap_or(
|
|
1152
|
-
let query_flags =
|
|
1153
|
-
|
|
1154
|
-
} else {
|
|
1155
|
-
0
|
|
1156
|
-
};
|
|
1207
|
+
.map(|info| info.capabilities)
|
|
1208
|
+
.unwrap_or(0);
|
|
1209
|
+
let query_flags =
|
|
1210
|
+
derive_query_flags(capabilities, self.reset_symbol_dict, self.timeout_ms)?;
|
|
1157
1211
|
let req = self
|
|
1158
1212
|
.builder
|
|
1159
1213
|
.request_id(request_id)
|
|
1160
1214
|
.query_flags(query_flags)
|
|
1215
|
+
.timeout_ms(self.timeout_ms)
|
|
1161
1216
|
.build()?;
|
|
1162
1217
|
let credit_enabled = req.initial_credit() > 0;
|
|
1163
1218
|
// Encode the QUERY_REQUEST once and stash the bytes on the
|
|
@@ -1223,6 +1278,7 @@ impl<'r> ReaderQuery<'r> {
|
|
|
1223
1278
|
done: false,
|
|
1224
1279
|
terminal_error: None,
|
|
1225
1280
|
encoded_request,
|
|
1281
|
+
timeout_ms: self.timeout_ms,
|
|
1226
1282
|
on_failover_reset: self.on_failover_reset,
|
|
1227
1283
|
on_failover_progress: self.on_failover_progress,
|
|
1228
1284
|
failover_budget,
|
|
@@ -1244,6 +1300,49 @@ impl<'r> ReaderQuery<'r> {
|
|
|
1244
1300
|
}
|
|
1245
1301
|
}
|
|
1246
1302
|
|
|
1303
|
+
/// The `query_flags` word for a query sent to a server advertising
|
|
1304
|
+
/// `capabilities`.
|
|
1305
|
+
fn derive_query_flags(capabilities: u32, reset_symbol_dict: bool, timeout_ms: u64) -> Result<u64> {
|
|
1306
|
+
let mut query_flags = 0u64;
|
|
1307
|
+
if reset_symbol_dict && has_query_flags(capabilities) {
|
|
1308
|
+
query_flags |= QUERY_FLAG_RESET_DICT;
|
|
1309
|
+
}
|
|
1310
|
+
if timeout_ms > 0 {
|
|
1311
|
+
// A timeout is a correctness request, not a hint, so it does
|
|
1312
|
+
// NOT degrade the way `reset_symbol_dict` does: silently
|
|
1313
|
+
// dropping it would leave the caller believing it had a long
|
|
1314
|
+
// budget while the server kills the query at `query.timeout`.
|
|
1315
|
+
// The field rides *inside* the query_flags trailer, so both
|
|
1316
|
+
// bits are required — a server advertising the timeout bit
|
|
1317
|
+
// without the flags bit could not parse the frame we would
|
|
1318
|
+
// send.
|
|
1319
|
+
require_query_timeout(capabilities, timeout_ms)?;
|
|
1320
|
+
query_flags |= QUERY_FLAG_TIMEOUT;
|
|
1321
|
+
}
|
|
1322
|
+
Ok(query_flags)
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/// Fails with `UnsupportedServer` when `capabilities` cannot carry a
|
|
1326
|
+
/// non-zero `timeout_ms`. Checked on every connection a query is sent to,
|
|
1327
|
+
/// so a failover replay cannot land the timeout on a server that ignores it.
|
|
1328
|
+
fn require_query_timeout(capabilities: u32, timeout_ms: u64) -> Result<()> {
|
|
1329
|
+
if timeout_ms > 0 && !(has_query_flags(capabilities) && has_query_timeout(capabilities)) {
|
|
1330
|
+
return Err(fmt!(
|
|
1331
|
+
UnsupportedServer,
|
|
1332
|
+
"server does not support per-query timeouts \
|
|
1333
|
+
(SERVER_INFO capabilities 0x{:08X}, need CAP_QUERY_TIMEOUT 0x{:08X} \
|
|
1334
|
+
and CAP_QUERY_FLAGS 0x{:08X}): the requested {} ms timeout cannot be \
|
|
1335
|
+
honoured. Raise the server's `query.timeout` instead, or clear the \
|
|
1336
|
+
per-query timeout.",
|
|
1337
|
+
capabilities,
|
|
1338
|
+
crate::egress::wire::capabilities::CAP_QUERY_TIMEOUT,
|
|
1339
|
+
crate::egress::wire::capabilities::CAP_QUERY_FLAGS,
|
|
1340
|
+
timeout_ms,
|
|
1341
|
+
));
|
|
1342
|
+
}
|
|
1343
|
+
Ok(())
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1247
1346
|
/// Patch the request_id span of a stashed `QUERY_REQUEST` payload in
|
|
1248
1347
|
/// place and return it as fresh `Bytes`.
|
|
1249
1348
|
///
|
|
@@ -1423,9 +1522,20 @@ pub enum Terminal {
|
|
|
1423
1522
|
/// `RESULT_END` (`0x12`).
|
|
1424
1523
|
End { final_seq: u64, total_rows: u64 },
|
|
1425
1524
|
/// `EXEC_DONE` (`0x16`) — non-SELECT acknowledgement.
|
|
1525
|
+
///
|
|
1526
|
+
/// `rows_affected` is the row count of an `INSERT` / `UPDATE`. The
|
|
1527
|
+
/// server reports no count for statements it executes at parse time
|
|
1528
|
+
/// (`TRUNCATE`, `RENAME TABLE`, `SET`, ...): it sends `-1`, which the
|
|
1529
|
+
/// unsigned varint carries as [`ROWS_AFFECTED_UNKNOWN`]. `op_type` is
|
|
1530
|
+
/// the server's operation-type byte, carried through opaquely.
|
|
1426
1531
|
ExecDone { op_type: u8, rows_affected: u64 },
|
|
1427
1532
|
}
|
|
1428
1533
|
|
|
1534
|
+
/// The `rows_affected` value of a [`Terminal::ExecDone`] whose statement
|
|
1535
|
+
/// reports no row count (`u64::MAX`, the server's `-1` as an unsigned
|
|
1536
|
+
/// varint). Compare against it rather than against the literal.
|
|
1537
|
+
pub const ROWS_AFFECTED_UNKNOWN: u64 = u64::MAX;
|
|
1538
|
+
|
|
1429
1539
|
/// Streaming cursor over `RESULT_BATCH` frames.
|
|
1430
1540
|
///
|
|
1431
1541
|
/// `next_batch` advances the stream by one batch, returning `None` once a
|
|
@@ -1453,6 +1563,8 @@ pub struct Cursor<'r> {
|
|
|
1453
1563
|
/// across reconnects, only the 8-byte request_id span is mutated
|
|
1454
1564
|
/// in place.
|
|
1455
1565
|
encoded_request: Bytes,
|
|
1566
|
+
/// Per-query timeout baked into `encoded_request`; `0` = none.
|
|
1567
|
+
timeout_ms: u64,
|
|
1456
1568
|
/// User callback fired right before replayed batches arrive on a
|
|
1457
1569
|
/// new connection. See [`ReaderQuery::on_failover_reset`].
|
|
1458
1570
|
on_failover_reset: Option<FailoverResetCallback<'r>>,
|
|
@@ -1572,9 +1684,17 @@ impl<'r> Cursor<'r> {
|
|
|
1572
1684
|
self.terminal.as_ref()
|
|
1573
1685
|
}
|
|
1574
1686
|
|
|
1575
|
-
/// Whether dropping this cursor leaves its reader connection reusable
|
|
1687
|
+
/// Whether dropping this cursor leaves its reader connection reusable:
|
|
1688
|
+
/// the stream reached a terminal frame (or was cancelled to one), no
|
|
1689
|
+
/// query is still active on the connection, and the transport is up.
|
|
1690
|
+
///
|
|
1691
|
+
/// `done` alone is not enough. A cursor ended mid-stream by a
|
|
1692
|
+
/// client-side error (an Arrow conversion failure, say) is `done` but
|
|
1693
|
+
/// still has the query's frames in flight; [`Drop`] sends `CANCEL` and
|
|
1694
|
+
/// tears the transport down for it, so it must report `false` here
|
|
1695
|
+
/// rather than let a pool recycle a connection that is about to die.
|
|
1576
1696
|
pub fn connection_reusable(&self) -> bool {
|
|
1577
|
-
self.done && !self.reader.transport_torn_down()
|
|
1697
|
+
self.done && !self.reader.cursor_active && !self.reader.transport_torn_down()
|
|
1578
1698
|
}
|
|
1579
1699
|
|
|
1580
1700
|
/// Pass-through to [`Reader::credit_granted_total`]. Exists so
|
|
@@ -2095,8 +2215,7 @@ impl<'r> Cursor<'r> {
|
|
|
2095
2215
|
match self.replay_query_same_connection() {
|
|
2096
2216
|
Ok(()) => continue,
|
|
2097
2217
|
Err(e) => {
|
|
2098
|
-
self.
|
|
2099
|
-
self.done = true;
|
|
2218
|
+
self.terminate_with_close();
|
|
2100
2219
|
return Err(e);
|
|
2101
2220
|
}
|
|
2102
2221
|
}
|
|
@@ -2448,6 +2567,33 @@ impl<'r> Cursor<'r> {
|
|
|
2448
2567
|
// and queries that may carry multi-MB `Bind::Binary`
|
|
2449
2568
|
// payloads, this is the difference between a few bytes
|
|
2450
2569
|
// and gigabytes of churn per failure event.
|
|
2570
|
+
// The stashed bytes carry the timeout the *original* server
|
|
2571
|
+
// accepted; a replacement that lacks the capability would run
|
|
2572
|
+
// the query under its own default instead.
|
|
2573
|
+
let capabilities = self
|
|
2574
|
+
.reader
|
|
2575
|
+
.server_info()
|
|
2576
|
+
.map(|info| info.capabilities)
|
|
2577
|
+
.unwrap_or(0);
|
|
2578
|
+
if let Err(e) = require_query_timeout(capabilities, self.timeout_ms) {
|
|
2579
|
+
if let Some(cb) = self.on_failover_progress.as_mut() {
|
|
2580
|
+
let event = FailoverProgressEvent {
|
|
2581
|
+
phase: FailoverPhase::GaveUp,
|
|
2582
|
+
failed_addr: failed_addr.clone(),
|
|
2583
|
+
new_addr: None,
|
|
2584
|
+
new_server_info: None,
|
|
2585
|
+
new_request_id: None,
|
|
2586
|
+
attempt: attempts,
|
|
2587
|
+
trigger: trigger.clone(),
|
|
2588
|
+
elapsed: started.elapsed(),
|
|
2589
|
+
final_error: Some(e.clone()),
|
|
2590
|
+
};
|
|
2591
|
+
cb(&event);
|
|
2592
|
+
}
|
|
2593
|
+
self.reader.cursor_active = false;
|
|
2594
|
+
self.done = true;
|
|
2595
|
+
return Err(e);
|
|
2596
|
+
}
|
|
2451
2597
|
let new_rid = self.reader.alloc_request_id();
|
|
2452
2598
|
self.request_id = new_rid;
|
|
2453
2599
|
self.encoded_request =
|
|
@@ -2681,6 +2827,19 @@ impl<'r> Cursor<'r> {
|
|
|
2681
2827
|
None => fmt!(InvalidApiCall, "cursor is terminal; add_credit not allowed"),
|
|
2682
2828
|
});
|
|
2683
2829
|
}
|
|
2830
|
+
// Same first-cause capture as `next_batch`: without it a cursor
|
|
2831
|
+
// killed here reads as a clean EOF on the next `next_batch`.
|
|
2832
|
+
let result = self.add_credit_inner(additional_bytes);
|
|
2833
|
+
if let Err(e) = &result
|
|
2834
|
+
&& self.done
|
|
2835
|
+
&& self.terminal_error.is_none()
|
|
2836
|
+
{
|
|
2837
|
+
self.terminal_error = Some(e.clone());
|
|
2838
|
+
}
|
|
2839
|
+
result
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
fn add_credit_inner(&mut self, additional_bytes: u64) -> Result<()> {
|
|
2684
2843
|
let first_err = match self.send_credit_frame(additional_bytes) {
|
|
2685
2844
|
Ok(()) => return Ok(()),
|
|
2686
2845
|
Err(e) => e,
|
|
@@ -3261,6 +3420,7 @@ fn map_server_status(
|
|
|
3261
3420
|
S::SecurityError => C::ServerSecurityError,
|
|
3262
3421
|
S::Cancelled => C::Cancelled,
|
|
3263
3422
|
S::LimitExceeded => C::ServerLimitExceeded,
|
|
3423
|
+
S::QueryTimeout => C::QueryTimeout,
|
|
3264
3424
|
};
|
|
3265
3425
|
crate::Error::new(code, message)
|
|
3266
3426
|
}
|
|
@@ -3367,6 +3527,90 @@ mod tests {
|
|
|
3367
3527
|
assert_eq!(i64::from_le_bytes(id_bytes), OLD_RID);
|
|
3368
3528
|
}
|
|
3369
3529
|
|
|
3530
|
+
/// The flag word `execute()` derives from the capability bits. Pinned
|
|
3531
|
+
/// here as a truth table so the cap-gating cannot regress into
|
|
3532
|
+
/// "always send" (which an older server rejects) or "never send".
|
|
3533
|
+
#[test]
|
|
3534
|
+
fn query_flag_derivation_matrix() {
|
|
3535
|
+
use crate::egress::wire::capabilities::{CAP_QUERY_FLAGS, CAP_QUERY_TIMEOUT, CAP_ZONE};
|
|
3536
|
+
|
|
3537
|
+
/// `None` = execute() must fail fast.
|
|
3538
|
+
fn derive(caps: u32, reset_dict: bool, timeout_ms: u64) -> Option<u64> {
|
|
3539
|
+
derive_query_flags(caps, reset_dict, timeout_ms).ok()
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
let both = CAP_QUERY_FLAGS | CAP_QUERY_TIMEOUT;
|
|
3543
|
+
|
|
3544
|
+
// No timeout requested: unchanged behaviour on every server.
|
|
3545
|
+
assert_eq!(derive(0, false, 0), Some(0));
|
|
3546
|
+
assert_eq!(
|
|
3547
|
+
derive(0, true, 0),
|
|
3548
|
+
Some(0),
|
|
3549
|
+
"no caps -> dict reset degrades"
|
|
3550
|
+
);
|
|
3551
|
+
assert_eq!(
|
|
3552
|
+
derive(CAP_QUERY_FLAGS, true, 0),
|
|
3553
|
+
Some(QUERY_FLAG_RESET_DICT)
|
|
3554
|
+
);
|
|
3555
|
+
|
|
3556
|
+
// Timeout requested against a capable server.
|
|
3557
|
+
assert_eq!(derive(both, false, 1_000), Some(QUERY_FLAG_TIMEOUT));
|
|
3558
|
+
assert_eq!(
|
|
3559
|
+
derive(both, true, 1_000),
|
|
3560
|
+
Some(QUERY_FLAG_RESET_DICT | QUERY_FLAG_TIMEOUT),
|
|
3561
|
+
"both bits ride in one flags word"
|
|
3562
|
+
);
|
|
3563
|
+
assert_eq!(
|
|
3564
|
+
derive(both | CAP_ZONE, false, 1_000),
|
|
3565
|
+
Some(QUERY_FLAG_TIMEOUT),
|
|
3566
|
+
"unrelated capability bits must not disturb the derivation"
|
|
3567
|
+
);
|
|
3568
|
+
|
|
3569
|
+
// Timeout requested against a server that cannot honour it.
|
|
3570
|
+
assert_eq!(derive(0, false, 1_000), None, "no capabilities at all");
|
|
3571
|
+
assert_eq!(
|
|
3572
|
+
derive(CAP_QUERY_FLAGS, false, 1_000),
|
|
3573
|
+
None,
|
|
3574
|
+
"flags trailer but no timeout support"
|
|
3575
|
+
);
|
|
3576
|
+
assert_eq!(
|
|
3577
|
+
derive(CAP_QUERY_TIMEOUT, false, 1_000),
|
|
3578
|
+
None,
|
|
3579
|
+
"timeout bit without the flags trailer it rides in"
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
#[test]
|
|
3584
|
+
fn missing_timeout_capability_is_unsupported_server_not_query_timeout() {
|
|
3585
|
+
use crate::egress::wire::capabilities::{CAP_QUERY_FLAGS, CAP_QUERY_TIMEOUT};
|
|
3586
|
+
|
|
3587
|
+
for caps in [0, CAP_QUERY_FLAGS, CAP_QUERY_TIMEOUT] {
|
|
3588
|
+
let err = require_query_timeout(caps, 1_000).expect_err("must refuse");
|
|
3589
|
+
assert_eq!(
|
|
3590
|
+
err.code(),
|
|
3591
|
+
ErrorCode::UnsupportedServer,
|
|
3592
|
+
"caps 0x{caps:08X}"
|
|
3593
|
+
);
|
|
3594
|
+
assert!(err.msg().contains("CAP_QUERY_TIMEOUT"), "{}", err.msg());
|
|
3595
|
+
assert!(err.msg().contains("query.timeout"), "{}", err.msg());
|
|
3596
|
+
}
|
|
3597
|
+
assert!(require_query_timeout(CAP_QUERY_FLAGS | CAP_QUERY_TIMEOUT, 1_000).is_ok());
|
|
3598
|
+
assert!(require_query_timeout(0, 0).is_ok());
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
/// A timeout error must never be in-doubt-free silence: a write that
|
|
3602
|
+
/// timed out may still land, so the code has to be distinguishable
|
|
3603
|
+
/// from a cancel and from a server limit.
|
|
3604
|
+
#[test]
|
|
3605
|
+
fn query_timeout_is_its_own_code() {
|
|
3606
|
+
use crate::egress::wire::msg_kind::StatusCode as S;
|
|
3607
|
+
let timeout = map_server_status(S::QueryTimeout, "too slow".to_string());
|
|
3608
|
+
assert_eq!(timeout.code(), ErrorCode::QueryTimeout);
|
|
3609
|
+
assert_ne!(timeout.code(), ErrorCode::Cancelled);
|
|
3610
|
+
assert_ne!(timeout.code(), ErrorCode::ServerLimitExceeded);
|
|
3611
|
+
assert_eq!(timeout.msg(), "too slow");
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3370
3614
|
/// Exhaustively pin `is_failover_eligible` against every
|
|
3371
3615
|
/// `ErrorCode` variant. The function is a single `matches!` arm
|
|
3372
3616
|
/// today; this guards against (a) silently dropping an arm
|
|
@@ -3414,6 +3658,11 @@ mod tests {
|
|
|
3414
3658
|
LimitExceeded,
|
|
3415
3659
|
ServerLimitExceeded,
|
|
3416
3660
|
Cancelled,
|
|
3661
|
+
// A timeout is the server having done exactly what it was
|
|
3662
|
+
// told. Replaying it on another endpoint would re-run the
|
|
3663
|
+
// work under the same budget and, for a write, risk applying
|
|
3664
|
+
// a statement the first attempt may already have applied.
|
|
3665
|
+
QueryTimeout,
|
|
3417
3666
|
] {
|
|
3418
3667
|
assert!(
|
|
3419
3668
|
!is_failover_eligible(code),
|
|
@@ -3441,6 +3690,7 @@ mod tests {
|
|
|
3441
3690
|
(S::SecurityError, C::ServerSecurityError),
|
|
3442
3691
|
(S::Cancelled, C::Cancelled),
|
|
3443
3692
|
(S::LimitExceeded, C::ServerLimitExceeded),
|
|
3693
|
+
(S::QueryTimeout, C::QueryTimeout),
|
|
3444
3694
|
];
|
|
3445
3695
|
|
|
3446
3696
|
for (status, expected_code) in cases {
|