pg 1.3.5 → 1.4.4

Sign up to get free protection for your applications and to get access to all the features.
data/ext/pg_copy_coder.c CHANGED
@@ -592,7 +592,7 @@ pg_text_dec_copy_row(t_pg_coder *conv, const char *input_line, int len, int _tup
592
592
 
593
593
 
594
594
  void
595
- init_pg_copycoder()
595
+ init_pg_copycoder(void)
596
596
  {
597
597
  /* Document-class: PG::CopyCoder < PG::Coder
598
598
  *
data/ext/pg_errors.c CHANGED
@@ -70,7 +70,7 @@ lookup_error_class(const char *sqlstate)
70
70
  }
71
71
 
72
72
  void
73
- init_pg_errors()
73
+ init_pg_errors(void)
74
74
  {
75
75
  rb_hErrors = rb_hash_new();
76
76
  rb_define_const( rb_mPG, "ERROR_CLASSES", rb_hErrors );
@@ -494,7 +494,7 @@ pg_text_dec_record(t_pg_coder *conv, char *input_line, int len, int _tuple, int
494
494
 
495
495
 
496
496
  void
497
- init_pg_recordcoder()
497
+ init_pg_recordcoder(void)
498
498
  {
499
499
  /* Document-class: PG::RecordCoder < PG::Coder
500
500
  *
data/ext/pg_result.c CHANGED
@@ -1382,21 +1382,20 @@ pgresult_type_map_get(VALUE self)
1382
1382
  }
1383
1383
 
1384
1384
 
1385
- static void
1385
+ static int
1386
1386
  yield_hash(VALUE self, int ntuples, int nfields, void *data)
1387
1387
  {
1388
1388
  int tuple_num;
1389
- t_pg_result *this = pgresult_get_this(self);
1390
1389
  UNUSED(nfields);
1391
1390
 
1392
1391
  for(tuple_num = 0; tuple_num < ntuples; tuple_num++) {
1393
1392
  rb_yield(pgresult_aref(self, INT2NUM(tuple_num)));
1394
1393
  }
1395
1394
 
1396
- pgresult_clear( this );
1395
+ return 1; /* clear the result */
1397
1396
  }
1398
1397
 
1399
- static void
1398
+ static int
1400
1399
  yield_array(VALUE self, int ntuples, int nfields, void *data)
1401
1400
  {
1402
1401
  int row;
@@ -1413,10 +1412,10 @@ yield_array(VALUE self, int ntuples, int nfields, void *data)
1413
1412
  rb_yield( rb_ary_new4( nfields, row_values ));
1414
1413
  }
1415
1414
 
1416
- pgresult_clear( this );
1415
+ return 1; /* clear the result */
1417
1416
  }
1418
1417
 
1419
- static void
1418
+ static int
1420
1419
  yield_tuple(VALUE self, int ntuples, int nfields, void *data)
1421
1420
  {
1422
1421
  int tuple_num;
@@ -1434,11 +1433,12 @@ yield_tuple(VALUE self, int ntuples, int nfields, void *data)
1434
1433
  VALUE tuple = pgresult_tuple(copy, INT2FIX(tuple_num));
1435
1434
  rb_yield( tuple );
1436
1435
  }
1436
+ return 0; /* don't clear the result */
1437
1437
  }
1438
1438
 
1439
1439
  /* Non-static, and data pointer for use by sequel_pg */
1440
1440
  VALUE
