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
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
require "minitest/autorun"
|
|
24
|
+
require "json"
|
|
25
|
+
require "open3"
|
|
26
|
+
require "rbconfig"
|
|
27
|
+
require "tmpdir"
|
|
28
|
+
|
|
29
|
+
# Generates a real Makefile with mkmf under each build Ruby's link settings
|
|
30
|
+
# and checks the expanded link flags. The macOS values are copied from the
|
|
31
|
+
# rbconfig.rb of the ruby/ruby-builder Rubies the release pipeline builds
|
|
32
|
+
# with; 0.1.0's macOS gems linked libruby through them.
|
|
33
|
+
#
|
|
34
|
+
# The scenarios are hermetic: each sets every RbConfig key that reaches an
|
|
35
|
+
# extension's link line, so the host Ruby cannot change the result. The test
|
|
36
|
+
# still runs only on Linux hosts. Its subject is the fake configurations, so
|
|
37
|
+
# other hosts add no coverage, and the release pipeline checks the real macOS
|
|
38
|
+
# and Windows extensions with script/check-native-linkage.
|
|
39
|
+
class ExtconfLinkFlagsTest < Minitest::Test
|
|
40
|
+
HELPER = File.expand_path("../ext/questdb_ingress/unlink_build_ruby.rb", __dir__)
|
|
41
|
+
|
|
42
|
+
# Every key mkmf's init_mkmf and the Makefile's DLDFLAGS/LIBS read.
|
|
43
|
+
NEUTRAL = {
|
|
44
|
+
"LDFLAGS" => "", "DLDFLAGS" => "", "ADDITIONAL_DLDFLAGS" => "", "EXTDLDFLAGS" => "",
|
|
45
|
+
"ARCH_FLAG" => "", "LIBS" => "", "DLDLIBS" => "", "ENABLE_SHARED" => "yes", "RUBY_SO_NAME" => "ruby",
|
|
46
|
+
"LIBRUBYARG" => "$(LIBRUBYARG_SHARED)", "LIBRUBYARG_SHARED" => "-l$(RUBY_SO_NAME)",
|
|
47
|
+
"LIBRUBYARG_STATIC" => "-l$(RUBY_SO_NAME)-static"
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
DARWIN_SHARED_31 = NEUTRAL.merge(
|
|
51
|
+
"RUBY_SO_NAME" => "ruby.3.1",
|
|
52
|
+
"EXTDLDFLAGS" => "$(LIBRUBYARG_SHARED)",
|
|
53
|
+
"DLDFLAGS" => "-L/usr/local/opt/gmp/lib -Wl,-undefined,dynamic_lookup"
|
|
54
|
+
).freeze
|
|
55
|
+
DARWIN_STATIC_31 = NEUTRAL.merge(
|
|
56
|
+
"ENABLE_SHARED" => "no",
|
|
57
|
+
"LIBRUBYARG" => "$(LIBRUBYARG_STATIC)",
|
|
58
|
+
"LIBRUBYARG_SHARED" => "",
|
|
59
|
+
"EXTDLDFLAGS" => "-bundle_loader '$(BUILTRUBY)'",
|
|
60
|
+
"DLDFLAGS" => "-L/opt/homebrew/opt/gmp/lib -Wl,-undefined,dynamic_lookup"
|
|
61
|
+
).freeze
|
|
62
|
+
DARWIN_SHARED_34 = NEUTRAL.merge(
|
|
63
|
+
"RUBY_SO_NAME" => "ruby.3.4",
|
|
64
|
+
"DLDFLAGS" => "-Wl,-multiply_defined,suppress"
|
|
65
|
+
).freeze
|
|
66
|
+
LINUX = NEUTRAL
|
|
67
|
+
WINDOWS = NEUTRAL.merge("RUBY_SO_NAME" => "x64-ucrt-ruby340").freeze
|
|
68
|
+
|
|
69
|
+
# Simulated host Rubies with noisy values for every key, applied before the
|
|
70
|
+
# scenario: a hermetic scenario gives the same flags under each of them.
|
|
71
|
+
HOSTS = {
|
|
72
|
+
"linux" => { "LDFLAGS" => "-L. -fstack-protector-strong -rdynamic -Wl,-export-dynamic",
|
|
73
|
+
"DLDFLAGS" => "-Wl,-z,relro", "LIBS" => "-lm -lpthread", "RUBY_SO_NAME" => "ruby" },
|
|
74
|
+
"macos-shared" => { "DLDFLAGS" => "-L/usr/local/opt/gmp/lib -Wl,-undefined,dynamic_lookup",
|
|
75
|
+
"EXTDLDFLAGS" => "$(LIBRUBYARG_SHARED)", "RUBY_SO_NAME" => "ruby.3.2",
|
|
76
|
+
"LDFLAGS" => "-L/usr/local/lib -Wl,-undefined,dynamic_lookup", "LIBS" => "-lpthread" },
|
|
77
|
+
"macos-static" => { "ENABLE_SHARED" => "no", "LIBRUBYARG" => "$(LIBRUBYARG_STATIC)", "LIBRUBYARG_SHARED" => "",
|
|
78
|
+
"EXTDLDFLAGS" => "-bundle_loader '$(BUILTRUBY)'", "ADDITIONAL_DLDFLAGS" => "-Wl,-x" },
|
|
79
|
+
"windows" => { "RUBY_SO_NAME" => "x64-ucrt-ruby340", "LIBRUBYARG_SHARED" => "-lx64-ucrt-ruby340",
|
|
80
|
+
"DLDFLAGS" => "-Wl,--enable-auto-image-base", "LIBS" => "-lshell32 -lws2_32",
|
|
81
|
+
"DLDLIBS" => "-lx64-ucrt-ruby340", "ARCH_FLAG" => "-m64" }
|
|
82
|
+
}.freeze
|
|
83
|
+
|
|
84
|
+
PROBE = <<~'RUBY'
|
|
85
|
+
require "mkmf"
|
|
86
|
+
require "json"
|
|
87
|
+
[ENV.fetch("HOST_RBCONFIG"), ENV.fetch("FAKE_RBCONFIG")].map { |json| JSON.parse(json) }.reduce(:merge).each do |key, value|
|
|
88
|
+
RbConfig::MAKEFILE_CONFIG[key] = value
|
|
89
|
+
RbConfig::CONFIG[key] = value
|
|
90
|
+
end
|
|
91
|
+
init_mkmf(RbConfig::MAKEFILE_CONFIG, RbConfig::CONFIG)
|
|
92
|
+
if ENV["APPLY_HELPER"] == "1"
|
|
93
|
+
require ENV.fetch("HELPER")
|
|
94
|
+
QuestDBExtconf.unlink_build_ruby!(ENV.fetch("FAKE_HOST_OS"))
|
|
95
|
+
end
|
|
96
|
+
create_makefile("probe")
|
|
97
|
+
RUBY
|
|
98
|
+
|
|
99
|
+
def setup
|
|
100
|
+
skip "probes fake build Ruby configurations; runs on Linux hosts only" unless RUBY_PLATFORM.include?("linux")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def link_flags(config, host_os, apply:, host: {})
|
|
104
|
+
Dir.mktmpdir do |dir|
|
|
105
|
+
File.write(File.join(dir, "probe.c"), "void Init_probe(void) {}\n")
|
|
106
|
+
File.write(File.join(dir, "print.mk"), "print:\n\t@echo $(DLDFLAGS) $(LIBS)\n")
|
|
107
|
+
env = { "FAKE_RBCONFIG" => JSON.generate(config), "HOST_RBCONFIG" => JSON.generate(host),
|
|
108
|
+
"FAKE_HOST_OS" => host_os, "HELPER" => HELPER, "APPLY_HELPER" => apply ? "1" : "0" }
|
|
109
|
+
out, status = Open3.capture2e(env, RbConfig.ruby, "-e", PROBE, chdir: dir)
|
|
110
|
+
assert status.success?, out
|
|
111
|
+
flags, status = Open3.capture2e("make", "-s", "-f", "Makefile", "-f", "print.mk", "print", chdir: dir)
|
|
112
|
+
assert status.success?, flags
|
|
113
|
+
flags.strip
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Runs the block once per simulated host and checks the result does not
|
|
118
|
+
# depend on the host.
|
|
119
|
+
def each_host(config, host_os, apply:)
|
|
120
|
+
results = HOSTS.to_h { |name, host| [name, link_flags(config, host_os, apply: apply, host: host)] }
|
|
121
|
+
assert_equal 1, results.values.uniq.size, "flags depend on the host Ruby: #{results.inspect}"
|
|
122
|
+
results.values.first
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def assert_unlinked(flags)
|
|
126
|
+
refute_match(/-lruby|libruby|bundle_loader|RUBY_SO_NAME|BUILTRUBY/, flags)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def test_macos_shared_ruby_31_does_not_link_libruby
|
|
130
|
+
assert_match(/-lruby\.3\.1/, each_host(DARWIN_SHARED_31, "darwin22", apply: false), "probe must see libruby")
|
|
131
|
+
flags = each_host(DARWIN_SHARED_31, "darwin22", apply: true)
|
|
132
|
+
assert_unlinked(flags)
|
|
133
|
+
assert_match(/-undefined,dynamic_lookup/, flags)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_macos_static_ruby_31_does_not_bind_to_the_executable
|
|
137
|
+
assert_match(/bundle_loader/, each_host(DARWIN_STATIC_31, "darwin23", apply: false), "probe must see -bundle_loader")
|
|
138
|
+
flags = each_host(DARWIN_STATIC_31, "darwin23", apply: true)
|
|
139
|
+
assert_unlinked(flags)
|
|
140
|
+
assert_match(/-undefined,dynamic_lookup/, flags)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_macos_shared_ruby_34_does_not_link_libruby
|
|
144
|
+
assert_match(/-lruby\.3\.4/, each_host(DARWIN_SHARED_34, "darwin24", apply: false), "probe must see libruby")
|
|
145
|
+
flags = each_host(DARWIN_SHARED_34, "darwin24", apply: true)
|
|
146
|
+
assert_unlinked(flags)
|
|
147
|
+
assert_match(/-undefined,dynamic_lookup/, flags)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_linux_does_not_link_libruby
|
|
151
|
+
assert_match(/-lruby/, each_host(LINUX, "linux-gnu", apply: false), "probe must see libruby")
|
|
152
|
+
flags = each_host(LINUX, "linux-gnu", apply: true)
|
|
153
|
+
assert_unlinked(flags)
|
|
154
|
+
refute_match(/dynamic_lookup/, flags)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_windows_keeps_the_ruby_dll
|
|
158
|
+
assert_match(/-lx64-ucrt-ruby340/, each_host(WINDOWS, "mingw32", apply: true))
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -67,8 +67,12 @@ inline constexpr uint8_t ROLE_PRIMARY = 0x01;
|
|
|
67
67
|
inline constexpr uint8_t ROLE_REPLICA = 0x02;
|
|
68
68
|
inline constexpr uint8_t ROLE_PRIMARY_CATCHUP = 0x03;
|
|
69
69
|
|
|
70
|
-
// SERVER_INFO capability bits
|
|
70
|
+
// SERVER_INFO capability bits (mirror
|
|
71
|
+
// questdb-rs/src/egress/wire/capabilities.rs).
|
|
71
72
|
inline constexpr uint32_t CAP_ZONE = 0x00000001u;
|
|
73
|
+
inline constexpr uint32_t CAP_QUERY_FLAGS = 0x00000002u;
|
|
74
|
+
inline constexpr uint32_t CAP_COMPRESSION = 0x00000004u;
|
|
75
|
+
inline constexpr uint32_t CAP_QUERY_TIMEOUT = 0x00000008u;
|
|
72
76
|
|
|
73
77
|
// ColumnKind wire codes (mirror questdb-rs/src/egress/column_kind.rs).
|
|
74
78
|
inline constexpr uint8_t COL_BOOLEAN = 0x01;
|
|
@@ -102,6 +106,11 @@ inline constexpr uint8_t STATUS_INTERNAL_ERROR = 0x06;
|
|
|
102
106
|
inline constexpr uint8_t STATUS_SECURITY_ERROR = 0x08;
|
|
103
107
|
inline constexpr uint8_t STATUS_CANCELLED = 0x0A;
|
|
104
108
|
inline constexpr uint8_t STATUS_LIMIT_EXCEEDED = 0x0B;
|
|
109
|
+
inline constexpr uint8_t STATUS_QUERY_TIMEOUT = 0x0E;
|
|
110
|
+
|
|
111
|
+
// QUERY_REQUEST `query_flags` bits (mirror query_request.rs).
|
|
112
|
+
inline constexpr uint8_t QUERY_FLAG_RESET_DICT = 0x01;
|
|
113
|
+
inline constexpr uint8_t QUERY_FLAG_TIMEOUT = 0x02;
|
|
105
114
|
|
|
106
115
|
// CACHE_RESET masks.
|
|
107
116
|
inline constexpr uint8_t CACHE_RESET_SYMBOLS = 0x01;
|
|
@@ -117,6 +117,43 @@ TEST(test_symbol_dict_full_is_a_distinct_appended_code)
|
|
|
117
117
|
"neutral alias resolves to the same enumerator");
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
TEST(test_query_timeout_is_a_distinct_appended_code)
|
|
121
|
+
{
|
|
122
|
+
/* A query that ran past its own timeout must be recognisable by code:
|
|
123
|
+
* callers branch on it to avoid retrying a write the server may have
|
|
124
|
+
* applied anyway. */
|
|
125
|
+
CHECK(
|
|
126
|
+
line_sender_error_query_timeout != line_sender_error_cancelled,
|
|
127
|
+
"query_timeout distinct from cancelled");
|
|
128
|
+
CHECK(
|
|
129
|
+
line_sender_error_query_timeout !=
|
|
130
|
+
line_sender_error_server_limit_exceeded,
|
|
131
|
+
"query_timeout distinct from server_limit_exceeded");
|
|
132
|
+
CHECK(
|
|
133
|
+
line_sender_error_query_timeout != line_sender_error_invalid_api_call,
|
|
134
|
+
"query_timeout distinct from invalid_api_call");
|
|
135
|
+
CHECK(
|
|
136
|
+
line_sender_error_query_timeout > line_sender_error_symbol_dict_full,
|
|
137
|
+
"query_timeout appended (not renumbered)");
|
|
138
|
+
CHECK(
|
|
139
|
+
line_sender_error_query_timeout == 38,
|
|
140
|
+
"query_timeout discriminant pinned at 38 for cached ABI consumers");
|
|
141
|
+
CHECK(
|
|
142
|
+
questdb_error_query_timeout == line_sender_error_query_timeout,
|
|
143
|
+
"neutral alias resolves to the same enumerator");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
TEST(test_query_set_timeout_ms_null_query_is_tolerated)
|
|
147
|
+
{
|
|
148
|
+
/* Proves the symbol links and argument-marshals under a plain C
|
|
149
|
+
* compiler (the Cython contract surface), and that the NULL-handle
|
|
150
|
+
* contract shared by every `qwp_reader_query_*` mutator holds: report
|
|
151
|
+
* and return, never crash. */
|
|
152
|
+
qwp_reader_query_set_timeout_ms(NULL, 1000);
|
|
153
|
+
qwp_reader_query_set_timeout_ms(NULL, 0);
|
|
154
|
+
CHECK(true, "NULL query handle did not crash");
|
|
155
|
+
}
|
|
156
|
+
|
|
120
157
|
TEST(test_egress_null_cursor_returns_error_tristate)
|
|
121
158
|
{
|
|
122
159
|
struct ArrowArray arr;
|
|
@@ -1609,6 +1646,8 @@ int main(void)
|
|
|
1609
1646
|
RUN(test_appended_query_error_codes_have_distinct_values);
|
|
1610
1647
|
RUN(test_appended_sender_error_codes_exist);
|
|
1611
1648
|
RUN(test_symbol_dict_full_is_a_distinct_appended_code);
|
|
1649
|
+
RUN(test_query_timeout_is_a_distinct_appended_code);
|
|
1650
|
+
RUN(test_query_set_timeout_ms_null_query_is_tolerated);
|
|
1612
1651
|
RUN(test_egress_null_cursor_returns_error_tristate);
|
|
1613
1652
|
RUN(test_egress_null_out_array_returns_error_tristate);
|
|
1614
1653
|
RUN(test_ingress_null_conn_returns_false);
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
#include <chrono>
|
|
39
39
|
#include <cstring>
|
|
40
40
|
#include <exception>
|
|
41
|
+
#include <limits>
|
|
41
42
|
#include <memory>
|
|
42
43
|
#include <mutex>
|
|
43
44
|
#include <optional>
|
|
@@ -675,6 +676,433 @@ TEST_CASE("mock: QueryError(cancelled) surfaces as Cancelled")
|
|
|
675
676
|
// and the server's CANCELLED status surfaces as the documented error code.
|
|
676
677
|
// ---------------------------------------------------------------------------
|
|
677
678
|
|
|
679
|
+
TEST_CASE("mock: QueryError(query_timeout) surfaces as QueryTimeout")
|
|
680
|
+
{
|
|
681
|
+
run_query_error_test(qm::STATUS_QUERY_TIMEOUT, questdb_error_query_timeout);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ---------------------------------------------------------------------------
|
|
685
|
+
// Per-query timeout (CAP_QUERY_TIMEOUT / QUERY_FLAG_TIMEOUT / timeout_ms).
|
|
686
|
+
// ---------------------------------------------------------------------------
|
|
687
|
+
|
|
688
|
+
namespace
|
|
689
|
+
{
|
|
690
|
+
// A SERVER_INFO advertising the capabilities a current server sends.
|
|
691
|
+
qm::ActionSendServerInfo server_info_with(uint32_t caps)
|
|
692
|
+
{
|
|
693
|
+
qm::ActionSendServerInfo si;
|
|
694
|
+
si.capabilities = caps;
|
|
695
|
+
return si;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Offset of the `query_flags` trailer in a QUERY_REQUEST with no binds:
|
|
699
|
+
// msg_kind(1) + request_id(8) + varint sql_len(1) + sql + varint credit(1)
|
|
700
|
+
// + varint bind_count(1).
|
|
701
|
+
size_t trailer_offset(size_t sql_len)
|
|
702
|
+
{
|
|
703
|
+
return 1 + 8 + 1 + sql_len + 1 + 1;
|
|
704
|
+
}
|
|
705
|
+
} // namespace
|
|
706
|
+
|
|
707
|
+
TEST_CASE("mock: no timeout -> QUERY_REQUEST carries no trailer")
|
|
708
|
+
{
|
|
709
|
+
// The baseline an older server has always seen. Byte-exactness here is
|
|
710
|
+
// what makes the feature safe to ship against one.
|
|
711
|
+
qm::Script s = {
|
|
712
|
+
server_info_with(qm::CAP_QUERY_FLAGS | qm::CAP_QUERY_TIMEOUT),
|
|
713
|
+
qm::ActionAwaitQueryRequest{},
|
|
714
|
+
qm::ActionSendResultEnd{},
|
|
715
|
+
};
|
|
716
|
+
qm::MockServer srv({s});
|
|
717
|
+
auto reader = connect_to(srv);
|
|
718
|
+
auto cur = reader.execute("X"_utf8);
|
|
719
|
+
while (cur.next_batch())
|
|
720
|
+
{
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
auto reqs = srv.captured_requests();
|
|
724
|
+
REQUIRE(reqs.size() == 1);
|
|
725
|
+
CHECK(reqs[0].size() == trailer_offset(1));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
TEST_CASE("mock: timeout_ms rides the wire after query_flags")
|
|
729
|
+
{
|
|
730
|
+
qm::Script s = {
|
|
731
|
+
server_info_with(qm::CAP_QUERY_FLAGS | qm::CAP_QUERY_TIMEOUT),
|
|
732
|
+
qm::ActionAwaitQueryRequest{},
|
|
733
|
+
qm::ActionSendResultEnd{},
|
|
734
|
+
};
|
|
735
|
+
qm::MockServer srv({s});
|
|
736
|
+
auto reader = connect_to(srv);
|
|
737
|
+
auto cur = reader.prepare("X"_utf8).timeout_ms(1000).execute();
|
|
738
|
+
while (cur.next_batch())
|
|
739
|
+
{
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
auto reqs = srv.captured_requests();
|
|
743
|
+
REQUIRE(reqs.size() == 1);
|
|
744
|
+
const auto& req = reqs[0];
|
|
745
|
+
const size_t p = trailer_offset(1);
|
|
746
|
+
// flags varint = QUERY_FLAG_TIMEOUT, then timeout_ms varint
|
|
747
|
+
// (1000 = 0xE8 0x07).
|
|
748
|
+
REQUIRE(req.size() == p + 3);
|
|
749
|
+
CHECK(req[p] == qm::QUERY_FLAG_TIMEOUT);
|
|
750
|
+
CHECK(req[p + 1] == 0xE8);
|
|
751
|
+
CHECK(req[p + 2] == 0x07);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
TEST_CASE("mock: chrono timeout rounds a sub-millisecond value up to 1 ms")
|
|
755
|
+
{
|
|
756
|
+
// Rounding down would mean "no timeout" — the opposite of the ask.
|
|
757
|
+
qm::Script s = {
|
|
758
|
+
server_info_with(qm::CAP_QUERY_FLAGS | qm::CAP_QUERY_TIMEOUT),
|
|
759
|
+
qm::ActionAwaitQueryRequest{},
|
|
760
|
+
qm::ActionSendResultEnd{},
|
|
761
|
+
};
|
|
762
|
+
qm::MockServer srv({s});
|
|
763
|
+
auto reader = connect_to(srv);
|
|
764
|
+
auto cur = reader.prepare("X"_utf8)
|
|
765
|
+
.timeout(std::chrono::microseconds{500})
|
|
766
|
+
.execute();
|
|
767
|
+
while (cur.next_batch())
|
|
768
|
+
{
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
auto reqs = srv.captured_requests();
|
|
772
|
+
REQUIRE(reqs.size() == 1);
|
|
773
|
+
const auto& req = reqs[0];
|
|
774
|
+
const size_t p = trailer_offset(1);
|
|
775
|
+
REQUIRE(req.size() == p + 2);
|
|
776
|
+
CHECK(req[p] == qm::QUERY_FLAG_TIMEOUT);
|
|
777
|
+
CHECK(req[p + 1] == 1);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
TEST_CASE("mock: chrono timeout converts seconds to milliseconds")
|
|
781
|
+
{
|
|
782
|
+
qm::Script s = {
|
|
783
|
+
server_info_with(qm::CAP_QUERY_FLAGS | qm::CAP_QUERY_TIMEOUT),
|
|
784
|
+
qm::ActionAwaitQueryRequest{},
|
|
785
|
+
qm::ActionSendResultEnd{},
|
|
786
|
+
};
|
|
787
|
+
qm::MockServer srv({s});
|
|
788
|
+
auto reader = connect_to(srv);
|
|
789
|
+
auto cur =
|
|
790
|
+
reader.prepare("X"_utf8).timeout(std::chrono::seconds{2}).execute();
|
|
791
|
+
while (cur.next_batch())
|
|
792
|
+
{
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
auto reqs = srv.captured_requests();
|
|
796
|
+
REQUIRE(reqs.size() == 1);
|
|
797
|
+
const auto& req = reqs[0];
|
|
798
|
+
const size_t p = trailer_offset(1);
|
|
799
|
+
// timeout_ms varint 2000 = 0xD0 0x0F.
|
|
800
|
+
REQUIRE(req.size() == p + 3);
|
|
801
|
+
CHECK(req[p] == qm::QUERY_FLAG_TIMEOUT);
|
|
802
|
+
CHECK(req[p + 1] == 0xD0);
|
|
803
|
+
CHECK(req[p + 2] == 0x0F);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
TEST_CASE("mock: chrono timeout rejects negative and non-finite durations")
|
|
807
|
+
{
|
|
808
|
+
qm::Script s = {
|
|
809
|
+
server_info_with(qm::CAP_QUERY_FLAGS | qm::CAP_QUERY_TIMEOUT),
|
|
810
|
+
};
|
|
811
|
+
qm::MockServer srv({s});
|
|
812
|
+
auto reader = connect_to(srv);
|
|
813
|
+
using ms_f = std::chrono::duration<double, std::milli>;
|
|
814
|
+
for (double v :
|
|
815
|
+
{-1.0,
|
|
816
|
+
std::numeric_limits<double>::quiet_NaN(),
|
|
817
|
+
std::numeric_limits<double>::infinity()})
|
|
818
|
+
{
|
|
819
|
+
auto q = reader.prepare("X"_utf8);
|
|
820
|
+
try
|
|
821
|
+
{
|
|
822
|
+
q.timeout(ms_f{v});
|
|
823
|
+
FAIL("negative or non-finite timeout must throw");
|
|
824
|
+
}
|
|
825
|
+
catch (const questdb::error& e)
|
|
826
|
+
{
|
|
827
|
+
CHECK(e.code() == questdb_error_invalid_api_call);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
TEST_CASE("mock: chrono timeout saturates instead of overflowing")
|
|
833
|
+
{
|
|
834
|
+
qm::Script s = {
|
|
835
|
+
server_info_with(qm::CAP_QUERY_FLAGS | qm::CAP_QUERY_TIMEOUT),
|
|
836
|
+
qm::ActionAwaitQueryRequest{},
|
|
837
|
+
qm::ActionSendResultEnd{},
|
|
838
|
+
};
|
|
839
|
+
qm::MockServer srv({s});
|
|
840
|
+
auto reader = connect_to(srv);
|
|
841
|
+
using hours64 = std::chrono::duration<int64_t, std::ratio<3600>>;
|
|
842
|
+
auto cur = reader.prepare("X"_utf8).timeout(hours64::max()).execute();
|
|
843
|
+
while (cur.next_batch())
|
|
844
|
+
{
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
auto reqs = srv.captured_requests();
|
|
848
|
+
REQUIRE(reqs.size() == 1);
|
|
849
|
+
const auto& req = reqs[0];
|
|
850
|
+
const size_t p = trailer_offset(1);
|
|
851
|
+
// timeout_ms = INT64_MAX (the server reads a signed long): eight 0xFF
|
|
852
|
+
// continuation bytes, then 0x7F.
|
|
853
|
+
REQUIRE(req.size() == p + 10);
|
|
854
|
+
CHECK(req[p] == qm::QUERY_FLAG_TIMEOUT);
|
|
855
|
+
for (size_t i = 1; i < 9; ++i)
|
|
856
|
+
CHECK(req[p + i] == 0xFF);
|
|
857
|
+
CHECK(req[p + 9] == 0x7F);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
TEST_CASE("mock: chrono zero timeout clears the request")
|
|
861
|
+
{
|
|
862
|
+
qm::Script s = {
|
|
863
|
+
server_info_with(qm::CAP_QUERY_FLAGS | qm::CAP_QUERY_TIMEOUT),
|
|
864
|
+
qm::ActionAwaitQueryRequest{},
|
|
865
|
+
qm::ActionSendResultEnd{},
|
|
866
|
+
};
|
|
867
|
+
qm::MockServer srv({s});
|
|
868
|
+
auto reader = connect_to(srv);
|
|
869
|
+
auto cur =
|
|
870
|
+
reader.prepare("X"_utf8).timeout(std::chrono::seconds{0}).execute();
|
|
871
|
+
while (cur.next_batch())
|
|
872
|
+
{
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
auto reqs = srv.captured_requests();
|
|
876
|
+
REQUIRE(reqs.size() == 1);
|
|
877
|
+
CHECK(reqs[0].size() == trailer_offset(1));
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
TEST_CASE("mock: connect-string query_timeout_ms applies to every query")
|
|
881
|
+
{
|
|
882
|
+
qm::Script s = {
|
|
883
|
+
server_info_with(qm::CAP_QUERY_FLAGS | qm::CAP_QUERY_TIMEOUT),
|
|
884
|
+
qm::ActionAwaitQueryRequest{},
|
|
885
|
+
qm::ActionSendResultEnd{},
|
|
886
|
+
qm::ActionAwaitQueryRequest{},
|
|
887
|
+
qm::ActionSendResultEnd{},
|
|
888
|
+
};
|
|
889
|
+
qm::MockServer srv({s});
|
|
890
|
+
const std::string conf =
|
|
891
|
+
"ws::addr=" + srv.addr() + ";query_timeout_ms=1000;";
|
|
892
|
+
eg::reader reader{questdb::ingress::utf8_view{conf}};
|
|
893
|
+
|
|
894
|
+
for (int i = 0; i < 2; ++i)
|
|
895
|
+
{
|
|
896
|
+
auto cur = reader.execute("X"_utf8);
|
|
897
|
+
while (cur.next_batch())
|
|
898
|
+
{
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
auto reqs = srv.captured_requests();
|
|
903
|
+
REQUIRE(reqs.size() == 2);
|
|
904
|
+
for (const auto& req : reqs)
|
|
905
|
+
{
|
|
906
|
+
const size_t p = trailer_offset(1);
|
|
907
|
+
REQUIRE(req.size() == p + 3);
|
|
908
|
+
CHECK(req[p] == qm::QUERY_FLAG_TIMEOUT);
|
|
909
|
+
CHECK(req[p + 1] == 0xE8);
|
|
910
|
+
CHECK(req[p + 2] == 0x07);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
TEST_CASE("mock: per-query timeout overrides the connect-string default")
|
|
915
|
+
{
|
|
916
|
+
qm::Script s = {
|
|
917
|
+
server_info_with(qm::CAP_QUERY_FLAGS | qm::CAP_QUERY_TIMEOUT),
|
|
918
|
+
qm::ActionAwaitQueryRequest{},
|
|
919
|
+
qm::ActionSendResultEnd{},
|
|
920
|
+
};
|
|
921
|
+
qm::MockServer srv({s});
|
|
922
|
+
const std::string conf =
|
|
923
|
+
"ws::addr=" + srv.addr() + ";query_timeout_ms=1000;";
|
|
924
|
+
eg::reader reader{questdb::ingress::utf8_view{conf}};
|
|
925
|
+
// Clearing it per query must also work, or a caller stuck with a
|
|
926
|
+
// profile-wide default could never opt a single query out.
|
|
927
|
+
auto cur = reader.prepare("X"_utf8).timeout_ms(0).execute();
|
|
928
|
+
while (cur.next_batch())
|
|
929
|
+
{
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
auto reqs = srv.captured_requests();
|
|
933
|
+
REQUIRE(reqs.size() == 1);
|
|
934
|
+
CHECK(reqs[0].size() == trailer_offset(1));
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
TEST_CASE("mock: failover replay re-sends the original timeout budget")
|
|
938
|
+
{
|
|
939
|
+
// The replay patches only the request_id, so the new endpoint gets the
|
|
940
|
+
// full timeout again. Pinned so a change to that is deliberate.
|
|
941
|
+
qm::Script s_a = {
|
|
942
|
+
qm::ActionSendServerInfo{
|
|
943
|
+
qm::ROLE_STANDALONE,
|
|
944
|
+
"c",
|
|
945
|
+
"a",
|
|
946
|
+
0,
|
|
947
|
+
qm::CAP_QUERY_FLAGS | qm::CAP_QUERY_TIMEOUT},
|
|
948
|
+
qm::ActionAwaitQueryRequest{},
|
|
949
|
+
qm::ActionHardDrop{},
|
|
950
|
+
};
|
|
951
|
+
qm::Script s_b = {
|
|
952
|
+
qm::ActionSendServerInfo{
|
|
953
|
+
qm::ROLE_STANDALONE,
|
|
954
|
+
"c",
|
|
955
|
+
"b",
|
|
956
|
+
0,
|
|
957
|
+
qm::CAP_QUERY_FLAGS | qm::CAP_QUERY_TIMEOUT},
|
|
958
|
+
qm::ActionAwaitQueryRequest{},
|
|
959
|
+
qm::ActionSendResultEnd{},
|
|
960
|
+
};
|
|
961
|
+
qm::MockServer srv_a({s_a});
|
|
962
|
+
qm::MockServer srv_b({s_b});
|
|
963
|
+
|
|
964
|
+
const std::string conf =
|
|
965
|
+
"ws::addr=" + srv_a.addr() + "," + srv_b.addr() +
|
|
966
|
+
";failover_backoff_initial_ms=1;failover_backoff_max_ms=10";
|
|
967
|
+
eg::reader reader{questdb::ingress::utf8_view{conf}};
|
|
968
|
+
|
|
969
|
+
auto cur = reader.prepare("X"_utf8).timeout_ms(1000).execute();
|
|
970
|
+
while (cur.next_batch())
|
|
971
|
+
{
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Each endpoint saw one request, and both carried the same budget.
|
|
975
|
+
auto reqs_a = srv_a.captured_requests();
|
|
976
|
+
auto reqs_b = srv_b.captured_requests();
|
|
977
|
+
REQUIRE(reqs_a.size() == 1);
|
|
978
|
+
REQUIRE(reqs_b.size() == 1);
|
|
979
|
+
const size_t p = trailer_offset(1);
|
|
980
|
+
for (const auto* reqs : {&reqs_a, &reqs_b})
|
|
981
|
+
{
|
|
982
|
+
const auto& req = (*reqs)[0];
|
|
983
|
+
REQUIRE(req.size() == p + 3);
|
|
984
|
+
CHECK(req[p] == qm::QUERY_FLAG_TIMEOUT);
|
|
985
|
+
CHECK(req[p + 1] == 0xE8); // varint(1000), unchanged by the replay
|
|
986
|
+
CHECK(req[p + 2] == 0x07);
|
|
987
|
+
}
|
|
988
|
+
// The replay differs from the original only in the request_id, which is
|
|
989
|
+
// what makes the trailer survive byte-identically.
|
|
990
|
+
const auto& orig = reqs_a[0];
|
|
991
|
+
const auto& replay = reqs_b[0];
|
|
992
|
+
CHECK(orig.size() == replay.size());
|
|
993
|
+
CHECK(std::equal(orig.begin() + 9, orig.end(), replay.begin() + 9));
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
TEST_CASE("mock: timeout without CAP_QUERY_TIMEOUT fails before the wire")
|
|
997
|
+
{
|
|
998
|
+
// Fail fast rather than degrade: running under the server default
|
|
999
|
+
// while the caller believes a 10-minute budget applied is how a dbt
|
|
1000
|
+
// transform dies at 60 s with a confusing error.
|
|
1001
|
+
qm::Script s = {
|
|
1002
|
+
server_info_with(qm::CAP_QUERY_FLAGS),
|
|
1003
|
+
qm::ActionAwaitQueryRequest{},
|
|
1004
|
+
qm::ActionSendResultEnd{},
|
|
1005
|
+
};
|
|
1006
|
+
qm::MockServer srv({s});
|
|
1007
|
+
auto reader = connect_to(srv);
|
|
1008
|
+
|
|
1009
|
+
bool threw = false;
|
|
1010
|
+
try
|
|
1011
|
+
{
|
|
1012
|
+
auto cur = reader.prepare("X"_utf8).timeout_ms(1000).execute();
|
|
1013
|
+
while (cur.next_batch())
|
|
1014
|
+
{
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
catch (const questdb::error& e)
|
|
1018
|
+
{
|
|
1019
|
+
threw = true;
|
|
1020
|
+
CHECK(e.code() == questdb_error_unsupported_server);
|
|
1021
|
+
// The message has to name the missing capability and point at the
|
|
1022
|
+
// server-side knob, or the operator cannot act on it.
|
|
1023
|
+
const std::string msg = e.what();
|
|
1024
|
+
CHECK(msg.find("CAP_QUERY_TIMEOUT") != std::string::npos);
|
|
1025
|
+
CHECK(msg.find("query.timeout") != std::string::npos);
|
|
1026
|
+
}
|
|
1027
|
+
CHECK(threw);
|
|
1028
|
+
// Nothing reached the wire: the guard runs before the QUERY_REQUEST is
|
|
1029
|
+
// encoded, so the server never sees a frame it would reject.
|
|
1030
|
+
CHECK(srv.captured_requests().empty());
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
TEST_CASE("mock: timeout without any capabilities fails before the wire")
|
|
1034
|
+
{
|
|
1035
|
+
qm::Script s = {
|
|
1036
|
+
qm::ActionSendServerInfo{}, // capabilities = 0
|
|
1037
|
+
qm::ActionAwaitQueryRequest{},
|
|
1038
|
+
qm::ActionSendResultEnd{},
|
|
1039
|
+
};
|
|
1040
|
+
qm::MockServer srv({s});
|
|
1041
|
+
auto reader = connect_to(srv);
|
|
1042
|
+
|
|
1043
|
+
bool threw = false;
|
|
1044
|
+
try
|
|
1045
|
+
{
|
|
1046
|
+
auto cur = reader.prepare("X"_utf8).timeout_ms(1000).execute();
|
|
1047
|
+
while (cur.next_batch())
|
|
1048
|
+
{
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
catch (const questdb::error& e)
|
|
1052
|
+
{
|
|
1053
|
+
threw = true;
|
|
1054
|
+
CHECK(e.code() == questdb_error_unsupported_server);
|
|
1055
|
+
}
|
|
1056
|
+
CHECK(threw);
|
|
1057
|
+
CHECK(srv.captured_requests().empty());
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
TEST_CASE("mock: a timed-out query leaves the connection usable")
|
|
1061
|
+
{
|
|
1062
|
+
// The contract that makes pooling work: QUERY_TIMEOUT is per-query, so
|
|
1063
|
+
// the next query runs on the same connection with no second handshake.
|
|
1064
|
+
qm::Script s = {
|
|
1065
|
+
server_info_with(qm::CAP_QUERY_FLAGS | qm::CAP_QUERY_TIMEOUT),
|
|
1066
|
+
qm::ActionAwaitQueryRequest{},
|
|
1067
|
+
qm::ActionSendBuilt{[](int64_t rid) {
|
|
1068
|
+
return qm::query_error_frame(
|
|
1069
|
+
rid, qm::STATUS_QUERY_TIMEOUT, "timeout, query aborted");
|
|
1070
|
+
}},
|
|
1071
|
+
qm::ActionAwaitQueryRequest{},
|
|
1072
|
+
qm::ActionSendResultEnd{},
|
|
1073
|
+
};
|
|
1074
|
+
qm::MockServer srv({s});
|
|
1075
|
+
auto reader = connect_to(srv);
|
|
1076
|
+
|
|
1077
|
+
bool threw = false;
|
|
1078
|
+
try
|
|
1079
|
+
{
|
|
1080
|
+
auto cur = reader.prepare("slow"_utf8).timeout_ms(1).execute();
|
|
1081
|
+
while (cur.next_batch())
|
|
1082
|
+
{
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
catch (const questdb::error& e)
|
|
1086
|
+
{
|
|
1087
|
+
threw = true;
|
|
1088
|
+
CHECK(e.code() == questdb_error_query_timeout);
|
|
1089
|
+
}
|
|
1090
|
+
REQUIRE(threw);
|
|
1091
|
+
|
|
1092
|
+
// Second query on the same reader: it must go through.
|
|
1093
|
+
auto cur2 = reader.execute("after"_utf8);
|
|
1094
|
+
while (cur2.next_batch())
|
|
1095
|
+
{
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
auto reqs = srv.captured_requests();
|
|
1099
|
+
REQUIRE(reqs.size() == 2);
|
|
1100
|
+
// Both requests arrived on the one scripted connection, which the mock
|
|
1101
|
+
// serves per slot — so no reconnect happened between them.
|
|
1102
|
+
CHECK(reqs[0][0] == qm::MSG_QUERY_REQUEST);
|
|
1103
|
+
CHECK(reqs[1][0] == qm::MSG_QUERY_REQUEST);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
678
1106
|
TEST_CASE("mock: cursor::cancel writes MSG_CANCEL and surfaces Cancelled")
|
|
679
1107
|
{
|
|
680
1108
|
// After the QueryRequest, server holds the response open; the test
|