clickhouse-native 0.11.0 → 0.12.0

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: d4bbe836a1758637fb9dd0a157505037b8bc45d42d253d261b3a349cecaf9716
4
+ data.tar.gz: 7c0cd5bbea6944c2c0672780088d4342cdf207227bce2c52f388986e9b26bf15
5
5
  SHA512:
6
- metadata.gz: c579da78d2185108712c92e902217c2e508655533c2e78efc7e36ae358a826f809f41d884684b8fcb0e1b36749bc80be5f23b76c85ebf081423adb615d73928b
7
- data.tar.gz: 93259646a6f150ed271202e3eaca075e0d1cf8d707aed5ac2ac9b530061de47cf1a65082289718857b367dd5b8d764c73cf270c34d080018ce8caac540d8bab6
6
+ metadata.gz: e3abc7862bbf8125ca8168d5a28337ebdf1e11c1f8a2965fff597ccaa4d27eaea70f2116d93b54d2822f6f6fd3fca237e8a864d1cf30e91c0195bb4e730f22be
7
+ data.tar.gz: 101b7ecc586641e3c0df0f14f0dbd977a450d578e029fbc220f1b86218d4e84ef4a22d5c29eaec1c1e463db96e5fdc9a2b26fb5efdfa0cf1c6780a40aee1d047
@@ -804,6 +804,64 @@ static void apply_default_settings(Query& q, CHClient* c) {
804
804
  }
805
805
  }
806
806
 
807
+ // ------------------------------------------------------------------
808
+ // Connect / reconnect, off the GVL
809
+ // ------------------------------------------------------------------
810
+
811
+ namespace {
812
+ struct ResetNoGVL {
813
+ Client* client;
814
+ std::exception_ptr err;
815
+ bool ran;
816
+ };
817
+
818
+ struct ConnectNoGVL {
819
+ const ClientOptions* opts;
820
+ std::unique_ptr<Client>* out;
821
+ std::exception_ptr err;
822
+ };
823
+ } // namespace
824
+
825
+ static void* connect_no_gvl(void* data) {
826
+ auto* a = static_cast<ConnectNoGVL*>(data);
827
+ try { *a->out = std::make_unique<Client>(*a->opts); }
828
+ catch (...) { a->err = std::current_exception(); }
829
+ return nullptr;
830
+ }
831
+
832
+ static void* reset_no_gvl(void* data) {
833
+ auto* a = static_cast<ResetNoGVL*>(data);
834
+ a->ran = true;
835
+ try { a->client->ResetConnection(); } catch (...) { a->err = std::current_exception(); }
836
+ return nullptr;
837
+ }
838
+
839
+ // ResetConnection() opens a fresh socket and re-handshakes, so it blocks on the
840
+ // network exactly like a query does. Every error path calls it, which is when
841
+ // the server is least likely to answer promptly - the worst moment to be
842
+ // holding the GVL. No unblock function: there is no in-flight query to cancel,
843
+ // and the half-built connection is this thread's.
844
+ //
845
+ // _gvl2, not the ordinary one: every caller is already reporting some other
846
+ // error, and the ordinary variant checks pending interrupts as it retakes the
847
+ // GVL and can longjmp out. That would jump between an rb_protect tag and the
848
+ // rb_jump_tag meant to re-raise it, or out of the active catch handler in
849
+ // insert_block, and would lose the error the caller came here to report.
850
+ //
851
+ // _gvl2 declines to run `func` at all when an interrupt is already pending,
852
+ // though, so the reset is not guaranteed - and one caller needs it to happen:
853
+ // the `break` out of query_each keeps its client, on the understanding that
854
+ // the reset here left it clean (see Pool#discard_unless_clean). Falling back
855
+ // to the plain call covers that. Holding the GVL for a reconnect is the lesser
856
+ // evil against handing back a connection with a reply still draining into it.
857
+ // Failures stay swallowed either way: the pool discards on every other path.
858
+ static void reset_connection_no_gvl(Client* client) {
859
+ if (!client) return;
860
+ ResetNoGVL args{client, nullptr, false};
861
+ rb_thread_call_without_gvl2(reset_no_gvl, &args, nullptr, nullptr);
862
+ if (!args.ran) { try { client->ResetConnection(); } catch (...) {} }
863
+ }
864
+
807
865
  // Client.new(host:, port:, database:, user:, password:)
