clickhouse-native 0.10.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: d3e2cb4cbe829d9afd7e30e0ac29ea828020896c93634852b4baef007348d3b0
4
- data.tar.gz: e5f13079f4de6e64e545f49736a8da6832de954dfe82f1a4d105dd1228680887
3
+ metadata.gz: dbc7f982fc96317d6b6721bc7162e22d01eac4f4fc9edb307443d24fdee59032
4
+ data.tar.gz: 824150b24641414bfc0ccc4a6e37aa521b768bb73423c1bda9c1bfbefc466044
5
5
  SHA512:
6
- metadata.gz: 9998240b8ca84a4d28ad1fb956c0bf92e1893d4a9ce024fa443eefaa01efb35b657b55d8c2ce92718fd9224a5167fe0201a955f7147ab7499560a331906e2567
7
- data.tar.gz: d097a39e1fa03b051a30fe1fb273a928b7e2e555e05945d32b22a9dd9dc1deaf280193b80ecae146d7d442d513bbfb6e638ba4b63589f5a58171ce4b25ba5b87
6
+ metadata.gz: 6cda8a41d58816d63aa064ae3008b20980d35be6f695ff04433bc760ec28c0a286b707bdeb725d5691ac8245533c05d90df4c9c5b3eb13af6b860a179d9349db
7
+ data.tar.gz: caf00f391e65eb920a2d917316fa05c1569b7aac7c89f83a122b149c3305a3b0b40b6dc02afa5bd1cebcd78f0897700d5a2f9fbafb2c0d327da973dd3bc542f6
@@ -17,11 +17,13 @@
17
17
  #include <clickhouse/exceptions.h>
18
18
  #include <clickhouse/types/types.h>
19
19
 
20
+ #include <chrono>
20
21
  #include <cstdint>
21
22
  #include <exception>
22
23
  #include <memory>
23
24
  #include <string>
24
25
  #include <system_error>
26
+ #include <utility>
25
27
  #include <vector>
26
28
 
27
29
  using namespace clickhouse;
