clickhouse-native 0.11.0 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 984a63d34de19388f777cfe71ecd0a844d22f0b67b89e9fddfe07c5c0ac1cc05
4
- data.tar.gz: 78f829e40f715aef0264e1419cb9b06b97faab1ac38f33329c5da6ada1dd251b
3
+ metadata.gz: dbc7f982fc96317d6b6721bc7162e22d01eac4f4fc9edb307443d24fdee59032
4
+ data.tar.gz: 824150b24641414bfc0ccc4a6e37aa521b768bb73423c1bda9c1bfbefc466044
5
5
  SHA512:
6
- metadata.gz: c579da78d2185108712c92e902217c2e508655533c2e78efc7e36ae358a826f809f41d884684b8fcb0e1b36749bc80be5f23b76c85ebf081423adb615d73928b
7
- data.tar.gz: 93259646a6f150ed271202e3eaca075e0d1cf8d707aed5ac2ac9b530061de47cf1a65082289718857b367dd5b8d764c73cf270c34d080018ce8caac540d8bab6
6
+ metadata.gz: 6cda8a41d58816d63aa064ae3008b20980d35be6f695ff04433bc760ec28c0a286b707bdeb725d5691ac8245533c05d90df4c9c5b3eb13af6b860a179d9349db
7
+ data.tar.gz: caf00f391e65eb920a2d917316fa05c1569b7aac7c89f83a122b149c3305a3b0b40b6dc02afa5bd1cebcd78f0897700d5a2f9fbafb2c0d327da973dd3bc542f6
@@ -872,6 +872,7 @@ struct ExecuteNoGVL {
872
872
  Client* client;
873
873
  const Query* query;
874
874
  std::exception_ptr err;
875
+ bool cancelled;
875
876
  };
876
877
  } // namespace
877
878
 
@@ -885,11 +886,16 @@ static void* execute_no_gvl(void* data) {
885
886
  return nullptr;
886
887
  }
887
888
 
889
+ // Unblock functions run on the *interrupting* thread while the blocked thread
890
+ // is still inside Client::Execute(). CancelInFlight() only shuts the socket
891
+ // down; ResetConnection() would free the very streams that thread is reading
892
+ // (Thread#kill from Parallel.in_threads, Timeout, Sidekiq shutdown), which
893
+ // reads back as a garbage packet type and then segfaults. The blocked call
894
+ // returns EOF, raises, and the pool discards the client.
888
895
  static void execute_unblock(void* data) {
889
- // The only safe abort clickhouse-cpp exposes is tearing the connection.
890
- // On interrupt we kill the socket; the pool will discard this client.
891
896
  auto* a = static_cast<ExecuteNoGVL*>(data);
892
- try { a->client->ResetConnection(); } catch (...) {}
897
+ a->cancelled = true;
898
+ try { a->client->CancelInFlight(); } catch (...) {}
893
899
  }
894
900
 