1441
- pgresult_stream_any(VALUE self, void (*yielder)(VALUE, int, int, void*), void* data)
1441
+ pgresult_stream_any(VALUE self, int (*yielder)(VALUE, int, int, void*), void* data)
1442
1442
  {
1443
1443
  t_pg_result *this;
1444
1444
  int nfields;
@@ -1467,7 +1467,9 @@ pgresult_stream_any(VALUE self, void (*yielder)(VALUE, int, int, void*), void* d
1467
1467
  pg_result_check( self );
1468
1468
  }
1469
1469
 
1470
- yielder( self, ntuples, nfields, data );
1470
+ if( yielder( self, ntuples, nfields, data ) ){
1471
+ pgresult_clear( this );
1472
+ }
1471
1473
 
1472
1474
  if( gvl_PQisBusy(pgconn) ){
1473
1475
  /* wait for input (without blocking) before reading each result */
@@ -1476,10 +1478,10 @@ pgresult_stream_any(VALUE self, void (*yielder)(VALUE, int, int, void*), void* d
1476
1478
 
1477
1479
  pgresult = gvl_PQgetResult(pgconn);
1478
1480
  if( pgresult == NULL )
1479
- rb_raise( rb_eNoResultError, "no result received - possibly an intersection with another result retrieval");
1481
+ rb_raise( rb_eNoResultError, "no result received - possibly an intersection with another query");
1480
1482
 
1481
1483
  if( nfields != PQnfields(pgresult) )
1482
- rb_raise( rb_eInvalidChangeOfResultFields, "number of fields must not change in single row mode");
1484
+ rb_raise( rb_eInvalidChangeOfResultFields, "number of fields changed in single row mode from %d to %d - this is a sign for intersection with another query", nfields, PQnfields(pgresult));
1483
1485
 
1484
1486
  this->pgresult = pgresult;
1485
1487
  }
@@ -1617,7 +1619,7 @@ pgresult_field_name_type_get(VALUE self)
1617
1619
  }
1618
1620
 
1619
1621
  void
1620
- init_pg_result()
1622
+ init_pg_result(void)
1621
1623
  {
1622
1624
  sym_string = ID2SYM(rb_intern("string"));
1623
1625
  sym_symbol = ID2SYM(rb_intern("symbol"));
@@ -923,7 +923,7 @@ pg_text_dec_inet(t_pg_coder *conv, const char *val, int len, int tuple, int fiel
923
923
  }
924
924
 
925
925
  void
926
- init_pg_text_decoder()
926
+ init_pg_text_decoder(void)
927
927
  {
928
928
  rb_require("ipaddr");
929
929
  s_IPAddr = rb_funcall(rb_cObject, rb_intern("const_get"), 1, rb_str_new2("IPAddr"));
@@ -775,7 +775,7 @@ pg_text_enc_to_base64(t_pg_coder *conv, VALUE value, char *out, VALUE *intermedi
775
775
 
776
776
 
777
777
  void
778
- init_pg_text_encoder()
778
+ init_pg_text_encoder(void)
779
779
  {
780
780
  s_id_encode = rb_intern("encode");
781
781
  s_id_to_i = rb_intern("to_i");
data/ext/pg_tuple.c CHANGED
@@ -545,7 +545,7 @@ pg_tuple_load(VALUE self, VALUE a)
545
545
  }
546
546
 
547
547
  void
548
- init_pg_tuple()
548
+ init_pg_tuple(void)
549
549
  {
550
550
  rb_cPG_Tuple = rb_define_class_under( rb_mPG, "Tuple", rb_cObject );
551
551
  rb_define_alloc_func( rb_cPG_Tuple, pg_tuple_s_allocate );
data/ext/pg_type_map.c CHANGED
@@ -176,7 +176,7 @@ pg_typemap_with_default_type_map(VALUE self, VALUE typemap)
176
176
  }
177
177
 
178
178
  void
179
- init_pg_type_map()
179
+ init_pg_type_map(void)
180
180
  {
181
181
  s_id_fit_to_query = rb_intern("fit_to_query");
182
182
  s_id_fit_to_result = rb_intern("fit_to_result");
@@ -105,7 +105,7 @@ pg_tmas_s_allocate( VALUE klass )
105
105
 
106
106
 
107
107
  void
108
- init_pg_type_map_all_strings()
108
+ init_pg_type_map_all_strings(void)
109
109
  {
110
110
  /*
111
111
  * Document-class: PG::TypeMapAllStrings < PG::TypeMap
@@ -247,7 +247,7 @@ pg_tmbk_coders( VALUE self )
247
247
  }
248
248
 
249
249
  void
250
- init_pg_type_map_by_class()
250
+ init_pg_type_map_by_class(void)
251
251
  {
252
252
  /*
253
253
  * Document-class: PG::TypeMapByClass < PG::TypeMap
@@ -243,7 +243,7 @@ pg_tmbc_s_allocate( VALUE klass )
243
243
  }
244
244
 
245
245
  VALUE
246
- pg_tmbc_allocate()
246
+ pg_tmbc_allocate(void)
247
247
  {
248
248
  return pg_tmbc_s_allocate(rb_cTypeMapByColumn);
249
249
  }
@@ -320,7 +320,7 @@ pg_tmbc_coders(VALUE self)
320
320
  }
321
321
 
322
322
  void
323
- init_pg_type_map_by_column()
323
+ init_pg_type_map_by_column(void)
324
324
  {
325
325
  s_id_decode = rb_intern("decode");
326
326
  s_id_encode = rb_intern("encode");
@@ -286,7 +286,7 @@ pg_tmbmt_coders( VALUE self )
286
286
  }
287
287
 
288
288
  void
289
- init_pg_type_map_by_mri_type()
289
+ init_pg_type_map_by_mri_type(void)
290
290
  {
291
291
  /*
292
292
  * Document-class: PG::TypeMapByMriType < PG::TypeMap
@@ -356,7 +356,7 @@ pg_tmbo_build_column_map( VALUE self, VALUE result )
356
356
 
357
357
 
358
358
  void
359
- init_pg_type_map_by_oid()
359
+ init_pg_type_map_by_oid(void)
360
360
  {
361
361
  s_id_decode = rb_intern("decode");
362
362
 
@@ -299,7 +299,7 @@ pg_tmir_s_allocate( VALUE klass )
299
299
 
300
300
 
301
301
  void
302
- init_pg_type_map_in_ruby()
302
+ init_pg_type_map_in_ruby(void)
303
303
  {
304
304
  s_id_fit_to_result = rb_intern("fit_to_result");
305
305
  s_id_fit_to_query = rb_intern("fit_to_query");
data/lib/pg/connection.rb CHANGED
@@ -46,37 +46,6 @@ class PG::Connection
46
46
  hash.map { |k,v| "#{k}=#{quote_connstr(v)}" }.join( ' ' )
47
47
  end
48
48
 
49
- # Decode a connection string to Hash options
50
- #
51
- # Value are properly unquoted and unescaped.
52
- def self.connect_string_to_hash( str )
53
- options = {}
54
- key = nil
55
- value = String.new
56
- str.scan(/\G\s*(?>([^\s\\\']+)\s*=\s*|([^\s\\\']+)|'((?:[^\'\\]|\\.)*)'|(\\.?)|(\S))(\s|\z)?/m) do
57
- |k, word, sq, esc, garbage, sep|
58
- raise ArgumentError, "unterminated quoted string in connection info string: #{str.inspect}" if garbage
59
- if k
60
- key = k
61
- else
62
- value << (word || (sq || esc).gsub(/\\(.)/, '\\1'))
63
- end
64
- if sep
65
- raise ArgumentError, "missing = after #{value.inspect}" unless key
66
- options[key.to_sym] = value
67
- key = nil
68
- value = String.new
69
- end
70
- end
71
- options
72
- end
73
-
74
- # URI defined in RFC3986
75
- # This regexp is modified to allow host to specify multiple comma separated components captured as <hostports> and to disallow comma in hostnames.
76
- # Taken from: https://github.com/ruby/ruby/blob/be04006c7d2f9aeb7e9d8d09d945b3a9c7850202/lib/uri/rfc3986_parser.rb#L6
77
- HOST_AND_PORT = /(?<hostport>(?<host>(?<IP-literal>\[(?:(?<IPv6address>(?:\h{1,4}:){6}(?<ls32>\h{1,4}:\h{1,4}|(?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>))|::(?:\h{1,4}:){5}\g<ls32>|\h{1,4}?::(?:\h{1,4}:){4}\g<ls32>|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?<IPvFuture>v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g<IPv4address>|(?<reg-name>(?:%\h\h|[-\.!$&-+0-9;=A-Z_a-z~])+))?(?::(?<port>\d*))?)/
78
- POSTGRESQL_URI = /\A(?<URI>(?<scheme>[A-Za-z][+\-.0-9A-Za-z]*):(?<hier-part>\/\/(?<authority>(?:(?<userinfo>(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?<hostports>#{HOST_AND_PORT}(?:,\g<hostport>)*))(?<path-abempty>(?:\/(?<segment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?<path-absolute>\/(?:(?<segment-nz>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g<segment>)*)?)|(?<path-rootless>\g<segment-nz>(?:\/\g<segment>)*)|(?<path-empty>))(?:\?(?<query>[^#]*))?(?:\#(?<fragment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/
79
-
80
49
  # Parse the connection +args+ into a connection-parameter string.
81
50
  # See PG::Connection.new for valid arguments.
82
51
  #
@@ -87,91 +56,64 @@ class PG::Connection
87
56
  # * URI object
88
57
  # * positional arguments
89
58
  #
90
- # The method adds the option "hostaddr" and "fallback_application_name" if they aren't already set.
91
- # The URI and the options string is passed through and "hostaddr" as well as "fallback_application_name"
92
- # are added to the end.
93
- def self::parse_connect_args( *args )
59
+ # The method adds the option "fallback_application_name" if it isn't already set.
60
+ # It returns a connection string with "key=value" pairs.
61
+ def self.parse_connect_args( *args )
94
62
  hash_arg = args.last.is_a?( Hash ) ? args.pop.transform_keys(&:to_sym) : {}
95
- option_string = ""
96
63
  iopts = {}
97
64
 
98
65
  if args.length == 1
99
66
  case args.first
100
- when URI, POSTGRESQL_URI
101
- uri = args.first.to_s
102
- uri_match = POSTGRESQL_URI.match(uri)
103
- if uri_match['query']
104
- iopts = URI.decode_www_form(uri_match['query']).to_h.transform_keys(&:to_sym)
105
- end
106
- # extract "host1,host2" from "host1:5432,host2:5432"
107
- iopts[:host] = uri_match['hostports'].split(',', -1).map do |hostport|
108
- hostmatch = /\A#{HOST_AND_PORT}\z/.match(hostport)
109
- hostmatch['IPv6address'] || hostmatch['IPv4address'] || hostmatch['reg-name']&.gsub(/%(\h\h)/){ $1.hex.chr }
110
- end.join(',')
111
- oopts = {}
112
- when /=/
113
- # Option string style
114
- option_string = args.first.to_s
115
- iopts = connect_string_to_hash(option_string)
116
- oopts = {}
67
+ when URI, /=/, /:\/\//
68
+ # Option or URL string style
69
+ conn_string = args.first.to_s
70
+ iopts = PG::Connection.conninfo_parse(conn_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
117
71
  else
118
72
  # Positional parameters (only host given)
119
73
  iopts[CONNECT_ARGUMENT_ORDER.first.to_sym] = args.first
120
- oopts = iopts.dup
121
74
  end
122
75
  else
123
- # Positional parameters
76
+ # Positional parameters with host and more
124
77
  max = CONNECT_ARGUMENT_ORDER.length
125
78
  raise ArgumentError,
126
- "Extra positional parameter %d: %p" % [ max + 1, args[max] ] if args.length > max
79
+ "Extra positional parameter %d: %p" % [ max + 1, args[max] ] if args.length > max
127
80
 
128
81
  CONNECT_ARGUMENT_ORDER.zip( args ) do |(k,v)|
129
82
  iopts[ k.to_sym ] = v if v
130
83
  end
131
84
  iopts.delete(:tty) # ignore obsolete tty parameter
132
- oopts = iopts.dup
133
85
  end
134
86
 
135
87
  iopts.merge!( hash_arg )
136
- oopts.merge!( hash_arg )
137
-
138
- # Resolve DNS in Ruby to avoid blocking state while connecting, when it ...
139
- if (host=iopts[:host]) && !iopts[:hostaddr]
140
- hostaddrs = host.split(",", -1).map do |mhost|
141
- if !mhost.empty? && !mhost.start_with?("/") && # isn't UnixSocket
142
- # isn't a path on Windows
143
- (RUBY_PLATFORM !~ /mingw|mswin/ || mhost !~ /\A\w:[\/\\]/)
144
-
145
- if Fiber.respond_to?(:scheduler) &&
146
- Fiber.scheduler &&
147
- RUBY_VERSION < '3.1.'
148
-
149
- # Use a second thread to avoid blocking of the scheduler.
150
- # `IPSocket.getaddress` isn't fiber aware before ruby-3.1.
151
- Thread.new{ IPSocket.getaddress(mhost) rescue '' }.value
152
- else
153
- IPSocket.getaddress(mhost) rescue ''
154
- end
155
- end
156
- end
157
- oopts[:hostaddr] = hostaddrs.join(",") if hostaddrs.any?
158
- end
159
88
 
160
89
  if !iopts[:fallback_application_name]
161
- oopts[:fallback_application_name] = $0.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
90
+ iopts[:fallback_application_name] = $0.sub( /^(.{30}).{4,}(.{30})$/ ){ $1+"..."+$2 }
162
91
  end
163
92
 
164
- if uri
165
- uri += uri_match['query'] ? "&" : "?"
166
- uri += URI.encode_www_form( oopts )
167
- return uri
93
+ return connect_hash_to_string(iopts)
94
+ end
95
+
96
+ # Return a String representation of the object suitable for debugging.
97
+ def inspect
98
+ str = self.to_s
99
+ str[-1,0] = if finished?
100
+ " finished"
168
101
  else
169
- option_string += ' ' unless option_string.empty? && oopts.empty?
170
- return option_string + connect_hash_to_string(oopts)
102
+ stats = []
103
+ stats << " status=#{ PG.constants.grep(/CONNECTION_/).find{|c| PG.const_get(c) == status} }" if status != CONNECTION_OK
104
+ stats << " transaction_status=#{ PG.constants.grep(/PQTRANS_/).find{|c| PG.const_get(c) == transaction_status} }" if transaction_status != PG::PQTRANS_IDLE
105
+ stats << " nonblocking=#{ isnonblocking }" if isnonblocking
106
+ stats << " pipeline_status=#{ PG.constants.grep(/PQ_PIPELINE_/).find{|c| PG.const_get(c) == pipeline_status} }" if respond_to?(:pipeline_status) && pipeline_status != PG::PQ_PIPELINE_OFF
107
+ stats << " client_encoding=#{ get_client_encoding }" if get_client_encoding != "UTF8"
108
+ stats << " type_map_for_results=#{ type_map_for_results.to_s }" unless type_map_for_results.is_a?(PG::TypeMapAllStrings)
109
+ stats << " type_map_for_queries=#{ type_map_for_queries.to_s }" unless type_map_for_queries.is_a?(PG::TypeMapAllStrings)
110
+ stats << " encoder_for_put_copy_data=#{ encoder_for_put_copy_data.to_s }" if encoder_for_put_copy_data
111
+ stats << " decoder_for_get_copy_data=#{ decoder_for_get_copy_data.to_s }" if decoder_for_get_copy_data
112
+ " host=#{host} port=#{port} user=#{user}#{stats.join}"
171
113
  end
114
+ return str
172
115
  end
173
116
 
174
-
175
117
  # call-seq:
176
118
  # conn.copy_data( sql [, coder] ) {|sql_result| ... } -> PG::Result
177
119
  #
@@ -241,7 +183,7 @@ class PG::Connection
241
183
  # ["more", "data", "to", "copy"]
242
184
 
243
185
  def copy_data( sql, coder=nil )
244
- raise PG::NotInBlockingMode, "copy_data can not be used in nonblocking mode" if nonblocking?
186
+ raise PG::NotInBlockingMode.new("copy_data can not be used in nonblocking mode", connection: self) if nonblocking?
245
187
  res = exec( sql )
246
188
 
247
189
  case res.result_status
@@ -273,11 +215,15 @@ class PG::Connection
273
215
  yield res
274
216
  rescue Exception => err
275
217
  cancel
276
- while get_copy_data
218
+ begin
219
+ while get_copy_data
220
+ end
221
+ rescue PG::Error
222
+ # Ignore error in cleanup to avoid losing original exception
277
223
  end
278
224
  while get_result
279
225
  end
280
- raise
226
+ raise err
281
227
  else
282
228
  res = get_last_result
283
229
  if !res || res.result_status != PGRES_COMMAND_OK
@@ -285,7 +231,7 @@ class PG::Connection
285
231
  end
286
232
  while get_result
287
233
  end
288
- raise PG::NotAllCopyDataRetrieved, "Not all COPY data retrieved"
234
+ raise PG::NotAllCopyDataRetrieved.new("Not all COPY data retrieved", connection: self)
289
235
  end
290
236
  res
291
237
  ensure
@@ -310,16 +256,17 @@ class PG::Connection
310
256
  # and a +COMMIT+ at the end of the block, or
311
257
  # +ROLLBACK+ if any exception occurs.
312
258
  def transaction
259
+ rollback = false
313
260
  exec "BEGIN"
314
- res = yield(self)
261
+ yield(self)
315
262
  rescue Exception
263
+ rollback = true
316
264
  cancel if transaction_status == PG::PQTRANS_ACTIVE
317
265
  block
318
266
  exec "ROLLBACK"
319
267
  raise
320
- else
321
- exec "COMMIT"
322
- res
268
+ ensure
269
+ exec "COMMIT" unless rollback
323
270
  end
324
271
 
325
272
  ### Returns an array of Hashes with connection defaults. See ::conndefaults
@@ -482,10 +429,20 @@ class PG::Connection
482
429
  # See also #copy_data.
483
430
  #
484
431
  def put_copy_data(buffer, encoder=nil)
485
- until sync_put_copy_data(buffer, encoder)
486
- flush
432
+ # sync_put_copy_data does a non-blocking attept to flush data.
433
+ until res=sync_put_copy_data(buffer, encoder)
434
+ # It didn't flush immediately and allocation of more buffering memory failed.
435
+ # Wait for all data sent by doing a blocking flush.
436
+ res = flush
487
437
  end
488
- flush
438
+
439
+ # And do a blocking flush every 100 calls.
440
+ # This is to avoid memory bloat, when sending the data is slower than calls to put_copy_data happen.
441
+ if (@calls_to_put_copy_data += 1) > 100
442
+ @calls_to_put_copy_data = 0
443
+ res = flush
444
+ end
445
+ res
489
446
  end
490
447
  alias async_put_copy_data put_copy_data
491
448
 
@@ -505,6 +462,7 @@ class PG::Connection
505
462
  until sync_put_copy_end(*args)
506
463
  flush
507
464
  end
465
+ @calls_to_put_copy_data = 0
508
466
  flush
509
467
  end
510
468
  alias async_put_copy_end put_copy_end
@@ -545,6 +503,7 @@ class PG::Connection
545
503
  def reset
546
504
  reset_start
547
505
  async_connect_or_reset(:reset_poll)
506
+ self
548
507
  end
549
508
  alias async_reset reset
550
509
 
@@ -613,18 +572,54 @@ class PG::Connection
613
572
 
614
573
  private def async_connect_or_reset(poll_meth)
615
574
  # Track the progress of the connection, waiting for the socket to become readable/writable before polling it
575
+
576
+ if (timeo = conninfo_hash[:connect_timeout].to_i) && timeo > 0
577
+ # Lowest timeout is 2 seconds - like in libpq
578
+ timeo = [timeo, 2].max
579
+ host_count = conninfo_hash[:host].to_s.count(",") + 1
580
+ stop_time = timeo * host_count + Process.clock_gettime(Process::CLOCK_MONOTONIC)
581
+ end
582
+
616
583
  poll_status = PG::PGRES_POLLING_WRITING
617
584
  until poll_status == PG::PGRES_POLLING_OK ||
618
585
  poll_status == PG::PGRES_POLLING_FAILED
619
586
 
620
- # If the socket needs to read, wait 'til it becomes readable to poll again
621
- case poll_status
622
- when PG::PGRES_POLLING_READING
623
- socket_io.wait_readable
587
+ # Set single timeout to parameter "connect_timeout" but
588
+ # don't exceed total connection time of number-of-hosts * connect_timeout.
589
+ timeout = [timeo, stop_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)].min if stop_time
590
+ event = if !timeout || timeout >= 0
591
+ # If the socket needs to read, wait 'til it becomes readable to poll again
592
+ case poll_status
593
+ when PG::PGRES_POLLING_READING
594
+ if defined?(IO::READABLE) # ruby-3.0+
595
+ socket_io.wait(IO::READABLE | IO::PRIORITY, timeout)
596
+ else
597
+ IO.select([socket_io], nil, [socket_io], timeout)
598
+ end
624
599
 
625
- # ...and the same for when the socket needs to write
626
- when PG::PGRES_POLLING_WRITING
627
- socket_io.wait_writable
600
+ # ...and the same for when the socket needs to write
601
+ when PG::PGRES_POLLING_WRITING
602
+ if defined?(IO::WRITABLE) # ruby-3.0+
603
+ # Use wait instead of wait_readable, since connection errors are delivered as
604
+ # exceptional/priority events on Windows.
605
+ socket_io.wait(IO::WRITABLE | IO::PRIORITY, timeout)
606
+ else
607
+ # io#wait on ruby-2.x doesn't wait for priority, so fallback to IO.select
608
+ IO.select(nil, [socket_io], [socket_io], timeout)
609
+ end
610
+ end
611
+ end
612
+ # connection to server at "localhost" (127.0.0.1), port 5433 failed: timeout expired (PG::ConnectionBad)
613
+ # connection to server on socket "/var/run/postgresql/.s.PGSQL.5433" failed: No such file or directory
614
+ unless event
615
+ if self.class.send(:host_is_named_pipe?, host)
616
+ connhost = "on socket \"#{host}\""
617
+ elsif respond_to?(:hostaddr)
618
+ connhost = "at \"#{host}\" (#{hostaddr}), port #{port}"
619
+ else
620
+ connhost = "at \"#{host}\", port #{port}"
621
+ end
622
+ raise PG::ConnectionBad.new("connection to server #{connhost} failed: timeout expired", connection: self)
628
623
  end
629
624
 
630
625
  # Check to see if it's finished or failed yet
@@ -634,7 +629,7 @@ class PG::Connection
634
629
  unless status == PG::CONNECTION_OK
635
630
  msg = error_message
636
631
  finish
637
- raise PG::ConnectionBad, msg
632
+ raise PG::ConnectionBad.new(msg, connection: self)
638
633
  end
639
634
 
640
635
  # Set connection to nonblocking to handle all blocking states in ruby.
@@ -642,8 +637,6 @@ class PG::Connection
642
637
  sync_setnonblocking(true)
643
638
  self.flush_data = true
644
639
  set_default_encoding
645
-
646
- self
647
640
  end
648
641
 
649
642
  class << self
@@ -698,13 +691,17 @@ class PG::Connection
698
691
  # connection will have its +client_encoding+ set accordingly.
699
692
  #
700
693
  # Raises a PG::Error if the connection fails.
701
- def new(*args, **kwargs)
702
- conn = self.connect_start(*args, **kwargs ) or
703
- raise(PG::Error, "Unable to create a new connection")
704
-
705
- raise(PG::ConnectionBad, conn.error_message) if conn.status == PG::CONNECTION_BAD
706
-
707
- conn.send(:async_connect_or_reset, :connect_poll)
694
+ def new(*args)
695
+ conn = connect_to_hosts(*args)
696
+
697
+ if block_given?
698
+ begin
699
+ return yield conn
700
+ ensure
701
+ conn.finish
702
+ end
703
+ end
704
+ conn
708
705
  end
709
706
  alias async_connect new
710
707
  alias connect new
@@ -712,6 +709,64 @@ class PG::Connection
712
709
  alias setdb new
713
710
  alias setdblogin new
714
711
 
712
+ private def connect_to_hosts(*args)
713
+ option_string = parse_connect_args(*args)
714
+ iopts = PG::Connection.conninfo_parse(option_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }
715
+ iopts = PG::Connection.conndefaults.each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }.merge(iopts)
716
+
717
+ if iopts[:hostaddr]
718
+ # hostaddr is provided -> no need to resolve hostnames
719
+
720
+ elsif iopts[:host] && !iopts[:host].empty? && PG.library_version >= 100000
721
+ # Resolve DNS in Ruby to avoid blocking state while connecting.
722
+ # Multiple comma-separated values are generated, if the hostname resolves to both IPv4 and IPv6 addresses.
723
+ # This requires PostgreSQL-10+, so no DNS resolving is done on earlier versions.
724
+ ihosts = iopts[:host].split(",", -1)
725
+ iports = iopts[:port].split(",", -1)
726
+ iports = iports * ihosts.size if iports.size == 1
727
+ raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihosts.size} hosts" if iports.size != ihosts.size
728
+
729
+ dests = ihosts.each_with_index.flat_map do |mhost, idx|
730
+ unless host_is_named_pipe?(mhost)
731
+ if Fiber.respond_to?(:scheduler) &&
732
+ Fiber.scheduler &&
733
+ RUBY_VERSION < '3.1.'
734
+
735
+ # Use a second thread to avoid blocking of the scheduler.
736
+ # `TCPSocket.gethostbyname` isn't fiber aware before ruby-3.1.
737
+ hostaddrs = Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
738
+ else
739
+ hostaddrs = Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
740
+ end
741
+ else
742
+ # No hostname to resolve (UnixSocket)
743
+ hostaddrs = [nil]
744
+ end
745
+ hostaddrs.map { |hostaddr| [hostaddr, mhost, iports[idx]] }
746
+ end
747
+ iopts.merge!(
748
+ hostaddr: dests.map{|d| d[0] }.join(","),
749
+ host: dests.map{|d| d[1] }.join(","),
750
+ port: dests.map{|d| d[2] }.join(","))
751
+ else
752
+ # No host given
753
+ end
754
+ conn = self.connect_start(iopts) or
755
+ raise(PG::Error, "Unable to create a new connection")
756
+
757
+ raise PG::ConnectionBad, conn.error_message if conn.status == PG::CONNECTION_BAD
758
+
759
+ conn.send(:async_connect_or_reset, :connect_poll)
760
+ conn
761
+ end
762
+
763
+ private def host_is_named_pipe?(host_string)
764
+ host_string.empty? || host_string.start_with?("/") || # it's UnixSocket?
765
+ host_string.start_with?("@") || # it's UnixSocket in the abstract namespace?
766
+ # it's a path on Windows?
767
+ (RUBY_PLATFORM =~ /mingw|mswin/ && host_string =~ /\A([\/\\]|\w:[\/\\])/)
768
+ end
769
+
715
770
  # call-seq:
716
771
  # PG::Connection.ping(connection_hash) -> Integer
717
772
  # PG::Connection.ping(connection_string) -> Integer
data/lib/pg/exceptions.rb CHANGED
@@ -6,7 +6,13 @@ require 'pg' unless defined?( PG )
6
6
 
7
7
  module PG
8
8
 
9
- class Error < StandardError; end
9
+ class Error < StandardError
10
+ def initialize(msg=nil, connection: nil, result: nil)
11
+ @connection = connection
12
+ @result = result
13
+ super(msg)
14
+ end
15
+ end
10
16
 
11
17
  end # module PG
12
18
 
data/lib/pg/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module PG
2
2
  # Library version
3
- VERSION = '1.3.5'
3
+ VERSION = '1.4.4'
4
4
  end