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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4bbe836a1758637fb9dd0a157505037b8bc45d42d253d261b3a349cecaf9716
|
|
4
|
+
data.tar.gz: 7c0cd5bbea6944c2c0672780088d4342cdf207227bce2c52f388986e9b26bf15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() —
|
|
921
|
-
//
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
#
|
|
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
|
|
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
|
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.
|
|
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
|