895
901
  static VALUE ch_client_execute(int argc, VALUE* argv, VALUE self) {
@@ -903,13 +909,20 @@ static VALUE ch_client_execute(int argc, VALUE* argv, VALUE self) {
903
909
  apply_default_settings(q, c);
904
910
  apply_settings(q, kwargs);
905
911
 
906
- ExecuteNoGVL args{c->client.get(), &q, nullptr};
912
+ ExecuteNoGVL args{c->client.get(), &q, nullptr, false};
907
913
  rb_thread_call_without_gvl(execute_no_gvl, &args, execute_unblock, &args);
908
914
  if (args.err) {
909
915
  // clickhouse-cpp may leave the read stream partially consumed when the
910
916
  // server exception or an unsupported-type error is thrown mid-block.
911
917
  // Reset so the next call on this Client starts from a clean protocol.
912
- try { c->client->ResetConnection(); } catch (...) {}
918
+ // Not after a cancel: that socket is already shut down and the caller
919
+ // discards the client, so the connect+handshake buys nothing. A
920
+ // Thread#kill or Timeout never gets here at all — step 5 of
921
+ // rb_thread_call_without_gvl delivers the interrupt before it returns —
922
+ // so this covers what is left: an unblock function that fires without a
923
+ // longjmp behind it, from a trap handler that does not raise or an
924
+ // interrupt Thread.handle_interrupt has deferred.
925
+ if (!args.cancelled) { try { c->client->ResetConnection(); } catch (...) {} }
913
926
  try { std::rethrow_exception(args.err); }
914
927
  catch (const std::exception& e) { raise_mapped_ex(e); }
915
928
  }
@@ -1006,6 +1019,7 @@ struct InsertNoGVL {
1006
1019
  const Block* block;
1007
1020
  const std::vector<std::pair<std::string, std::string>>* settings;
1008
1021
  std::exception_ptr err;
1022
+ bool cancelled;
1009
1023
  };
1010
1024
  } // namespace
1011
1025
 
@@ -1021,7 +1035,8 @@ static void* insert_no_gvl(void* data) {
1021
1035
 
1022
1036
  static void insert_unblock(void* data) {
1023
1037
  auto* a = static_cast<InsertNoGVL*>(data);
1024
- try { a->client->ResetConnection(); } catch (...) {}
1038
+ a->cancelled = true;
1039
+ try { a->client->CancelInFlight(); } catch (...) {}
1025
1040
  }
1026
1041
 
1027
1042
  static VALUE ch_client_insert_block(VALUE self, VALUE rb_table, VALUE rb_columns, VALUE rb_rows) {
@@ -1078,10 +1093,10 @@ static VALUE ch_client_insert_block(VALUE self, VALUE rb_table, VALUE rb_columns
1078
1093
  block.AppendColumn(names[i], cols[i]);
1079
1094
  }
1080
1095
 
1081
- InsertNoGVL args{c->client.get(), &table, &block, &c->default_settings, nullptr};
1096
+ InsertNoGVL args{c->client.get(), &table, &block, &c->default_settings, nullptr, false};
1082
1097
  rb_thread_call_without_gvl(insert_no_gvl, &args, insert_unblock, &args);
1083
1098
  if (args.err) {
1084
- try { c->client->ResetConnection(); } catch (...) {}
1099
+ if (!args.cancelled) { try { c->client->ResetConnection(); } catch (...) {} }
1085
1100
  try { std::rethrow_exception(args.err); }
1086
1101
  catch (const std::exception& e) { raise_mapped_ex(e); }
1087
1102
  }
@@ -1114,6 +1129,7 @@ struct QueryEachNoGVL {
1114
1129
  const Query* query;
1115
1130
  QueryEachState* state;
1116
1131
  std::exception_ptr err;
1132
+ bool cancelled;
1117
1133
  };
1118
1134
  } // namespace
1119
1135
 
@@ -1166,7 +1182,8 @@ static void* query_each_no_gvl(void* data) {
1166
1182
  static void query_each_unblock(void* data) {
1167
1183
  auto* a = static_cast<QueryEachNoGVL*>(data);
1168
1184
  a->state->aborted = true;
1169
- try { a->client->ResetConnection(); } catch (...) {}
1185
+ a->cancelled = true;
1186
+ try { a->client->CancelInFlight(); } catch (...) {}
1170
1187
  }
1171
1188
 
1172
1189
  static VALUE ch_client_query_each(int argc, VALUE* argv, VALUE self) {
@@ -1187,12 +1204,12 @@ static VALUE ch_client_query_each(int argc, VALUE* argv, VALUE self) {
1187
1204
  rb_thread_call_with_gvl(with_gvl_yield, &ya);
1188
1205
  return !state.aborted;
1189
1206
  });
1190
- QueryEachNoGVL args{c->client.get(), &q, &state, nullptr};
1207
+ QueryEachNoGVL args{c->client.get(), &q, &state, nullptr, false};
1191
1208
 
1192
1209
  rb_thread_call_without_gvl(query_each_no_gvl, &args, query_each_unblock, &args);
1193
1210
 
1194
1211
  if (args.err) {
1195
- try { c->client->ResetConnection(); } catch (...) {}
1212
+ if (!args.cancelled) { try { c->client->ResetConnection(); } catch (...) {} }
1196
1213
  if (state.exc_tag) rb_jump_tag(state.exc_tag);
1197
1214
  try { std::rethrow_exception(args.err); }
1198
1215
  catch (const std::exception& e) { raise_mapped_ex(e); }
@@ -0,0 +1,136 @@
1
+ diff --git a/clickhouse/base/socket.cpp b/clickhouse/base/socket.cpp
2
+ index 3bb1aa5..f188d3f 100644
3
+ --- a/clickhouse/base/socket.cpp
4
+ +++ b/clickhouse/base/socket.cpp
5
+ @@ -345,6 +345,19 @@ Socket::~Socket() {
6
+ Close();
7
+ }
8
+
9
+ +void Socket::Cancel() {
10
+ + if (handle_ == INVALID_SOCKET) {
11
+ + return;
12
+ + }
13
+ + // shutdown(), not close(): the descriptor stays valid, so a concurrent
14
+ + // reader returns EOF instead of racing a reused fd number.
15
+ +#if defined(_win_)
16
+ + shutdown(handle_, SD_BOTH);
17
+ +#else
18
+ + ::shutdown(handle_, SHUT_RDWR);
19
+ +#endif
20
+ +}
21
+ +
22
+ void Socket::Close() {
23
+ CloseSocket(handle_);
24
+ handle_ = INVALID_SOCKET;
25
+ diff --git a/clickhouse/base/socket.h b/clickhouse/base/socket.h
26
+ index 9bd9ca3..0234b1e 100644
27
+ --- a/clickhouse/base/socket.h
28
+ +++ b/clickhouse/base/socket.h
29
+ @@ -80,6 +80,12 @@ public:
30
+
31
+ virtual std::unique_ptr<InputStream> makeInputStream() const = 0;
32
+ virtual std::unique_ptr<OutputStream> makeOutputStream() const = 0;
33
+ +
34
+ + /// Unblock a thread parked in recv()/send() on this socket. Unlike
35
+ + /// closing or replacing the socket it destroys nothing, so it is safe
36
+ + /// to call from another thread while the owner is mid-read: the owner
37
+ + /// sees EOF and unwinds through its own frames.
38
+ + virtual void Cancel() {}
39
+ };
40
+
41
+
42
+ @@ -123,6 +129,8 @@ public:
43
+ std::unique_ptr<InputStream> makeInputStream() const override;
44
+ std::unique_ptr<OutputStream> makeOutputStream() const override;
45
+
46
+ + void Cancel() override;
47
+ +
48
+ protected:
49
+ Socket(const Socket&) = delete;
50
+ Socket& operator = (const Socket&) = delete;
51
+ diff --git a/clickhouse/client.cpp b/clickhouse/client.cpp
52
+ index c9732f4..88c9b84 100644
53
+ --- a/clickhouse/client.cpp
54
+ +++ b/clickhouse/client.cpp
55
+ @@ -9,6 +9,7 @@
56
+ #include "columns/factory.h"
57
+
58
+ #include <cassert>
59
+ +#include <mutex>
60
+ #include <optional>
61
+ #include <sstream>
62
+ #include <system_error>
63
+ @@ -226,6 +227,8 @@ public:
64
+
65
+ void ResetConnection();
66
+
67
+ + void CancelInFlight();
68
+ +
69
+ void ResetConnectionEndpoint();
70
+
71
+ const ServerInfo& GetServerInfo() const;
72
+ @@ -313,6 +316,11 @@ private:
73
+
74
+ std::unique_ptr<SocketFactory> socket_factory_;
75
+
76
+ + /// Guards socket_ against CancelInFlight() on another thread. Covers the
77
+ + /// swap in ResetConnection() and the displaced socket's destructor, so a
78
+ + /// canceller can never shutdown() an fd that is closing or already reissued.
79
+ + std::mutex socket_mutex_;
80
+ +
81
+ std::unique_ptr<InputStream> input_;
82
+ std::unique_ptr<OutputStream> output_;
83
+ std::unique_ptr<SocketBase> socket_;
84
+ @@ -610,8 +618,24 @@ void Client::Impl::Ping() {
85
+ }
86
+ }
87
+
88
+ +void Client::Impl::CancelInFlight() {
89
+ + std::lock_guard<std::mutex> lock(socket_mutex_);
90
+ + if (socket_) {
91
+ + socket_->Cancel();
92
+ + }
93
+ +}
94
+ +
95
+ void Client::Impl::ResetConnection() {
96
+ - InitializeStreams(socket_factory_->connect(options_, current_endpoint_.value()));
97
+ + auto fresh = socket_factory_->connect(options_, current_endpoint_.value());
98
+ + {
99
+ + // InitializeStreams swaps, so `fresh` comes back holding the *old*
100
+ + // socket; reset it here so its destructor runs under the lock too.
101
+ + // Left to itself it would die in this function's full-expression,
102
+ + // outside any lock, which is the window CancelInFlight() must not hit.
103
+ + std::lock_guard<std::mutex> lock(socket_mutex_);
104
+ + InitializeStreams(std::move(fresh));
105
+ + fresh.reset();
106
+ + }
107
+ state_ = State::Idle;
108
+
109
+ if (!Handshake()) {
110
+ @@ -1408,6 +1432,10 @@ void Client::ResetConnection() {
111
+ impl_->ResetConnection();
112
+ }
113
+
114
+ +void Client::CancelInFlight() {
115
+ + impl_->CancelInFlight();
116
+ +}
117
+ +
118
+ void Client::ResetConnectionEndpoint() {
119
+ impl_->ResetConnectionEndpoint();
120
+ }
121
+ diff --git a/clickhouse/client.h b/clickhouse/client.h
122
+ index 6c6ff53..27be58c 100644
123
+ --- a/clickhouse/client.h
124
+ +++ b/clickhouse/client.h
125
+ @@ -326,6 +326,11 @@ public:
126
+ /// Reset connection with initial params.
127
+ void ResetConnection();
128
+
129
+ + /// Unblock a thread parked in an Execute()/Insert() socket read.
130
+ + /// Destroys nothing, so unlike ResetConnection() it is safe to call
131
+ + /// from another thread while a query is still in flight.
132
+ + void CancelInFlight();
133
+ +
134
+ const ServerInfo& GetServerInfo() const;
135
+
136
+ /// Get current connected endpoint.
@@ -22,8 +22,8 @@ module ClickhouseNative
22
22
  end
23
23
  end
24
24
 
25
- # On exception, discard the client rather than reuse it: an error
26
- # path leaves the socket in an unknown state. The C++ binding issues
25
+ # On an aborted operation, discard the client rather than reuse it: the
26
+ # socket is left in an unknown state. The C++ binding issues
27
27
  # ResetConnection, but a subsequent send can still surface buffered
28
28
  # protocol errors from the prior aborted operation — those get
29
29
  # attributed to whatever SQL we tried next, producing misleading log
@@ -44,15 +44,18 @@ module ClickhouseNative
44
44
  # before running the query, so most stale connections never surface as
45
45
  # a ConnectionError here at all. This retry still covers the residual
46
46
  # race (socket dies between the ping and the query).
47
+ #
48
+ # A bare `rescue` is not enough to spot an abandoned query. Timeout and
49
+ # Sidekiq shutdown raise off Exception rather than StandardError, and
50
+ # Thread#kill raises nothing at all — Parallel.in_threads kills every
51
+ # sibling worker the moment one of them fails. Miss those and the
52
+ # connection goes back in with the server still streaming a response at
53
+ # it; the next checkout reads that leftover as its own, which surfaces
54
+ # as a bogus packet type far from here.
47
55
  def with
48
56
  attempts = 0
49
57
  begin
50
- @pool.with do |client|
51
- yield client
52
- rescue
53
- @pool.discard_current_connection(&:close)
54
- raise
55
- end
58
+ @pool.with { |client| discard_unless_clean { yield client } }
56
59
  rescue ConnectionError
57
60
  attempts += 1
58
61
  retry if attempts == 1
@@ -91,5 +94,30 @@ module ClickhouseNative
91
94
  def describe_table(table, db_name: nil)
92
95
  with { |c| c.describe_table(table, db_name:) }
93
96
  end
97
+
98
+ private
99
+
100
+ # Keep the client only when the block either finished or left of its own
101
+ # accord. A `break` out of a streaming read is deliberate, and the binding
102
+ # has already reconnected the client on its way out (query_each resets the
103
+ # connection before rb_jump_tag), so it is good to reuse — discarding would
104
+ # buy a second connect on top of the one already paid. An exception is an
105
+ # abort, and so is Thread#kill: it raises nothing, so no rescue ever sees
106
+ # it, and its only trace is an "aborting" thread while ensure runs.
107
+ def discard_unless_clean
108
+ finished = false
109
+ begin
110
+ result = yield
111
+ finished = true
112
+ result
113
+ rescue Exception # rubocop:disable Lint/RescueException
114
+ @pool.discard_current_connection(&:close)
115
+ raise
116
+ ensure
117
+ if !finished && Thread.current.status == "aborting"
118
+ @pool.discard_current_connection(&:close)
119
+ end
120
+ end
121
+ end
94
122
  end
95
123
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClickhouseNative
4
- VERSION = "0.11.0"
4
+ VERSION = "0.11.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clickhouse-native
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuri Smirnov
@@ -36,6 +36,7 @@ files:
36
36
  - ext/clickhouse_native/extconf.rb
37
37
  - ext/clickhouse_native/patches/0001-preserve-declared-column-type.patch
38
38
  - ext/clickhouse_native/patches/0002-carry-settings-into-insert.patch
39
+ - ext/clickhouse_native/patches/0003-cancelable-socket.patch
39
40
  - ext/clickhouse_native/vendor/clickhouse-cpp/.clang-format
40
41
  - ext/clickhouse_native/vendor/clickhouse-cpp/.git
41
42
  - ext/clickhouse_native/vendor/clickhouse-cpp/.gitattributes