808
866
  static VALUE ch_client_initialize(int argc, VALUE* argv, VALUE self) {
809
867
  VALUE kwargs = Qnil;
@@ -838,7 +896,7 @@ static VALUE ch_client_initialize(int argc, VALUE* argv, VALUE self) {
838
896
  reinterpret_cast<VALUE>(&c->default_settings));
839
897
  }
840
898
 
841
- try {
899
+ {
842
900
  ClientOptions opts;
843
901
  opts.SetHost(host).SetPort(port)
844
902
  .SetDefaultDatabase(database).SetUser(user).SetPassword(password)
@@ -846,9 +904,17 @@ static VALUE ch_client_initialize(int argc, VALUE* argv, VALUE self) {
846
904
  .SetPingBeforeQuery(ping_before_query)
847
905
  .TcpKeepAlive(tcp_keepalive)
848
906
  .SetRetryTimeout(std::chrono::seconds(retry_timeout));
849
- c->client = std::make_unique<Client>(opts);
850
- } catch (const std::exception& e) {
851
- raise_mapped_ex(e);
907
+ // The constructor connects and handshakes, and retries for
908
+ // SetRetryTimeout seconds against an unreachable host - long enough to
909
+ // freeze every other thread if it ran under the GVL. No unblock
910
+ // function: there is no in-flight query to cancel, and the socket being
911
+ // built belongs to this thread.
912
+ ConnectNoGVL args{&opts, &c->client, nullptr};
913
+ rb_thread_call_without_gvl(connect_no_gvl, &args, nullptr, nullptr);
914
+ if (args.err) {
915
+ try { std::rethrow_exception(args.err); }
916
+ catch (const std::exception& e) { raise_mapped_ex(e); }
917
+ }
852
918
  }
853
919
 
854
920
  rb_ivar_set(self, rb_intern("@host"), rb_utf8_str_new(host.data(), host.size()));
@@ -872,6 +938,7 @@ struct ExecuteNoGVL {
872
938
  Client* client;
873
939
  const Query* query;
874
940
  std::exception_ptr err;
941
+ bool cancelled;
875
942
  };
876
943
  } // namespace
877
944
 
@@ -885,11 +952,16 @@ static void* execute_no_gvl(void* data) {
885
952
  return nullptr;
886
953
  }
887
954
 
955
+ // Unblock functions run on the *interrupting* thread while the blocked thread
956
+ // is still inside Client::Execute(). CancelInFlight() only shuts the socket
957
+ // down; ResetConnection() would free the very streams that thread is reading
958
+ // (Thread#kill from Parallel.in_threads, Timeout, Sidekiq shutdown), which
959
+ // reads back as a garbage packet type and then segfaults. The blocked call
960
+ // returns EOF, raises, and the pool discards the client.
888
961
  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
962
  auto* a = static_cast<ExecuteNoGVL*>(data);
892
- try { a->client->ResetConnection(); } catch (...) {}
963
+ a->cancelled = true;
964
+ try { a->client->CancelInFlight(); } catch (...) {}
893
965
  }
894
966
 
