@clickhouse/client 1.18.0 → 1.18.2-head.084b623.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.
@@ -6,37 +6,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.NodeBaseConnection = void 0;
7
7
  const client_common_1 = require("@clickhouse/client-common");
8
8
  const crypto_1 = __importDefault(require("crypto"));
9
- const stream_1 = __importDefault(require("stream"));
10
- const zlib_1 = __importDefault(require("zlib"));
11
9
  const utils_1 = require("../utils");
12
- const compression_1 = require("./compression");
13
- const stream_2 = require("./stream");
10
+ const stream_1 = require("./stream");
11
+ const socket_pool_1 = require("./socket_pool");
14
12
  class NodeBaseConnection {
15
13
  params;
16
14
  agent;
17
15
  defaultAuthHeader;
18
16
  defaultHeaders;
19
- jsonHandling;
20
- knownSockets = new WeakMap();
21
- idleSocketTTL;
22
17
  connectionId = crypto_1.default.randomUUID();
23
- socketCounter = 0;
24
- // For overflow concerns:
25
- // node -e 'console.log(Number.MAX_SAFE_INTEGER / (1_000_000 * 60 * 60 * 24 * 366))'
26
- // gives 284 years of continuous operation at 1M requests per second
27
- // before overflowing the 53-bit integer
28
- requestCounter = 0;
29
- getNewSocketId() {
30
- this.socketCounter += 1;
31
- return `${this.connectionId}:${this.socketCounter}`;
32
- }
33
- getNewRequestId() {
34
- this.requestCounter += 1;
35
- return `${this.connectionId}:${this.requestCounter}`;
36
- }
18
+ socketPool;
37
19
  constructor(params, agent) {
38
20
  this.params = params;
39
21
  this.agent = agent;
22
+ this.socketPool = new socket_pool_1.SocketPool(this.connectionId, this.params, this.createClientRequest.bind(this), this.agent);
40
23
  if (params.auth.type === 'Credentials') {
41
24
  this.defaultAuthHeader = `Basic ${Buffer.from(`${params.auth.username}:${params.auth.password}`).toString('base64')}`;
42
25
  }
@@ -51,18 +34,13 @@ class NodeBaseConnection {
51
34
  Connection: this.params.keep_alive.enabled ? 'keep-alive' : 'close',
52
35
  'User-Agent': (0, utils_1.getUserAgent)(this.params.application_id),
53
36
  };
54
- this.idleSocketTTL = params.keep_alive.idle_socket_ttl;
55
- this.jsonHandling = params.json ?? {
56
- parse: JSON.parse,
57
- stringify: JSON.stringify,
58
- };
59
37
  }
60
38
  async ping(params) {
61
39
  const { log_writer, log_level } = this.params;
62
40
  const query_id = this.getQueryId(params.query_id);
63
41
  const { controller, controllerCleanup } = this.getAbortController(params);
64
- let result;
65
42
  try {
43
+ let result;
66
44
  if (params.select) {
67
45
  const searchParams = (0, client_common_1.toSearchParams)({
68
46
  database: undefined,
@@ -70,9 +48,9 @@ class NodeBaseConnection {
70
48
  query_id,
71
49
  });
72
50
  result = await this.request({
51
+ query: PingQuery,
73
52
  method: 'GET',
74
53
  url: (0, client_common_1.transformUrl)({ url: this.params.url, searchParams }),
75
- query: PingQuery,
76
54
  abort_signal: controller.signal,
77
55
  headers: this.buildRequestHeaders(),
78
56
  query_id,
@@ -82,17 +60,17 @@ class NodeBaseConnection {
82
60
  }
83
61
  else {
84
62
  result = await this.request({
63
+ query: 'ping',
85
64
  method: 'GET',
86
65
  url: (0, client_common_1.transformUrl)({ url: this.params.url, pathname: '/ping' }),
87
66
  abort_signal: controller.signal,
88
67
  headers: this.buildRequestHeaders(),
89
- query: 'ping',
90
68
  query_id,
91
69
  log_writer,
92
70
  log_level,
93
71
  }, 'Ping');
94
72
  }
95
- await (0, stream_2.drainStream)({
73
+ await (0, stream_1.drainStreamInternal)({
96
74
  op: 'Ping',
97
75
  log_writer,
98
76
  query_id,
@@ -205,7 +183,7 @@ class NodeBaseConnection {
205
183
  log_writer,
206
184
  log_level,
207
185
  }, 'Insert');
208
- await (0, stream_2.drainStream)({
186
+ await (0, stream_1.drainStreamInternal)({
209
187
  op: 'Insert',
210
188
  log_writer,
211
189
  query_id,
@@ -250,9 +228,6 @@ class NodeBaseConnection {
250
228
  operation: 'Command',
251
229
  connection_id: this.connectionId,
252
230
  query_id,
253
- query: this.params.unsafeLogUnredactedQueries
254
- ? params.query
255
- : undefined,
256
231
  },
257
232
  });
258
233
  }
@@ -280,7 +255,7 @@ class NodeBaseConnection {
280
255
  }
281
256
  // ignore the response stream and release the socket immediately
282
257
  const drainStartTime = Date.now();
283
- await (0, stream_2.drainStream)({
258
+ await (0, stream_1.drainStreamInternal)({
284
259
  op: 'Command',
285
260
  log_writer,
286
261
  query_id,
@@ -362,14 +337,8 @@ class NodeBaseConnection {
362
337
  },
363
338
  };
364
339
  }
365
- logRequestError({ op, err, query_id, query_params, search_params, extra_args, }) {
340
+ logRequestError({ op, err, query_id, query_params, extra_args, }) {
366
341
  if (this.params.log_level <= client_common_1.ClickHouseLogLevel.ERROR) {
367
- // Redact query parameter from search params unless explicitly allowed
368
- if (!this.params.unsafeLogUnredactedQueries && search_params) {
369
- // Clone to avoid mutating the original search params
370
- search_params = new URLSearchParams(search_params);
371
- search_params.delete('query');
372
- }
373
342
  this.params.log_writer.error({
374
343
  message: this.httpRequestErrorMessage(op),
375
344
  err: err,
@@ -377,10 +346,6 @@ class NodeBaseConnection {
377
346
  operation: op,
378
347
  connection_id: this.connectionId,
379
348
  query_id,
380
- query: this.params.unsafeLogUnredactedQueries
381
- ? query_params.query
382
- : undefined,
383
- search_params: search_params?.toString(),
384
349
  with_abort_signal: query_params.abort_signal !== undefined,
385
350
  session_id: query_params.session_id,
386
351
  ...extra_args,
@@ -391,27 +356,6 @@ class NodeBaseConnection {
391
356
  httpRequestErrorMessage(op) {
392
357
  return `${op}: HTTP request error.`;
393
358
  }
394
- parseSummary(op, response) {
395
- const summaryHeader = response.headers['x-clickhouse-summary'];
396
- if (typeof summaryHeader === 'string') {
397
- try {
398
- return this.jsonHandling.parse(summaryHeader);
399
- }
400
- catch (err) {
401
- if (this.params.log_level <= client_common_1.ClickHouseLogLevel.ERROR) {
402
- this.params.log_writer.error({
403
- message: `${op}: failed to parse X-ClickHouse-Summary header.`,
404
- args: {
405
- operation: op,
406
- connection_id: this.connectionId,
407
- 'X-ClickHouse-Summary': summaryHeader,
408
- },
409
- err: err,
410
- });
411
- }
412
- }
413
- }
414
- }
415
359
  async runExec(params) {
416
360
  const { log_writer, log_level } = this.params;
417
361
  const query_id = params.query_id;
@@ -480,425 +424,7 @@ class NodeBaseConnection {
480
424
  }
481
425
  }
482
426
  async request(params, op) {
483
- // allows the event loop to process the idle socket timers, if the CPU load is high
484
- // otherwise, we can occasionally get an expired socket, see https://github.com/ClickHouse/clickhouse-js/issues/294
485
- await (0, client_common_1.sleep)(0);
486
- const { log_writer, query_id, log_level } = params;
487
- const currentStackTrace = this.params.capture_enhanced_stack_trace
488
- ? (0, client_common_1.getCurrentStackTrace)()
489
- : undefined;
490
- const requestTimeout = this.params.request_timeout;
491
- return new Promise((resolve, reject) => {
492
- const start = Date.now();
493
- const request = this.createClientRequest(params);
494
- const request_id = this.getNewRequestId();
495
- function onError(e) {
496
- removeRequestListeners();
497
- const err = (0, client_common_1.enhanceStackTrace)(e, currentStackTrace);
498
- reject(err);
499
- }
500
- let responseStream;
501
- const onResponse = async (_response) => {
502
- if (this.params.log_level <= client_common_1.ClickHouseLogLevel.DEBUG) {
503
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
504
- const { authorization, host, ...headers } = request.getHeaders();
505
- const duration = Date.now() - start;
506
- // Redact query parameter from URL search params unless explicitly allowed
507
- let searchParams = params.url.searchParams;
508
- if (!this.params.unsafeLogUnredactedQueries) {
509
- // Clone to avoid mutating the original search params
510
- searchParams = new URLSearchParams(searchParams);
511
- searchParams.delete('query');
512
- }
513
- this.params.log_writer.debug({
514
- module: 'HTTP Adapter',
515
- message: `${op}: got a response from ClickHouse`,
516
- args: {
517
- operation: op,
518
- connection_id: this.connectionId,
519
- query_id,
520
- request_id,
521
- request_method: params.method,
522
- request_path: params.url.pathname,
523
- request_params: searchParams.toString(),
524
- request_headers: headers,
525
- response_status: _response.statusCode,
526
- response_headers: _response.headers,
527
- response_time_ms: duration,
528
- },
529
- });
530
- }
531
- const tryDecompressResponseStream = params.try_decompress_response_stream ?? true;
532
- const ignoreErrorResponse = params.ignore_error_response ?? false;
533
- // even if the stream decompression is disabled, we have to decompress it in case of an error
534
- const isFailedResponse = !(0, client_common_1.isSuccessfulResponse)(_response.statusCode);
535
- if (tryDecompressResponseStream ||
536
- (isFailedResponse && !ignoreErrorResponse)) {
537
- const decompressionResult = (0, compression_1.decompressResponse)(_response, log_writer, log_level);
538
- if ((0, compression_1.isDecompressionError)(decompressionResult)) {
539
- const err = (0, client_common_1.enhanceStackTrace)(decompressionResult.error, currentStackTrace);
540
- return reject(err);
541
- }
542
- responseStream = decompressionResult.response;
543
- }
544
- else {
545
- responseStream = _response;
546
- }
547
- if (log_level <= client_common_1.ClickHouseLogLevel.TRACE) {
548
- log_writer.trace({
549
- message: `${op}: response stream created`,
550
- args: {
551
- operation: op,
552
- connection_id: this.connectionId,
553
- query_id,
554
- request_id,
555
- stream_state: {
556
- readable: responseStream.readable,
557
- readableEnded: responseStream.readableEnded,
558
- readableLength: responseStream.readableLength,
559
- },
560
- is_failed_response: isFailedResponse,
561
- will_decompress: tryDecompressResponseStream,
562
- },
563
- });
564
- }
565
- if (isFailedResponse && !ignoreErrorResponse) {
566
- try {
567
- const errorMessage = await (0, utils_1.getAsText)(responseStream);
568
- const err = (0, client_common_1.enhanceStackTrace)((0, client_common_1.parseError)(errorMessage), currentStackTrace);
569
- reject(err);
570
- }
571
- catch (e) {
572
- // If the ClickHouse response is malformed
573
- const err = (0, client_common_1.enhanceStackTrace)(e, currentStackTrace);
574
- reject(err);
575
- }
576
- }
577
- else {
578
- return resolve({
579
- stream: responseStream,
580
- summary: params.parse_summary
581
- ? this.parseSummary(op, _response)
582
- : undefined,
583
- response_headers: { ..._response.headers },
584
- http_status_code: _response.statusCode ?? undefined,
585
- });
586
- }
587
- };
588
- function onAbort() {
589
- // Prefer 'abort' event since it always triggered unlike 'error' and 'close'
590
- // see the full sequence of events https://nodejs.org/api/http.html#httprequesturl-options-callback
591
- removeRequestListeners();
592
- request.once('error', function () {
593
- /**
594
- * catch "Error: ECONNRESET" error which shouldn't be reported to users.
595
- * see the full sequence of events https://nodejs.org/api/http.html#httprequesturl-options-callback
596
- * */
597
- });
598
- const err = (0, client_common_1.enhanceStackTrace)(new Error('The user aborted a request.'), currentStackTrace);
599
- reject(err);
600
- }
601
- function onClose() {
602
- // Adapter uses 'close' event to clean up listeners after the successful response.
603
- // It's necessary in order to handle 'abort' and 'timeout' events while response is streamed.
604
- // It's always the last event, according to https://nodejs.org/docs/latest-v14.x/api/http.html#http_http_request_url_options_callback
605
- removeRequestListeners();
606
- }
607
- function pipeStream() {
608
- // if request.end() was called due to no data to send
609
- if (request.writableEnded) {
610
- return;
611
- }
612
- const bodyStream = (0, utils_1.isStream)(params.body)
613
- ? params.body
614
- : stream_1.default.Readable.from([params.body]);
615
- const callback = (e) => {
616
- if (e) {
617
- removeRequestListeners();
618
- const err = (0, client_common_1.enhanceStackTrace)(e, currentStackTrace);
619
- reject(err);
620
- }
621
- };
622
- if (params.enable_request_compression) {
623
- stream_1.default.pipeline(bodyStream, zlib_1.default.createGzip(), request, callback);
624
- }
625
- else {
626
- stream_1.default.pipeline(bodyStream, request, callback);
627
- }
628
- }
629
- const onSocket = (socket) => {
630
- try {
631
- if (this.params.keep_alive.enabled &&
632
- this.params.keep_alive.idle_socket_ttl > 0) {
633
- const socketInfo = this.knownSockets.get(socket);
634
- // It is the first time we've encountered this socket,
635
- // so it doesn't have the idle timeout handler attached to it
636
- if (socketInfo === undefined) {
637
- const socket_id = this.getNewSocketId();
638
- if (log_level <= client_common_1.ClickHouseLogLevel.TRACE) {
639
- log_writer.trace({
640
- message: `${op}: using a fresh socket, setting up a new 'free' listener`,
641
- args: {
642
- operation: op,
643
- connection_id: this.connectionId,
644
- query_id,
645
- request_id,
646
- socket_id,
647
- },
648
- });
649
- }
650
- const newSocketInfo = {
651
- id: socket_id,
652
- idle_timeout_handle: undefined,
653
- usage_count: 1,
654
- };
655
- this.knownSockets.set(socket, newSocketInfo);
656
- // When the request is complete and the socket is released,
657
- // make sure that the socket is removed after `idleSocketTTL`.
658
- socket.on('free', () => {
659
- if (log_level <= client_common_1.ClickHouseLogLevel.TRACE) {
660
- log_writer.trace({
661
- message: `${op}: socket was released`,
662
- args: {
663
- operation: op,
664
- connection_id: this.connectionId,
665
- query_id,
666
- request_id,
667
- socket_id,
668
- },
669
- });
670
- }
671
- // Avoiding the built-in socket.timeout() method usage here,
672
- // as we don't want to clash with the actual request timeout.
673
- const idleTimeoutHandle = setTimeout(() => {
674
- if (log_level <= client_common_1.ClickHouseLogLevel.TRACE) {
675
- log_writer.trace({
676
- message: `${op}: removing idle socket`,
677
- args: {
678
- operation: op,
679
- connection_id: this.connectionId,
680
- query_id,
681
- request_id,
682
- socket_id,
683
- idle_socket_ttl_ms: this.idleSocketTTL,
684
- },
685
- });
686
- }
687
- this.knownSockets.delete(socket);
688
- socket.destroy();
689
- }, this.idleSocketTTL).unref();
690
- newSocketInfo.idle_timeout_handle = idleTimeoutHandle;
691
- });
692
- const cleanup = (eventName) => () => {
693
- const maybeSocketInfo = this.knownSockets.get(socket);
694
- // clean up a possibly dangling idle timeout handle (preventing leaks)
695
- if (maybeSocketInfo?.idle_timeout_handle) {
696
- clearTimeout(maybeSocketInfo.idle_timeout_handle);
697
- }
698
- if (log_level <= client_common_1.ClickHouseLogLevel.TRACE) {
699
- log_writer.trace({
700
- message: `${op}: received '${eventName}' event, 'free' listener removed`,
701
- args: {
702
- operation: op,
703
- connection_id: this.connectionId,
704
- query_id,
705
- request_id,
706
- socket_id,
707
- event: eventName,
708
- },
709
- });
710
- }
711
- if (log_level <= client_common_1.ClickHouseLogLevel.WARN) {
712
- if (responseStream && !responseStream.readableEnded) {
713
- log_writer.warn({
714
- message: `${op}: socket was closed or ended before the response was fully read. ` +
715
- 'This can potentially result in an uncaught ECONNRESET error! ' +
716
- 'Consider fully consuming, draining, or destroying the response stream.',
717
- args: {
718
- operation: op,
719
- connection_id: this.connectionId,
720
- query_id,
721
- request_id,
722
- socket_id,
723
- event: eventName,
724
- query: this.params.unsafeLogUnredactedQueries
725
- ? params.query
726
- : undefined,
727
- },
728
- });
729
- }
730
- }
731
- };
732
- socket.once('end', cleanup('end'));
733
- socket.once('close', cleanup('close'));
734
- }
735
- else {
736
- clearTimeout(socketInfo.idle_timeout_handle);
737
- socketInfo.idle_timeout_handle = undefined;
738
- if (log_level <= client_common_1.ClickHouseLogLevel.TRACE) {
739
- log_writer.trace({
740
- message: `${op}: reusing socket`,
741
- args: {
742
- operation: op,
743
- connection_id: this.connectionId,
744
- query_id,
745
- request_id,
746
- socket_id: socketInfo.id,
747
- usage_count: socketInfo.usage_count,
748
- },
749
- });
750
- }
751
- socketInfo.usage_count++;
752
- }
753
- }
754
- }
755
- catch (e) {
756
- if (log_level <= client_common_1.ClickHouseLogLevel.ERROR) {
757
- log_writer.error({
758
- message: `${op}: an error occurred while housekeeping the idle sockets`,
759
- err: e,
760
- args: {
761
- operation: op,
762
- connection_id: this.connectionId,
763
- query_id,
764
- request_id,
765
- },
766
- });
767
- }
768
- }
769
- // Socket is "prepared" with idle handlers, continue with our request
770
- pipeStream();
771
- // This is for request timeout only. Surprisingly, it is not always enough to set in the HTTP request.
772
- // The socket won't be destroyed, and it will be returned to the pool.
773
- if (log_level <= client_common_1.ClickHouseLogLevel.TRACE) {
774
- const socketInfo = this.knownSockets.get(socket);
775
- if (socketInfo) {
776
- log_writer.trace({
777
- message: `${op}: setting up request timeout`,
778
- args: {
779
- operation: op,
780
- connection_id: this.connectionId,
781
- query_id,
782
- request_id,
783
- socket_id: socketInfo.id,
784
- timeout_ms: requestTimeout,
785
- },
786
- });
787
- }
788
- else {
789
- log_writer.trace({
790
- message: `${op}: setting up request timeout on a socket`,
791
- args: {
792
- operation: op,
793
- connection_id: this.connectionId,
794
- query_id,
795
- request_id,
796
- timeout_ms: requestTimeout,
797
- },
798
- });
799
- }
800
- }
801
- socket.setTimeout(this.params.request_timeout, onTimeout);
802
- };
803
- const onTimeout = () => {
804
- removeRequestListeners();
805
- if (log_level <= client_common_1.ClickHouseLogLevel.TRACE) {
806
- const socket = request.socket;
807
- const maybeSocketInfo = socket
808
- ? this.knownSockets.get(socket)
809
- : undefined;
810
- const socketState = request.socket
811
- ? {
812
- connecting: request.socket.connecting,
813
- pending: request.socket.pending,
814
- destroyed: request.socket.destroyed,
815
- readyState: request.socket.readyState,
816
- }
817
- : undefined;
818
- const responseStreamState = responseStream
819
- ? {
820
- readable: responseStream.readable,
821
- readableEnded: responseStream.readableEnded,
822
- readableLength: responseStream.readableLength,
823
- }
824
- : undefined;
825
- log_writer.trace({
826
- message: `${op}: timeout occurred`,
827
- args: {
828
- operation: op,
829
- connection_id: this.connectionId,
830
- query_id,
831
- request_id,
832
- socket_id: maybeSocketInfo?.id,
833
- timeout_ms: requestTimeout,
834
- socket_state: socketState,
835
- response_stream_state: responseStreamState,
836
- has_response_stream: responseStream !== undefined,
837
- },
838
- });
839
- }
840
- const err = (0, client_common_1.enhanceStackTrace)(new Error('Timeout error.'), currentStackTrace);
841
- try {
842
- request.destroy();
843
- }
844
- catch (e) {
845
- if (log_level <= client_common_1.ClickHouseLogLevel.ERROR) {
846
- log_writer.error({
847
- message: `${op}: An error occurred while destroying the request`,
848
- err: e,
849
- args: {
850
- operation: op,
851
- connection_id: this.connectionId,
852
- query_id,
853
- request_id,
854
- },
855
- });
856
- }
857
- }
858
- reject(err);
859
- };
860
- function removeRequestListeners() {
861
- if (request.socket !== null) {
862
- request.socket.setTimeout(0); // reset previously set timeout
863
- request.socket.removeListener('timeout', onTimeout);
864
- }
865
- request.removeListener('socket', onSocket);
866
- request.removeListener('response', onResponse);
867
- request.removeListener('error', onError);
868
- request.removeListener('close', onClose);
869
- if (params.abort_signal !== undefined) {
870
- request.removeListener('abort', onAbort);
871
- }
872
- }
873
- request.on('socket', onSocket);
874
- request.on('response', onResponse);
875
- request.on('error', onError);
876
- request.on('close', onClose);
877
- if (params.abort_signal !== undefined) {
878
- params.abort_signal.addEventListener('abort', onAbort, {
879
- once: true,
880
- });
881
- }
882
- if (!params.body) {
883
- try {
884
- return request.end();
885
- }
886
- catch (e) {
887
- if (log_level <= client_common_1.ClickHouseLogLevel.ERROR) {
888
- log_writer.error({
889
- message: `${op}: an error occurred while ending the request without body`,
890
- err: e,
891
- args: {
892
- operation: op,
893
- connection_id: this.connectionId,
894
- query_id,
895
- request_id,
896
- },
897
- });
898
- }
899
- }
900
- }
901
- });
427
+ return this.socketPool.request(params, op);
902
428
  }
903
429
  }
904
430
  exports.NodeBaseConnection = NodeBaseConnection;