@@ -666,6 +668,11 @@ static void append_value(const ColumnRef& col, VALUE value) {
666
668
 
667
669
  struct CHClient {
668
670
  std::unique_ptr<Client> client;
671
+ // Session settings applied to *every* query as per-query settings, rather
672
+ // than a one-time session `SET`. Per-query settings ride each query packet,
673
+ // so they survive a transparent ping_before_query reconnect (a fresh socket
674
+ // would otherwise lose a session-level SET and fall back to server defaults).
675
+ std::vector<std::pair<std::string, std::string>> default_settings;
669
676
  };
670
677
 
671
678
  static void ch_client_free(void* p) {
@@ -707,6 +714,18 @@ static uint16_t kwarg_uint16(VALUE kwargs, const char* key, uint16_t fallback) {
707
714
  return static_cast<uint16_t>(NUM2UINT(v));
708
715
  }
709
716
 
717
+ static bool kwarg_bool(VALUE kwargs, const char* key, bool fallback) {
718
+ VALUE v = rb_hash_lookup2(kwargs, ID2SYM(rb_intern(key)), Qundef);
719
+ if (v == Qundef) return fallback;
720
+ return RTEST(v);
721
+ }
722
+
723
+ static unsigned int kwarg_uint(VALUE kwargs, const char* key, unsigned int fallback) {
724
+ VALUE v = rb_hash_lookup2(kwargs, ID2SYM(rb_intern(key)), Qundef);
725
+ if (v == Qundef || NIL_P(v)) return fallback;
726
+ return NUM2UINT(v);
727
+ }
728
+
710
729
  static CompressionMethod kwarg_compression(VALUE kwargs) {
711
730
  VALUE v = rb_hash_lookup2(kwargs, ID2SYM(rb_intern("compression")), Qundef);
712
731
  if (v == Qundef || NIL_P(v)) return CompressionMethod::None;
@@ -742,6 +761,16 @@ static int apply_settings_cb(VALUE key, VALUE val, VALUE arg) {
742
761
  return ST_CONTINUE;
743
762
  }
744
763
 
764
+ // Collect a `settings:` Hash into a client's default_settings vector at
765
+ // construction. Values are stringified the same way per-query settings are.
766
+ static int collect_setting_cb(VALUE key, VALUE val, VALUE arg) {
767
+ auto* vec = reinterpret_cast<std::vector<std::pair<std::string, std::string>>*>(arg);
768
+ VALUE k = SYMBOL_P(key) ? rb_sym2str(key) : key;
769
+ StringValue(k);
770
+ vec->emplace_back(std::string(RSTRING_PTR(k), RSTRING_LEN(k)), stringify_setting_value(val));
771
+ return ST_CONTINUE;
772
+ }
773
+
745
774
  // Read a `settings:` Hash out of the parsed kwargs and stamp each entry
746
775
  // onto `q` as a per-query setting. No-op if kwargs is nil or settings is
747
776
  // missing/empty. Raises TypeError if settings is not a Hash.
@@ -765,6 +794,16 @@ static void apply_read_settings(Query& q, VALUE kwargs) {
765
794
  apply_settings(q, kwargs);
766
795
  }
767
796
 
797
+ // Stamp the client's construction-time `settings:` onto `q`. Marked important
798
+ // (flag 1) so an unknown setting name errors — matching the old session `SET`,
799
+ // which failed loudly instead of silently ignoring a typo'd setting. Applied
800
+ // before per-call settings so an explicit `settings:` on the call still wins.
801
+ static void apply_default_settings(Query& q, CHClient* c) {
802
+ for (const auto& kv : c->default_settings) {
803
+ q.SetSetting(kv.first, QuerySettingsField{kv.second, 1});
804
+ }
805
+ }
806
+
768
807
  // Client.new(host:, port:, database:, user:, password:)
769
808
  static VALUE ch_client_initialize(int argc, VALUE* argv, VALUE self) {
770
809
  VALUE kwargs = Qnil;
@@ -778,12 +817,35 @@ static VALUE ch_client_initialize(int argc, VALUE* argv, VALUE self) {
778
817
  std::string password = kwarg_str(kwargs, "password", "");
779
818
  CompressionMethod compression = kwarg_compression(kwargs);
780
819
 
820
+ // Connection-resilience defaults. Long-lived pooled connections get
821
+ // silently closed by the server (idle_connection_timeout) or an LB, so
822
+ // the next use of a checked-out client would otherwise hit recv()==0 and
823
+ // raise ConnectionError("closed: ..."). ping_before_query makes the driver
824
+ // ping first and transparently reconnect on a dead socket; tcp_keepalive
825
+ // keeps idle sockets healthy at the OS level. retry_timeout is the backoff
826
+ // before each reconnect attempt — kept low (1s) since a pooled reconnect
827
+ // to a live server should be quick and we don't want to stall queries.
828
+ bool ping_before_query = kwarg_bool(kwargs, "ping_before_query", true);
829
+ bool tcp_keepalive = kwarg_bool(kwargs, "tcp_keepalive", true);
830
+ unsigned int retry_timeout = kwarg_uint(kwargs, "retry_timeout", 1);
831
+
781
832
  CHClient* c = as_client(self);
833
+
834
+ VALUE settings = rb_hash_lookup2(kwargs, ID2SYM(rb_intern("settings")), Qnil);
835
+ if (!NIL_P(settings)) {
836
+ Check_Type(settings, T_HASH);
837
+ rb_hash_foreach(settings, collect_setting_cb,
838
+ reinterpret_cast<VALUE>(&c->default_settings));
839
+ }
840
+
782
841
  try {
783
842
  ClientOptions opts;
784
843
  opts.SetHost(host).SetPort(port)
785
844
  .SetDefaultDatabase(database).SetUser(user).SetPassword(password)
786
- .SetCompressionMethod(compression);
845
+ .SetCompressionMethod(compression)
846
+ .SetPingBeforeQuery(ping_before_query)
847
+ .TcpKeepAlive(tcp_keepalive)
848
+ .SetRetryTimeout(std::chrono::seconds(retry_timeout));
787
849
  c->client = std::make_unique<Client>(opts);
788
850
  } catch (const std::exception& e) {
789
851
  raise_mapped_ex(e);
@@ -792,6 +854,9 @@ static VALUE ch_client_initialize(int argc, VALUE* argv, VALUE self) {
792
854
  rb_ivar_set(self, rb_intern("@host"), rb_utf8_str_new(host.data(), host.size()));
793
855
  rb_ivar_set(self, rb_intern("@port"), UINT2NUM(port));
794
856
  rb_ivar_set(self, rb_intern("@database"), rb_utf8_str_new(database.data(), database.size()));
857
+ rb_ivar_set(self, rb_intern("@ping_before_query"), ping_before_query ? Qtrue : Qfalse);
858
+ rb_ivar_set(self, rb_intern("@tcp_keepalive"), tcp_keepalive ? Qtrue : Qfalse);
859
+ rb_ivar_set(self, rb_intern("@retry_timeout"), UINT2NUM(retry_timeout));
795
860
 
796
861
  VALUE logger = rb_hash_lookup2(kwargs, ID2SYM(rb_intern("logger")), Qnil);
797
862
  rb_ivar_set(self, rb_intern("@logger"), logger);
@@ -807,6 +872,7 @@ struct ExecuteNoGVL {
807
872
  Client* client;
808
873
  const Query* query;
809
874
  std::exception_ptr err;
875
+ bool cancelled;
810
876
  };
811
877
  } // namespace
812
878
 
@@ -820,11 +886,16 @@ static void* execute_no_gvl(void* data) {
820
886
  return nullptr;
821
887
  }
822
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.
823
895
  static void execute_unblock(void* data) {
824
- // The only safe abort clickhouse-cpp exposes is tearing the connection.
825
- // On interrupt we kill the socket; the pool will discard this client.
826
896
  auto* a = static_cast<ExecuteNoGVL*>(data);
827
- try { a->client->ResetConnection(); } catch (...) {}
897
+ a->cancelled = true;
898
+ try { a->client->CancelInFlight(); } catch (...) {}
828
899
  }
829
900
 
830
901
  static VALUE ch_client_execute(int argc, VALUE* argv, VALUE self) {
@@ -835,15 +906,23 @@ static VALUE ch_client_execute(int argc, VALUE* argv, VALUE self) {
835
906
  if (!c->client) rb_raise(err_connection, "clickhouse-native: client is closed");
836
907
 
837
908
  Query q(std::string(RSTRING_PTR(rb_sql), RSTRING_LEN(rb_sql)));
909
+ apply_default_settings(q, c);
838
910
  apply_settings(q, kwargs);
839
911
 
840
- ExecuteNoGVL args{c->client.get(), &q, nullptr};
912
+ ExecuteNoGVL args{c->client.get(), &q, nullptr, false};
841
913
  rb_thread_call_without_gvl(execute_no_gvl, &args, execute_unblock, &args);
842
914
  if (args.err) {
843
915
  // clickhouse-cpp may leave the read stream partially consumed when the
844
916
  // server exception or an unsupported-type error is thrown mid-block.
845
917
  // Reset so the next call on this Client starts from a clean protocol.
846
- 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 (...) {} }
847
926
  try { std::rethrow_exception(args.err); }
848
927
  catch (const std::exception& e) { raise_mapped_ex(e); }
849
928
  }
@@ -866,6 +945,7 @@ static VALUE ch_client_query(int argc, VALUE* argv, VALUE self) {
866
945
  try {
867
946
  std::vector<ID> col_ids;
868
947
  Query q(std::string(RSTRING_PTR(rb_sql), RSTRING_LEN(rb_sql)));
948
+ apply_default_settings(q, c);
869
949
  apply_read_settings(q, kwargs);
870
950
  q.OnData([&](const Block& block) {
871
951
  size_t ncols = block.GetColumnCount();
@@ -911,6 +991,7 @@ static VALUE ch_client_query_value(int argc, VALUE* argv, VALUE self) {
911
991
  VALUE out = Qnil;
912
992
  bool seen = false;
913
993
  Query q(std::string(RSTRING_PTR(rb_sql), RSTRING_LEN(rb_sql)));
994
+ apply_default_settings(q, c);
914
995
  apply_read_settings(q, kwargs);
915
996
  q.OnData([&](const Block& block) {
916
997
  if (seen) return;
@@ -936,14 +1017,16 @@ struct InsertNoGVL {
936
1017
  Client* client;
937
1018
  const std::string* table;
938
1019
  const Block* block;
1020
+ const std::vector<std::pair<std::string, std::string>>* settings;
939
1021
  std::exception_ptr err;
1022
+ bool cancelled;
940
1023
  };
941
1024
  } // namespace
942
1025
 
943
1026
  static void* insert_no_gvl(void* data) {
944
1027
  auto* a = static_cast<InsertNoGVL*>(data);
945
1028
  try {
946
- a->client->Insert(*a->table, *a->block);
1029
+ a->client->Insert(*a->table, *a->block, *a->settings);
947
1030
  } catch (...) {
948
1031
  a->err = std::current_exception();
949
1032
  }
@@ -952,7 +1035,8 @@ static void* insert_no_gvl(void* data) {
952
1035
 
953
1036
  static void insert_unblock(void* data) {
954
1037
  auto* a = static_cast<InsertNoGVL*>(data);
955
- try { a->client->ResetConnection(); } catch (...) {}
1038
+ a->cancelled = true;
1039
+ try { a->client->CancelInFlight(); } catch (...) {}
956
1040
  }
957
1041
 
958
1042
  static VALUE ch_client_insert_block(VALUE self, VALUE rb_table, VALUE rb_columns, VALUE rb_rows) {
@@ -1009,10 +1093,10 @@ static VALUE ch_client_insert_block(VALUE self, VALUE rb_table, VALUE rb_columns
1009
1093
  block.AppendColumn(names[i], cols[i]);
1010
1094
  }
1011
1095
 
1012
- InsertNoGVL args{c->client.get(), &table, &block, nullptr};
1096
+ InsertNoGVL args{c->client.get(), &table, &block, &c->default_settings, nullptr, false};
1013
1097
  rb_thread_call_without_gvl(insert_no_gvl, &args, insert_unblock, &args);
1014
1098
  if (args.err) {
1015
- try { c->client->ResetConnection(); } catch (...) {}
1099
+ if (!args.cancelled) { try { c->client->ResetConnection(); } catch (...) {} }
1016
1100
  try { std::rethrow_exception(args.err); }
1017
1101
  catch (const std::exception& e) { raise_mapped_ex(e); }
1018
1102
  }
@@ -1045,6 +1129,7 @@ struct QueryEachNoGVL {
1045
1129
  const Query* query;
1046
1130
  QueryEachState* state;
1047
1131
  std::exception_ptr err;
1132
+ bool cancelled;
1048
1133
  };
1049
1134
  } // namespace
1050
1135
 
@@ -1097,7 +1182,8 @@ static void* query_each_no_gvl(void* data) {
1097
1182
  static void query_each_unblock(void* data) {
1098
1183
  auto* a = static_cast<QueryEachNoGVL*>(data);
1099
1184
  a->state->aborted = true;
1100
- try { a->client->ResetConnection(); } catch (...) {}
1185
+ a->cancelled = true;
1186
+ try { a->client->CancelInFlight(); } catch (...) {}
1101
1187
  }
1102
1188
 
1103
1189
  static VALUE ch_client_query_each(int argc, VALUE* argv, VALUE self) {
@@ -1110,6 +1196,7 @@ static VALUE ch_client_query_each(int argc, VALUE* argv, VALUE self) {
1110
1196
 
1111
1197
  QueryEachState state{rb_block_proc(), {}, 0, false};
1112
1198
  Query q(std::string(RSTRING_PTR(rb_sql), RSTRING_LEN(rb_sql)));
1199
+ apply_default_settings(q, c);
1113
1200
  apply_read_settings(q, kwargs);
1114
1201
  q.OnDataCancelable([&state](const Block& block) -> bool {
1115
1202
  if (state.aborted) return false;
@@ -1117,12 +1204,12 @@ static VALUE ch_client_query_each(int argc, VALUE* argv, VALUE self) {
1117
1204
  rb_thread_call_with_gvl(with_gvl_yield, &ya);
1118
1205
  return !state.aborted;
1119
1206
  });
1120
- QueryEachNoGVL args{c->client.get(), &q, &state, nullptr};
1207
+ QueryEachNoGVL args{c->client.get(), &q, &state, nullptr, false};
1121
1208
 
1122
1209
  rb_thread_call_without_gvl(query_each_no_gvl, &args, query_each_unblock, &args);
1123
1210
 
1124
1211
  if (args.err) {
1125
- try { c->client->ResetConnection(); } catch (...) {}
1212
+ if (!args.cancelled) { try { c->client->ResetConnection(); } catch (...) {} }
1126
1213
  if (state.exc_tag) rb_jump_tag(state.exc_tag);
1127
1214
  try { std::rethrow_exception(args.err); }
1128
1215
  catch (const std::exception& e) { raise_mapped_ex(e); }
@@ -0,0 +1,82 @@
1
+ diff --git a/clickhouse/client.cpp b/clickhouse/client.cpp
2
+ index 34a733d..c9732f4 100644
3
+ --- a/clickhouse/client.cpp
4
+ +++ b/clickhouse/client.cpp
5
+ @@ -211,7 +211,8 @@ public:
6
+
7
+ bool IsSelecting() const { return state_ == State::Selecting; }
8
+
9
+ - void Insert(const std::string& table_name, const std::string& query_id, const Block& block);
10
+ + void Insert(const std::string& table_name, const std::string& query_id, const Block& block,
11
+ + const std::vector<std::pair<std::string, std::string>>& settings = {});
12
+
13
+ Block BeginInsert(Query query);
14
+
15
+ @@ -485,7 +486,8 @@ std::string NameToQueryString(const std::string &input)
16
+ return output;
17
+ }
18
+
19
+ -void Client::Impl::Insert(const std::string& table_name, const std::string& query_id, const Block& block) {
20
+ +void Client::Impl::Insert(const std::string& table_name, const std::string& query_id, const Block& block,
21
+ + const std::vector<std::pair<std::string, std::string>>& settings) {
22
+ if (state_ == State::Inserting) {
23
+ throw ValidationError("cannot execute query while inserting, use SendInsertData instead");
24
+ }
25
+ @@ -511,6 +513,9 @@ void Client::Impl::Insert(const std::string& table_name, const std::string& quer
26
+ }
27
+
28
+ Query query("INSERT INTO " + table_name + " ( " + fields_section.str() + " ) VALUES", query_id);
29
+ + for (const auto& kv : settings) {
30
+ + query.SetSetting(kv.first, QuerySettingsField{kv.second, 1});
31
+ + }
32
+ SendQuery(query);
33
+
34
+ // Wait for a data packet and return
35
+ @@ -1365,12 +1370,14 @@ bool Client::IsSelecting() const
36
+ return impl_->IsSelecting();
37
+ }
38
+
39
+ -void Client::Insert(const std::string& table_name, const Block& block) {
40
+ - impl_->Insert(table_name, Query::default_query_id, block);
41
+ +void Client::Insert(const std::string& table_name, const Block& block,
42
+ + const std::vector<std::pair<std::string, std::string>>& settings) {
43
+ + impl_->Insert(table_name, Query::default_query_id, block, settings);
44
+ }
45
+
46
+ -void Client::Insert(const std::string& table_name, const std::string& query_id, const Block& block) {
47
+ - impl_->Insert(table_name, query_id, block);
48
+ +void Client::Insert(const std::string& table_name, const std::string& query_id, const Block& block,
49
+ + const std::vector<std::pair<std::string, std::string>>& settings) {
50
+ + impl_->Insert(table_name, query_id, block, settings);
51
+ }
52
+
53
+ Block Client::BeginInsert(const std::string& query) {
54
+ diff --git a/clickhouse/client.h b/clickhouse/client.h
55
+ index a55fd74..6c6ff53 100644
56
+ --- a/clickhouse/client.h
57
+ +++ b/clickhouse/client.h
58
+ @@ -23,6 +23,8 @@
59
+ #include "columns/bool.h"
60
+
61
+ #include <chrono>
62
+ +#include <utility>
63
+ +#include <vector>
64
+ #include <cstdint>
65
+ #include <memory>
66
+ #include <ostream>
67
+ @@ -298,8 +300,13 @@ public:
68
+ bool IsSelecting() const;
69
+
70
+ /// Intends for insert block of data into a table \p table_name.
71
+ - void Insert(const std::string& table_name, const Block& block);
72
+ - void Insert(const std::string& table_name, const std::string& query_id, const Block& block);
73
+ + /// \p settings are applied to the generated INSERT query as per-query
74
+ + /// settings (patched-in for clickhouse-native: lets a pooled client carry
75
+ + /// its session settings into inserts without a session-level SET).
76
+ + void Insert(const std::string& table_name, const Block& block,
77
+ + const std::vector<std::pair<std::string, std::string>>& settings = {});
78
+ + void Insert(const std::string& table_name, const std::string& query_id, const Block& block,
79
+ + const std::vector<std::pair<std::string, std::string>>& settings = {});
80
+
81
+ /// Start an \p INSERT statement, insert batches of data, then finish the insert.
82
+ Block BeginInsert(const std::string& query);
@@ -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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ClickhouseNative
4
4
  class Client
5
- attr_reader :host, :port, :database
5
+ attr_reader :host, :port, :database, :ping_before_query, :tcp_keepalive, :retry_timeout
6
6
 
7
7
  def describe_table(table, db_name: nil)
8
8
  fq = db_name ? "#{db_name}.#{table}" : table
@@ -8,44 +8,54 @@ module ClickhouseNative
8
8
 
9
9
  def initialize(host:, port:, database: "default", user: "default", password: "",
10
10
  compression: :none, logger: nil, settings: {},
11
- pool_size: 5, pool_timeout: 5)
11
+ pool_size: 5, pool_timeout: 5,
12
+ ping_before_query: true, tcp_keepalive: true, retry_timeout: 1)
12
13
  @host = host
13
14
  @port = port
14
15
  @database = database
15
- client_kwargs = { host:, port:, database:, user:, password:, compression:, logger: }
16
- @set_sql = settings_sql(settings)
16
+ client_kwargs = {
17
+ host:, port:, database:, user:, password:, compression:, logger:, settings:,
18
+ ping_before_query:, tcp_keepalive:, retry_timeout:
19
+ }
17
20
  @pool = ConnectionPool.new(size: pool_size, timeout: pool_timeout) do
18
- client = Client.new(**client_kwargs)
19
- client.execute(@set_sql) if @set_sql
20
- client
21
+ Client.new(**client_kwargs)
21
22
  end
22
23
  end
23
24
 
24
- # On exception, discard the client rather than reuse it: an error
25
- # 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
26
27
  # ResetConnection, but a subsequent send can still surface buffered
27
28
  # protocol errors from the prior aborted operation — those get
28
- # attributed to whatever SQL we tried next (e.g. the SET reapplying
29
- # session settings), producing misleading log lines and re-raises in
30
- # unrelated code. A fresh socket + handshake is cheap relative to
31
- # debugging that.
29
+ # attributed to whatever SQL we tried next, producing misleading log
30
+ # lines and re-raises in unrelated code. A fresh socket + handshake is
31
+ # cheap relative to debugging that.
32
32
  #
33
33
  # ConnectionError gets one automatic retry: pooled connections that
34
34
  # have been idle long enough for the server / an LB to FIN them
35
- # surface as "closed" on the very next recv (errno 0, message
36
- # "closed: Success"). Discarding and re-checking out lands a fresh
37
- # socket and the operation succeeds. The retry only triggers when
38
- # the dead-connection error fired before any data was sent, so
39
- # write operations don't risk double-execution from this path.
35
+ # surface as "closed" on the very next recv (errno is whatever stale
36
+ # value was left in the thread — "closed: Success", "closed: Operation
37
+ # now in progress", etc. all mean the same recv()==0). Discarding and
38
+ # re-checking out lands a fresh socket and the operation succeeds. The
39
+ # retry only triggers when the dead-connection error fired before any
40
+ # data was sent, so write operations don't risk double-execution.
41
+ #
42
+ # This is the backstop: with ping_before_query on (the default) the
43
+ # driver already pings and transparently reconnects a dead socket
44
+ # before running the query, so most stale connections never surface as
45
+ # a ConnectionError here at all. This retry still covers the residual
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.
40
55
  def with
41
56
  attempts = 0
42
57
  begin
43
- @pool.with do |client|
44
- yield client
45
- rescue
46
- @pool.discard_current_connection(&:close)
47
- raise
48
- end
58
+ @pool.with { |client| discard_unless_clean { yield client } }
49
59
  rescue ConnectionError
50
60
  attempts += 1
51
61
  retry if attempts == 1
@@ -87,18 +97,27 @@ module ClickhouseNative
87
97
 
88
98
  private
89
99
 
90
- # Render a `SET key1 = val1, key2 = val2` statement once at pool setup
91
- # so every checked-out connection starts with the same session
92
- # settings. Matches how the HTTP driver injected global_params per
93
- # request. Values: Integer / Float render bare; anything else is
94
- # quoted as a SQL string literal.
95
- def settings_sql(settings)
96
- return nil if settings.nil? || settings.empty?
97
- parts = settings.map do |k, v|
98
- literal = v.is_a?(Numeric) ? v.to_s : "'#{v.to_s.gsub("'", "''")}'"
99
- "#{k} = #{literal}"
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
100
120
  end
101
- "SET #{parts.join(', ')}"
102
121
  end
103
122
  end
104
123
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClickhouseNative
4
- VERSION = "0.10.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.10.0
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuri Smirnov
@@ -35,6 +35,8 @@ files:
35
35
  - ext/clickhouse_native/client.cpp
36
36
  - ext/clickhouse_native/extconf.rb
37
37
  - ext/clickhouse_native/patches/0001-preserve-declared-column-type.patch
38
+ - ext/clickhouse_native/patches/0002-carry-settings-into-insert.patch
39
+ - ext/clickhouse_native/patches/0003-cancelable-socket.patch
38
40
  - ext/clickhouse_native/vendor/clickhouse-cpp/.clang-format
39
41
  - ext/clickhouse_native/vendor/clickhouse-cpp/.git
40
42
  - ext/clickhouse_native/vendor/clickhouse-cpp/.gitattributes
@@ -323,7 +325,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
323
325
  - !ruby/object:Gem::Version
324
326
  version: '0'
325
327
  requirements: []
326
- rubygems_version: 4.0.10
328
+ rubygems_version: 4.0.16
327
329
  specification_version: 4
328
330
  summary: ClickHouse Ruby driver over the native TCP protocol
329
331
  test_files: []