895
967
  static VALUE ch_client_execute(int argc, VALUE* argv, VALUE self) {
@@ -903,13 +975,20 @@ static VALUE ch_client_execute(int argc, VALUE* argv, VALUE self) {
903
975
  apply_default_settings(q, c);
904
976
  apply_settings(q, kwargs);
905
977
 
906
- ExecuteNoGVL args{c->client.get(), &q, nullptr};
978
+ ExecuteNoGVL args{c->client.get(), &q, nullptr, false};
907
979
  rb_thread_call_without_gvl(execute_no_gvl, &args, execute_unblock, &args);
908
980
  if (args.err) {
909
981
  // clickhouse-cpp may leave the read stream partially consumed when the
910
982
  // server exception or an unsupported-type error is thrown mid-block.
911
983
  // Reset so the next call on this Client starts from a clean protocol.
912
- try { c->client->ResetConnection(); } catch (...) {}
984
+ // Not after a cancel: that socket is already shut down and the caller
985
+ // discards the client, so the connect+handshake buys nothing. A
986
+ // Thread#kill or Timeout never gets here at all — step 5 of
987
+ // rb_thread_call_without_gvl delivers the interrupt before it returns —
988
+ // so this covers what is left: an unblock function that fires without a
989
+ // longjmp behind it, from a trap handler that does not raise or an
990
+ // interrupt Thread.handle_interrupt has deferred.
991
+ if (!args.cancelled) reset_connection_no_gvl(c->client.get());
913
992
  try { std::rethrow_exception(args.err); }
914
993
  catch (const std::exception& e) { raise_mapped_ex(e); }
915
994
  }
@@ -917,82 +996,171 @@ static VALUE ch_client_execute(int argc, VALUE* argv, VALUE self) {
917
996
  }
918
997
 
919
998
  // ------------------------------------------------------------------
920
- // query() — buffers all rows into an array; GVL is held for the duration.
921
- // See query_each below for a streaming, GVL-releasing variant.
999
+ // query() / query_value() buffer the result, releasing the GVL while the
1000
+ // server streams. The data callback is the only part that needs Ruby, so it
1001
+ // takes the GVL back per block (as query_each does) rather than the whole
1002
+ // call holding it: a multi-second read otherwise stops every other thread in
1003
+ // the process, which starves timers and lock heartbeats.
922
1004
  // ------------------------------------------------------------------
923
1005
 
924
- static VALUE ch_client_query(int argc, VALUE* argv, VALUE self) {
925
- VALUE rb_sql, kwargs = Qnil;
926
- rb_scan_args(argc, argv, "1:", &rb_sql, &kwargs);
927
- Check_Type(rb_sql, T_STRING);
928
- CHClient* c = as_client(self);
929
- if (!c->client) rb_raise(err_connection, "clickhouse-native: client is closed");
1006
+ namespace {
1007
+ struct CollectState {
1008
+ VALUE rows; // Qnil for query_value, which keeps `out` instead
1009
+ VALUE out;
1010
+ bool first_only;
1011
+ bool seen;
1012
+ std::vector<ID> col_ids;
1013
+ int exc_tag;
1014
+ std::exception_ptr cpp_err;
1015
+ bool aborted;
1016
+ };
1017
+
1018
+ struct CollectBlockArgs {
1019
+ const Block* block;
1020
+ CollectState* state;
1021
+ };
1022
+
1023
+ struct QueryNoGVL {
1024
+ Client* client;
1025
+ const Query* query;
1026
+ CollectState* state;
1027
+ std::exception_ptr err;
1028
+ bool cancelled;
1029
+ };
1030
+ } // namespace
1031
+
1032
+ // The decode throw is caught here, inside the frame rb_protect calls, so it
1033
+ // never unwinds through rb_protect itself - doing that walks a setjmp frame
1034
+ // whose epilogue never runs and takes the interpreter down. Carried out via
1035
+ // state instead; run_buffered_query rethrows it after the no-GVL region.
1036
+ static VALUE collect_rows_body(VALUE arg) {
1037
+ auto* args = reinterpret_cast<CollectBlockArgs*>(arg);
1038
+ const Block& block = *args->block;
1039
+ auto* state = args->state;
930
1040
 
931
- VALUE rows = rb_ary_new();
932
1041
  try {
933
- std::vector<ID> col_ids;
934
- Query q(std::string(RSTRING_PTR(rb_sql), RSTRING_LEN(rb_sql)));
935
- apply_default_settings(q, c);
936
- apply_read_settings(q, kwargs);
937
- q.OnData([&](const Block& block) {
938
- size_t ncols = block.GetColumnCount();
939
- size_t nrows = block.GetRowCount();
940
- if (nrows == 0) return;
941
- if (col_ids.empty()) {
942
- col_ids.reserve(ncols);
943
- for (size_t i = 0; i < ncols; i++) {
944
- const std::string& name = block.GetColumnName(i);
945
- col_ids.push_back(rb_intern2(name.data(), name.size()));
946
- }
1042
+ size_t ncols = block.GetColumnCount();
1043
+ size_t nrows = block.GetRowCount();
1044
+ if (nrows == 0 || ncols == 0) return Qnil;
1045
+
1046
+ if (state->first_only) {
1047
+ if (!state->seen) {
1048
+ state->out = value_at(block[0], 0, block.GetColumnType(0));
1049
+ state->seen = true;
947
1050
  }
948
- for (size_t r = 0; r < nrows; r++) {
949
- VALUE h = rb_hash_new();
950
- for (size_t cc = 0; cc < ncols; cc++) {
951
- rb_hash_aset(h, ID2SYM(col_ids[cc]),
952
- value_at(block[cc], r, block.GetColumnType(cc)));
953
- }
954
- rb_ary_push(rows, h);
1051
+ return Qnil;
1052
+ }
1053
+
1054
+ if (state->col_ids.empty()) {
1055
+ state->col_ids.reserve(ncols);
1056
+ for (size_t i = 0; i < ncols; i++) {
1057
+ const std::string& name = block.GetColumnName(i);
1058
+ state->col_ids.push_back(rb_intern2(name.data(), name.size()));
955
1059
  }
956
- });
957
- c->client->Execute(q);
958
- return rows;
959
- } catch (const std::exception& e) {
960
- try { c->client->ResetConnection(); } catch (...) {}
961
- raise_mapped_ex(e);
1060
+ }
1061
+ for (size_t r = 0; r < nrows; r++) {
1062
+ VALUE h = rb_hash_new();
1063
+ for (size_t cc = 0; cc < ncols; cc++) {
1064
+ rb_hash_aset(h, ID2SYM(state->col_ids[cc]),
1065
+ value_at(block[cc], r, block.GetColumnType(cc)));
1066
+ }
1067
+ rb_ary_push(state->rows, h);
1068
+ }
1069
+ } catch (...) {
1070
+ state->cpp_err = std::current_exception();
1071
+ state->aborted = true;
962
1072
  }
963
1073
  return Qnil;
964
1074
  }
965
1075
 
966
- // ------------------------------------------------------------------
967
- // query_value returns the first cell of the first row, or nil
968
- // ------------------------------------------------------------------
1076
+ static void* with_gvl_collect(void* data) {
1077
+ auto* args = static_cast<CollectBlockArgs*>(data);
1078
+ int tag = 0;
1079
+ rb_protect(collect_rows_body, reinterpret_cast<VALUE>(args), &tag);
1080
+ if (tag != 0) {
1081
+ args->state->exc_tag = tag;
1082
+ args->state->aborted = true;
1083
+ }
1084
+ return nullptr;
1085
+ }
969
1086
 
970
- static VALUE ch_client_query_value(int argc, VALUE* argv, VALUE self) {
1087
+ static void* query_no_gvl(void* data) {
1088
+ auto* a = static_cast<QueryNoGVL*>(data);
1089
+ try {
1090
+ a->client->Execute(*a->query);
1091
+ } catch (...) {
1092
+ a->err = std::current_exception();
1093
+ }
1094
+ return nullptr;
1095
+ }
1096
+
1097
+ // See execute_unblock: cancel the in-flight query, never tear the streams down
1098
+ // from another thread.
1099
+ static void query_unblock(void* data) {
1100
+ auto* a = static_cast<QueryNoGVL*>(data);
1101
+ a->state->aborted = true;
1102
+ a->cancelled = true;
1103
+ try { a->client->CancelInFlight(); } catch (...) {}
1104
+ }
1105
+
1106
+ static VALUE run_buffered_query(VALUE self, int argc, VALUE* argv, bool first_only) {
971
1107
  VALUE rb_sql, kwargs = Qnil;
972
1108
  rb_scan_args(argc, argv, "1:", &rb_sql, &kwargs);
973
1109
  Check_Type(rb_sql, T_STRING);
974
1110
  CHClient* c = as_client(self);
975
1111
  if (!c->client) rb_raise(err_connection, "clickhouse-native: client is closed");
976
1112
 
977
- try {
978
- VALUE out = Qnil;
979
- bool seen = false;
980
- Query q(std::string(RSTRING_PTR(rb_sql), RSTRING_LEN(rb_sql)));
981
- apply_default_settings(q, c);
982
- apply_read_settings(q, kwargs);
983
- q.OnData([&](const Block& block) {
984
- if (seen) return;
985
- if (block.GetRowCount() == 0 || block.GetColumnCount() == 0) return;
986
- out = value_at(block[0], 0, block.GetColumnType(0));
987
- seen = true;
988
- });
989
- c->client->Execute(q);
990
- return out;
991
- } catch (const std::exception& e) {
992
- try { c->client->ResetConnection(); } catch (...) {}
993
- raise_mapped_ex(e);
1113
+ // `state` lives on this thread's machine stack, and that is what marks
1114
+ // state.rows and state.out while the GVL is dropped below: the GC scans the
1115
+ // stack conservatively, and the context saved at the release covers the
1116
+ // no-GVL window. Moving CollectState off the stack would remove the only
1117
+ // thing keeping the result alive.
1118
+ CollectState state{first_only ? Qnil : rb_ary_new(), Qnil,
1119
+ first_only, false, {}, 0, nullptr, false};
1120
+ Query q(std::string(RSTRING_PTR(rb_sql), RSTRING_LEN(rb_sql)));
1121
+ apply_default_settings(q, c);
1122
+ apply_read_settings(q, kwargs);
1123
+ q.OnDataCancelable([&state](const Block& block) -> bool {
1124
+ if (state.aborted) return false;
1125
+ // query_value keeps reading to leave the connection clean, but has
1126
+ // nothing left to do once it holds its cell - taking the GVL per block
1127
+ // to discard it costs a round trip each time on a large result.
1128
+ if (state.first_only && state.seen) return true;
1129
+ CollectBlockArgs ca{&block, &state};
1130
+ rb_thread_call_with_gvl(with_gvl_collect, &ca);
1131
+ return !state.aborted;
1132
+ });
1133
+ QueryNoGVL args{c->client.get(), &q, &state, nullptr, false};
1134
+
1135
+ rb_thread_call_without_gvl(query_no_gvl, &args, query_unblock, &args);
1136
+
1137
+ // A decode throw caught in the data callback is the real error; the read
1138
+ // that follows it only reports the abort, so this is checked first.
1139
+ if (state.cpp_err) {
1140
+ reset_connection_no_gvl(c->client.get());
1141
+ try { std::rethrow_exception(state.cpp_err); }
1142
+ catch (const std::exception& e) { raise_mapped_ex(e); }
994
1143
  }
995
- return Qnil;
1144
+ if (args.err) {
1145
+ if (!args.cancelled) reset_connection_no_gvl(c->client.get());
1146
+ if (state.exc_tag) rb_jump_tag(state.exc_tag);
1147
+ try { std::rethrow_exception(args.err); }
1148
+ catch (const std::exception& e) { raise_mapped_ex(e); }
1149
+ }
1150
+ if (state.exc_tag) {
1151
+ reset_connection_no_gvl(c->client.get());
1152
+ rb_jump_tag(state.exc_tag);
1153
+ }
1154
+
1155
+ return first_only ? state.out : state.rows;
1156
+ }
1157
+
1158
+ static VALUE ch_client_query(int argc, VALUE* argv, VALUE self) {
1159
+ return run_buffered_query(self, argc, argv, false);
1160
+ }
1161
+
1162
+ static VALUE ch_client_query_value(int argc, VALUE* argv, VALUE self) {
1163
+ return run_buffered_query(self, argc, argv, true);
996
1164
  }
997
1165
 
998
1166
  // ------------------------------------------------------------------
@@ -1006,6 +1174,7 @@ struct InsertNoGVL {
1006
1174
  const Block* block;
1007
1175
  const std::vector<std::pair<std::string, std::string>>* settings;
1008
1176
  std::exception_ptr err;
1177
+ bool cancelled;
1009
1178
  };
1010
1179
  } // namespace
1011
1180
 
@@ -1021,7 +1190,8 @@ static void* insert_no_gvl(void* data) {
1021
1190
 
1022
1191
  static void insert_unblock(void* data) {
1023
1192
  auto* a = static_cast<InsertNoGVL*>(data);
1024
- try { a->client->ResetConnection(); } catch (...) {}
1193
+ a->cancelled = true;
1194
+ try { a->client->CancelInFlight(); } catch (...) {}
1025
1195
  }
1026
1196
 
1027
1197
  static VALUE ch_client_insert_block(VALUE self, VALUE rb_table, VALUE rb_columns, VALUE rb_rows) {
@@ -1078,15 +1248,15 @@ static VALUE ch_client_insert_block(VALUE self, VALUE rb_table, VALUE rb_columns
1078
1248
  block.AppendColumn(names[i], cols[i]);
1079
1249
  }
1080
1250
 
1081
- InsertNoGVL args{c->client.get(), &table, &block, &c->default_settings, nullptr};
1251
+ InsertNoGVL args{c->client.get(), &table, &block, &c->default_settings, nullptr, false};
1082
1252
  rb_thread_call_without_gvl(insert_no_gvl, &args, insert_unblock, &args);
1083
1253
  if (args.err) {
1084
- try { c->client->ResetConnection(); } catch (...) {}
1254
+ if (!args.cancelled) reset_connection_no_gvl(c->client.get());
1085
1255
  try { std::rethrow_exception(args.err); }
1086
1256
  catch (const std::exception& e) { raise_mapped_ex(e); }
1087
1257
  }
1088
1258
  } catch (const std::exception& e) {
1089
- try { c->client->ResetConnection(); } catch (...) {}
1259
+ reset_connection_no_gvl(c->client.get());
1090
1260
  raise_mapped_ex(e);
1091
1261
  }
1092
1262
  return LONG2NUM(nrows);
@@ -1101,6 +1271,7 @@ struct QueryEachState {
1101
1271
  VALUE user_proc;
1102
1272
  std::vector<ID> col_ids;
1103
1273
  int exc_tag;
1274
+ std::exception_ptr cpp_err;
1104
1275
  bool aborted;
1105
1276
  };
1106
1277
 
@@ -1114,30 +1285,39 @@ struct QueryEachNoGVL {
1114
1285
  const Query* query;
1115
1286
  QueryEachState* state;
1116
1287
  std::exception_ptr err;
1288
+ bool cancelled;
1117
1289
  };
1118
1290
  } // namespace
1119
1291
 
1292
+ // See collect_rows_body: the decode throw is caught inside this frame so it
1293
+ // never unwinds through rb_protect.
1120
1294
  static VALUE yield_rows_body(VALUE arg) {
1121
1295
  auto* args = reinterpret_cast<YieldBlockArgs*>(arg);
1122
1296
  const Block& block = *args->block;
1123
1297
  auto* state = args->state;
1124
- size_t ncols = block.GetColumnCount();
1125
- size_t nrows = block.GetRowCount();
1126
- if (nrows == 0) return Qnil;
1127
- if (state->col_ids.empty() && ncols > 0) {
1128
- state->col_ids.reserve(ncols);
1129
- for (size_t i = 0; i < ncols; i++) {
1130
- const std::string& name = block.GetColumnName(i);
1131
- state->col_ids.push_back(rb_intern2(name.data(), name.size()));
1298
+
1299
+ try {
1300
+ size_t ncols = block.GetColumnCount();
1301
+ size_t nrows = block.GetRowCount();
1302
+ if (nrows == 0) return Qnil;
1303
+ if (state->col_ids.empty() && ncols > 0) {
1304
+ state->col_ids.reserve(ncols);
1305
+ for (size_t i = 0; i < ncols; i++) {
1306
+ const std::string& name = block.GetColumnName(i);
1307
+ state->col_ids.push_back(rb_intern2(name.data(), name.size()));
1308
+ }
1132
1309
  }
1133
- }
1134
- for (size_t r = 0; r < nrows; r++) {
1135
- VALUE h = rb_hash_new();
1136
- for (size_t cc = 0; cc < ncols; cc++) {
1137
- rb_hash_aset(h, ID2SYM(state->col_ids[cc]),
1138
- value_at(block[cc], r, block.GetColumnType(cc)));
1310
+ for (size_t r = 0; r < nrows; r++) {
1311
+ VALUE h = rb_hash_new();
1312
+ for (size_t cc = 0; cc < ncols; cc++) {
1313
+ rb_hash_aset(h, ID2SYM(state->col_ids[cc]),
1314
+ value_at(block[cc], r, block.GetColumnType(cc)));
1315
+ }
1316
+ rb_funcall(state->user_proc, rb_intern("call"), 1, h);
1139
1317
  }
1140
- rb_funcall(state->user_proc, rb_intern("call"), 1, h);
1318
+ } catch (...) {
1319
+ state->cpp_err = std::current_exception();
1320
+ state->aborted = true;
1141
1321
  }
1142
1322
  return Qnil;
1143
1323
  }
@@ -1166,7 +1346,8 @@ static void* query_each_no_gvl(void* data) {
1166
1346
  static void query_each_unblock(void* data) {
1167
1347
  auto* a = static_cast<QueryEachNoGVL*>(data);
1168
1348
  a->state->aborted = true;
1169
- try { a->client->ResetConnection(); } catch (...) {}
1349
+ a->cancelled = true;
1350
+ try { a->client->CancelInFlight(); } catch (...) {}
1170
1351
  }
1171
1352
 
1172
1353
  static VALUE ch_client_query_each(int argc, VALUE* argv, VALUE self) {
@@ -1177,7 +1358,7 @@ static VALUE ch_client_query_each(int argc, VALUE* argv, VALUE self) {
1177
1358
  CHClient* c = as_client(self);
1178
1359
  if (!c->client) rb_raise(err_connection, "clickhouse-native: client is closed");
1179
1360
 
1180
- QueryEachState state{rb_block_proc(), {}, 0, false};
1361
+ QueryEachState state{rb_block_proc(), {}, 0, nullptr, false};
1181
1362
  Query q(std::string(RSTRING_PTR(rb_sql), RSTRING_LEN(rb_sql)));
1182
1363
  apply_default_settings(q, c);
1183
1364
  apply_read_settings(q, kwargs);
@@ -1187,18 +1368,25 @@ static VALUE ch_client_query_each(int argc, VALUE* argv, VALUE self) {
1187
1368
  rb_thread_call_with_gvl(with_gvl_yield, &ya);
1188
1369
  return !state.aborted;
1189
1370
  });
1190
- QueryEachNoGVL args{c->client.get(), &q, &state, nullptr};
1371
+ QueryEachNoGVL args{c->client.get(), &q, &state, nullptr, false};
1191
1372
 
1192
1373
  rb_thread_call_without_gvl(query_each_no_gvl, &args, query_each_unblock, &args);
1193
1374
 
1375
+ // A decode throw caught in the data callback is the real error; the read
1376
+ // that follows it only reports the abort, so this is checked first.
1377
+ if (state.cpp_err) {
1378
+ reset_connection_no_gvl(c->client.get());
1379
+ try { std::rethrow_exception(state.cpp_err); }
1380
+ catch (const std::exception& e) { raise_mapped_ex(e); }
1381
+ }
1194
1382
  if (args.err) {
1195
- try { c->client->ResetConnection(); } catch (...) {}
1383
+ if (!args.cancelled) reset_connection_no_gvl(c->client.get());
1196
1384
  if (state.exc_tag) rb_jump_tag(state.exc_tag);
1197
1385
  try { std::rethrow_exception(args.err); }
1198
1386
  catch (const std::exception& e) { raise_mapped_ex(e); }
1199
1387
  }
1200
1388
  if (state.exc_tag) {
1201
- try { c->client->ResetConnection(); } catch (...) {}
1389
+ reset_connection_no_gvl(c->client.get());
1202
1390
  rb_jump_tag(state.exc_tag);
1203
1391
  }
1204
1392
  return self;
@@ -1253,7 +1441,7 @@ static VALUE ch_client_server_version(VALUE self) {
1253
1441
  static VALUE ch_client_reset_connection(VALUE self) {
1254
1442
  CHClient* c = as_client(self);
1255
1443
  if (!c->client) return Qnil;
1256
- try { c->client->ResetConnection(); } catch (...) {}
1444
+ reset_connection_no_gvl(c->client.get());
1257
1445
  return Qtrue;
1258
1446
  }
1259
1447
 
@@ -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.12.0"
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.12.0
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