sequel 5.68.0 → 5.77.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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +134 -0
  3. data/README.rdoc +3 -3
  4. data/doc/mass_assignment.rdoc +1 -1
  5. data/doc/migration.rdoc +15 -0
  6. data/doc/opening_databases.rdoc +12 -3
  7. data/doc/release_notes/5.69.0.txt +26 -0
  8. data/doc/release_notes/5.70.0.txt +35 -0
  9. data/doc/release_notes/5.71.0.txt +21 -0
  10. data/doc/release_notes/5.72.0.txt +33 -0
  11. data/doc/release_notes/5.73.0.txt +66 -0
  12. data/doc/release_notes/5.74.0.txt +45 -0
  13. data/doc/release_notes/5.75.0.txt +35 -0
  14. data/doc/release_notes/5.76.0.txt +86 -0
  15. data/doc/release_notes/5.77.0.txt +63 -0
  16. data/doc/sharding.rdoc +3 -1
  17. data/doc/testing.rdoc +4 -2
  18. data/lib/sequel/adapters/ibmdb.rb +1 -1
  19. data/lib/sequel/adapters/jdbc/h2.rb +3 -0
  20. data/lib/sequel/adapters/jdbc/hsqldb.rb +2 -0
  21. data/lib/sequel/adapters/jdbc/postgresql.rb +3 -0
  22. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +15 -0
  23. data/lib/sequel/adapters/jdbc/sqlserver.rb +4 -0
  24. data/lib/sequel/adapters/jdbc.rb +10 -6
  25. data/lib/sequel/adapters/mysql.rb +19 -7
  26. data/lib/sequel/adapters/mysql2.rb +2 -2
  27. data/lib/sequel/adapters/odbc/mssql.rb +1 -1
  28. data/lib/sequel/adapters/postgres.rb +6 -5
  29. data/lib/sequel/adapters/shared/db2.rb +12 -0
  30. data/lib/sequel/adapters/shared/mssql.rb +1 -1
  31. data/lib/sequel/adapters/shared/mysql.rb +31 -1
  32. data/lib/sequel/adapters/shared/oracle.rb +4 -6
  33. data/lib/sequel/adapters/shared/postgres.rb +79 -4
  34. data/lib/sequel/adapters/shared/sqlanywhere.rb +10 -4
  35. data/lib/sequel/adapters/shared/sqlite.rb +20 -3
  36. data/lib/sequel/adapters/sqlite.rb +42 -3
  37. data/lib/sequel/adapters/trilogy.rb +117 -0
  38. data/lib/sequel/connection_pool/sharded_threaded.rb +11 -10
  39. data/lib/sequel/connection_pool/sharded_timed_queue.rb +374 -0
  40. data/lib/sequel/connection_pool/threaded.rb +6 -0
  41. data/lib/sequel/connection_pool/timed_queue.rb +16 -3
  42. data/lib/sequel/connection_pool.rb +10 -1
  43. data/lib/sequel/database/connecting.rb +1 -1
  44. data/lib/sequel/database/misc.rb +2 -2
  45. data/lib/sequel/database/schema_methods.rb +9 -2
  46. data/lib/sequel/database/transactions.rb +6 -0
  47. data/lib/sequel/dataset/actions.rb +8 -6
  48. data/lib/sequel/dataset/features.rb +10 -1
  49. data/lib/sequel/dataset/sql.rb +47 -34
  50. data/lib/sequel/extensions/any_not_empty.rb +2 -2
  51. data/lib/sequel/extensions/async_thread_pool.rb +3 -2
  52. data/lib/sequel/extensions/auto_cast_date_and_time.rb +94 -0
  53. data/lib/sequel/extensions/connection_expiration.rb +15 -9
  54. data/lib/sequel/extensions/connection_validator.rb +15 -10
  55. data/lib/sequel/extensions/duplicate_columns_handler.rb +10 -9
  56. data/lib/sequel/extensions/index_caching.rb +5 -1
  57. data/lib/sequel/extensions/migration.rb +52 -13
  58. data/lib/sequel/extensions/named_timezones.rb +1 -1
  59. data/lib/sequel/extensions/pg_array.rb +10 -0
  60. data/lib/sequel/extensions/pg_auto_parameterize_in_array.rb +110 -0
  61. data/lib/sequel/extensions/pg_extended_date_support.rb +4 -4
  62. data/lib/sequel/extensions/pg_json_ops.rb +52 -0
  63. data/lib/sequel/extensions/pg_range.rb +2 -2
  64. data/lib/sequel/extensions/pg_timestamptz.rb +27 -3
  65. data/lib/sequel/extensions/round_timestamps.rb +1 -1
  66. data/lib/sequel/extensions/schema_caching.rb +1 -1
  67. data/lib/sequel/extensions/server_block.rb +2 -1
  68. data/lib/sequel/extensions/transaction_connection_validator.rb +78 -0
  69. data/lib/sequel/model/associations.rb +9 -2
  70. data/lib/sequel/model/base.rb +25 -12
  71. data/lib/sequel/model/dataset_module.rb +3 -0
  72. data/lib/sequel/model/exceptions.rb +15 -3
  73. data/lib/sequel/plugins/column_encryption.rb +27 -6
  74. data/lib/sequel/plugins/defaults_setter.rb +16 -0
  75. data/lib/sequel/plugins/list.rb +5 -2
  76. data/lib/sequel/plugins/mssql_optimistic_locking.rb +8 -38
  77. data/lib/sequel/plugins/optimistic_locking.rb +9 -42
  78. data/lib/sequel/plugins/optimistic_locking_base.rb +55 -0
  79. data/lib/sequel/plugins/paged_operations.rb +181 -0
  80. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +5 -1
  81. data/lib/sequel/plugins/pg_xmin_optimistic_locking.rb +109 -0
  82. data/lib/sequel/plugins/rcte_tree.rb +7 -4
  83. data/lib/sequel/plugins/static_cache.rb +38 -0
  84. data/lib/sequel/plugins/static_cache_cache.rb +5 -1
  85. data/lib/sequel/plugins/validation_helpers.rb +1 -1
  86. data/lib/sequel/version.rb +1 -1
  87. metadata +43 -3
@@ -111,6 +111,18 @@ module Sequel
111
111
  # static data that you do not want to modify
112
112
  # :timeout :: how long to wait for the database to be available if it
113
113
  # is locked, given in milliseconds (default is 5000)
114
+ # :setup_regexp_function :: enable use of Regexp objects with SQL
115
+ # 'REGEXP' operator. If the value is :cached or "cached",
116
+ # caches the generated regexps, which can result in a memory
117
+ # leak if dynamic regexps are used. If the value is a Proc,
118
+ # it will be called with a string for the regexp and a string
119
+ # for the value to compare, and should return whether the regexp
120
+ # matches.
121
+ # :regexp_function_cache :: If setting +setup_regexp_function+ to +cached+, this
122
+ # determines the cache to use. It should either be a proc or a class, and it
123
+ # defaults to +Hash+. You can use +ObjectSpace::WeakKeyMap+ on Ruby 3.3+ to
124
+ # have the VM automatically remove regexps from the cache after they
125
+ # are no longer used.
114
126
  def connect(server)
115
127
  opts = server_opts(server)
116
128
  opts[:database] = ':memory:' if blank_object?(opts[:database])
@@ -126,9 +138,7 @@ module Sequel
126
138
  connection_pragmas.each{|s| log_connection_yield(s, db){db.execute_batch(s)}}
127
139
 
128
140
  if typecast_value_boolean(opts[:setup_regexp_function])
129
- db.create_function("regexp", 2) do |func, regexp_str, string|
130
- func.result = Regexp.new(regexp_str).match(string) ? 1 : 0
131
- end
141
+ setup_regexp_function(db, opts[:setup_regexp_function])
132
142
  end
133
143
 
134
144
  class << db
@@ -202,6 +212,35 @@ module Sequel
202
212
  @conversion_procs['datetime'] = @conversion_procs['timestamp'] = method(:to_application_timestamp)
203
213
  set_integer_booleans
204
214
  end
215
+
216
+ def setup_regexp_function(db, how)
217
+ case how
218
+ when Proc
219
+ # nothing
220
+ when :cached, "cached"
221
+ cache = @opts[:regexp_function_cache] || Hash
222
+ cache = cache.is_a?(Proc) ? cache.call : cache.new
223
+ how = if RUBY_VERSION >= '2.4'
224
+ lambda do |regexp_str, str|
225
+ (cache[regexp_str] ||= Regexp.new(regexp_str)).match?(str)
226
+ end
227
+ else
228
+ lambda do |regexp_str, str|
229
+ (cache[regexp_str] ||= Regexp.new(regexp_str)).match(str)
230
+ end
231
+ end
232
+ else
233
+ how = if RUBY_VERSION >= '2.4'
234
+ lambda{|regexp_str, str| Regexp.new(regexp_str).match?(str)}
235
+ else
236
+ lambda{|regexp_str, str| Regexp.new(regexp_str).match(str)}
237
+ end
238
+ end
239
+
240
+ db.create_function("regexp", 2) do |func, regexp_str, str|
241
+ func.result = how.call(regexp_str, str) ? 1 : 0
242
+ end
243
+ end
205
244
 
206
245
  # Yield an available connection. Rescue
207
246
  # any SQLite3::Exceptions and turn them into DatabaseErrors.
@@ -0,0 +1,117 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'trilogy'
4
+ require_relative 'shared/mysql'
5
+
6
+ module Sequel
7
+ module Trilogy
8
+ class Database < Sequel::Database
9
+ include Sequel::MySQL::DatabaseMethods
10
+
11
+ QUERY_FLAGS = ::Trilogy::QUERY_FLAGS_CAST | ::Trilogy::QUERY_FLAGS_CAST_BOOLEANS
12
+ LOCAL_TIME_QUERY_FLAGS = QUERY_FLAGS | ::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
13
+
14
+ set_adapter_scheme :trilogy
15
+
16
+ # Connect to the database. See Trilogy documentation for options.
17
+ def connect(server)
18
+ opts = server_opts(server)
19
+ opts[:username] ||= opts.delete(:user)
20
+ opts[:found_rows] = true
21
+ conn = ::Trilogy.new(opts)
22
+ mysql_connection_setting_sqls.each{|sql| log_connection_yield(sql, conn){conn.query(sql)}}
23
+ conn
24
+ end
25
+
26
+ def disconnect_connection(c)
27
+ c.discard!
28
+ rescue ::Trilogy::Error
29
+ nil
30
+ end
31
+
32
+ # Execute the given SQL on the given connection and yield the result.
33
+ def execute(sql, opts)
34
+ r = synchronize(opts[:server]) do |conn|
35
+ log_connection_yield((log_sql = opts[:log_sql]) ? sql + log_sql : sql, conn) do
36
+ conn.query_with_flags(sql, timezone.nil? || timezone == :local ? LOCAL_TIME_QUERY_FLAGS : QUERY_FLAGS)
37
+ end
38
+ end
39
+ yield r
40
+ rescue ::Trilogy::Error => e
41
+ raise_error(e)
42
+ end
43
+
44
+ def execute_dui(sql, opts=OPTS)
45
+ execute(sql, opts, &:affected_rows)
46
+ end
47
+
48
+ def execute_insert(sql, opts=OPTS)
49
+ execute(sql, opts, &:last_insert_id)
50
+ end
51
+
52
+ def freeze
53
+ server_version
54
+ super
55
+ end
56
+
57
+ # Return the version of the MySQL server to which we are connecting.
58
+ def server_version(_server=nil)
59
+ @server_version ||= super()
60
+ end
61
+
62
+ private
63
+
64
+ def database_specific_error_class(exception, opts)
65
+ case exception.message
66
+ when /1205 - Lock wait timeout exceeded; try restarting transaction\z/
67
+ DatabaseLockTimeout
68
+ else
69
+ super
70
+ end
71
+ end
72
+
73
+ def connection_execute_method
74
+ :query
75
+ end
76
+
77
+ def database_error_classes
78
+ [::Trilogy::Error]
79
+ end
80
+
81
+ def dataset_class_default
82
+ Dataset
83
+ end
84
+
85
+ # Convert tinyint(1) type to boolean if convert_tinyint_to_bool is true
86
+ def schema_column_type(db_type)
87
+ db_type =~ /\Atinyint\(1\)/ ? :boolean : super
88
+ end
89
+ end
90
+
91
+ class Dataset < Sequel::Dataset
92
+ include Sequel::MySQL::DatasetMethods
93
+
94
+ def fetch_rows(sql)
95
+ execute(sql) do |r|
96
+ self.columns = r.fields.map!{|c| output_identifier(c.to_s)}
97
+ r.each_hash{|h| yield h}
98
+ end
99
+ self
100
+ end
101
+
102
+ private
103
+
104
+ def execute(sql, opts=OPTS)
105
+ opts = Hash[opts]
106
+ opts[:type] = :select
107
+ super
108
+ end
109
+
110
+ # Handle correct quoting of strings using ::Trilogy#escape.
111
+ def literal_string_append(sql, v)
112
+ sql << "'" << db.synchronize(@opts[:server]){|c| c.escape(v)} << "'"
113
+ end
114
+ end
115
+ end
116
+ end
117
+
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative 'threaded'
4
4
 
5
- # The slowest and most advanced connection, dealing with both multi-threaded
5
+ # The slowest and most advanced connection pool, dealing with both multi-threaded
6
6
  # access and configurations with multiple shards/servers.
7
7
  #
8
8
  # In addition, this pool subclass also handles scheduling in-use connections
@@ -112,7 +112,7 @@ class Sequel::ShardedThreadedConnectionPool < Sequel::ThreadedConnectionPool
112
112
  # available, creates a new connection. Passes the connection to the supplied
113
113
  # block:
114
114
  #
115
- # pool.hold {|conn| conn.execute('DROP TABLE posts')}
115
+ # pool.hold(:server1) {|conn| conn.execute('DROP TABLE posts')}
116
116
  #
117
117
  # Pool#hold is re-entrant, meaning it can be called recursively in
118
118
  # the same thread without blocking.
@@ -145,12 +145,13 @@ class Sequel::ShardedThreadedConnectionPool < Sequel::ThreadedConnectionPool
145
145
  # except that after it is used, future requests for the server will use the
146
146
  # :default server instead.
147
147
  def remove_servers(servers)
148
- conns = nil
148
+ conns = []
149
+ raise(Sequel::Error, "cannot remove default server") if servers.include?(:default)
150
+
149
151
  sync do
150
- raise(Sequel::Error, "cannot remove default server") if servers.include?(:default)
151
152
  servers.each do |server|
152
153
  if @servers.include?(server)
153
- conns = disconnect_server_connections(server)
154
+ conns.concat(disconnect_server_connections(server))
154
155
  @waiters.delete(server)
155
156
  @available_connections.delete(server)
156
157
  @allocated.delete(server)
@@ -159,9 +160,9 @@ class Sequel::ShardedThreadedConnectionPool < Sequel::ThreadedConnectionPool
159
160
  end
160
161
  end
161
162
 
162
- if conns
163
- disconnect_connections(conns)
164
- end
163
+ nil
164
+ ensure
165
+ disconnect_connections(conns)
165
166
  end
166
167
 
167
168
  # Return an array of symbols for servers in the connection pool.
@@ -186,7 +187,7 @@ class Sequel::ShardedThreadedConnectionPool < Sequel::ThreadedConnectionPool
186
187
  # is available. The calling code should NOT already have the mutex when
187
188
  # calling this.
188
189
  #
189
- # This should return a connection is one is available within the timeout,
190
+ # This should return a connection if one is available within the timeout,
190
191
  # or nil if a connection could not be acquired within the timeout.
191
192
  def acquire(thread, server)
192
193
  if conn = assign_connection(thread, server)
@@ -325,7 +326,7 @@ class Sequel::ShardedThreadedConnectionPool < Sequel::ThreadedConnectionPool
325
326
  # Create the maximum number of connections immediately. The calling code should
326
327
  # NOT have the mutex before calling this.
327
328
  def preconnect(concurrent = false)
328
- conn_servers = @servers.keys.map!{|s| Array.new(max_size - _size(s), s)}.flatten!
329
+ conn_servers = sync{@servers.keys}.map!{|s| Array.new(max_size - _size(s), s)}.flatten!
329
330
 
330
331
  if concurrent
331
332
  conn_servers.map!{|s| Thread.new{[s, make_new(s)]}}.map!(&:value)
@@ -0,0 +1,374 @@
1
+ # frozen-string-literal: true
2
+
3
+ # :nocov:
4
+ raise LoadError, "Sequel::ShardedTimedQueueConnectionPool is only available on Ruby 3.2+" unless RUBY_VERSION >= '3.2'
5
+ # :nocov:
6
+
7
+ # A connection pool allowing multi-threaded access to a sharded pool of connections,
8
+ # using a timed queue (only available in Ruby 3.2+).
9
+ class Sequel::ShardedTimedQueueConnectionPool < Sequel::ConnectionPool
10
+ # The maximum number of connections this pool will create per shard.
11
+ attr_reader :max_size
12
+
13
+ # The following additional options are respected:
14
+ # :max_connections :: The maximum number of connections the connection pool
15
+ # will open (default 4)
16
+ # :pool_timeout :: The amount of seconds to wait to acquire a connection
17
+ # before raising a PoolTimeout (default 5)
18
+ # :servers :: A hash of servers to use. Keys should be symbols. If not
19
+ # present, will use a single :default server.
20
+ # :servers_hash :: The base hash to use for the servers. By default,
21
+ # Sequel uses Hash.new(:default). You can use a hash with a default proc
22
+ # that raises an error if you want to catch all cases where a nonexistent
23
+ # server is used.
24
+ def initialize(db, opts = OPTS)
25
+ super
26
+
27
+ @max_size = Integer(opts[:max_connections] || 4)
28
+ raise(Sequel::Error, ':max_connections must be positive') if @max_size < 1
29
+ @mutex = Mutex.new
30
+ @timeout = Float(opts[:pool_timeout] || 5)
31
+
32
+ @allocated = {}
33
+ @sizes = {}
34
+ @queues = {}
35
+ @servers = opts.fetch(:servers_hash, Hash.new(:default))
36
+
37
+ add_servers([:default])
38
+ add_servers(opts[:servers].keys) if opts[:servers]
39
+ end
40
+
41
+ # Adds new servers to the connection pool. Allows for dynamic expansion of the potential replicas/shards
42
+ # at runtime. +servers+ argument should be an array of symbols.
43
+ def add_servers(servers)
44
+ sync do
45
+ servers.each do |server|
46
+ next if @servers.has_key?(server)
47
+
48
+ @servers[server] = server
49
+ @sizes[server] = 0
50
+ @queues[server] = Queue.new
51
+ (@allocated[server] = {}).compare_by_identity
52
+ end
53
+ end
54
+ nil
55
+ end
56
+
57
+ # Yield all of the available connections, and the one currently allocated to
58
+ # this thread (if one is allocated). This will not yield connections currently
59
+ # allocated to other threads, as it is not safe to operate on them.
60
+ def all_connections
61
+ thread = Sequel.current
62
+ sync{@queues.to_a}.each do |server, queue|
63
+ if conn = owned_connection(thread, server)
64
+ yield conn
65
+ end
66
+
67
+ # Use a hash to record all connections already seen. As soon as we
68
+ # come across a connection we've already seen, we stop the loop.
69
+ conns = {}
70
+ conns.compare_by_identity
71
+ while true
72
+ conn = nil
73
+ begin
74
+ break unless (conn = queue.pop(timeout: 0)) && !conns[conn]
75
+ conns[conn] = true
76
+ yield conn
77
+ ensure
78
+ queue.push(conn) if conn
79
+ end
80
+ end
81
+ end
82
+
83
+ nil
84
+ end
85
+
86
+ # Removes all connections currently in the pool's queue. This method has the effect of
87
+ # disconnecting from the database, assuming that no connections are currently
88
+ # being used.
89
+ #
90
+ # Once a connection is requested using #hold, the connection pool
91
+ # creates new connections to the database.
92
+ #
93
+ # If the :server option is provided, it should be a symbol or array of symbols,
94
+ # and then the method will only disconnect connectsion from those specified shards.
95
+ def disconnect(opts=OPTS)
96
+ (opts[:server] ? Array(opts[:server]) : sync{@servers.keys}).each do |server|
97
+ raise Sequel::Error, "invalid server" unless queue = sync{@queues[server]}
98
+ while conn = queue.pop(timeout: 0)
99
+ disconnect_pool_connection(conn, server)
100
+ end
101
+ fill_queue(server)
102
+ end
103
+ nil
104
+ end
105
+
106
+ # Chooses the first available connection for the given server, or if none are
107
+ # available, creates a new connection. Passes the connection to the supplied
108
+ # block:
109
+ #
110
+ # pool.hold(:server1) {|conn| conn.execute('DROP TABLE posts')}
111
+ #
112
+ # Pool#hold is re-entrant, meaning it can be called recursively in
113
+ # the same thread without blocking.
114
+ #
115
+ # If no connection is immediately available and the pool is already using the maximum
116
+ # number of connections, Pool#hold will block until a connection
117
+ # is available or the timeout expires. If the timeout expires before a
118
+ # connection can be acquired, a Sequel::PoolTimeout is raised.
119
+ def hold(server=:default)
120
+ server = pick_server(server)
121
+ t = Sequel.current
122
+ if conn = owned_connection(t, server)
123
+ return yield(conn)
124
+ end
125
+
126
+ begin
127
+ conn = acquire(t, server)
128
+ yield conn
129
+ rescue Sequel::DatabaseDisconnectError, *@error_classes => e
130
+ if disconnect_error?(e)
131
+ oconn = conn
132
+ conn = nil
133
+ disconnect_pool_connection(oconn, server) if oconn
134
+ sync{@allocated[server].delete(t)}
135
+ fill_queue(server)
136
+ end
137
+ raise
138
+ ensure
139
+ release(t, conn, server) if conn
140
+ end
141
+ end
142
+
143
+ # The total number of connections in the pool. Using a non-existant server will return nil.
144
+ def size(server=:default)
145
+ sync{@sizes[server]}
146
+ end
147
+
148
+ # Remove servers from the connection pool. Similar to disconnecting from all given servers,
149
+ # except that after it is used, future requests for the servers will use the
150
+ # :default server instead.
151
+ #
152
+ # Note that an error will be raised if there are any connections currently checked
153
+ # out for the given servers.
154
+ def remove_servers(servers)
155
+ conns = []
156
+ raise(Sequel::Error, "cannot remove default server") if servers.include?(:default)
157
+
158
+ sync do
159
+ servers.each do |server|
160
+ next unless @servers.has_key?(server)
161
+
162
+ queue = @queues[server]
163
+
164
+ while conn = queue.pop(timeout: 0)
165
+ @sizes[server] -= 1
166
+ conns << conn
167
+ end
168
+
169
+ unless @sizes[server] == 0
170
+ raise Sequel::Error, "cannot remove server #{server} as it has allocated connections"
171
+ end
172
+
173
+ @servers.delete(server)
174
+ @sizes.delete(server)
175
+ @queues.delete(server)
176
+ @allocated.delete(server)
177
+ end
178
+ end
179
+
180
+ nil
181
+ ensure
182
+ disconnect_connections(conns)
183
+ end
184
+
185
+ # Return an array of symbols for servers in the connection pool.
186
+ def servers
187
+ sync{@servers.keys}
188
+ end
189
+
190
+ def pool_type
191
+ :sharded_timed_queue
192
+ end
193
+
194
+ private
195
+
196
+ # Create a new connection, after the pool's current size has already
197
+ # been updated to account for the new connection. If there is an exception
198
+ # when creating the connection, decrement the current size.
199
+ #
200
+ # This should only be called after can_make_new?. If there is an exception
201
+ # between when can_make_new? is called and when preallocated_make_new
202
+ # is called, it has the effect of reducing the maximum size of the
203
+ # connection pool by 1, since the current size of the pool will show a
204
+ # higher number than the number of connections allocated or
205
+ # in the queue.
206
+ #
207
+ # Calling code should not have the mutex when calling this.
208
+ def preallocated_make_new(server)
209
+ make_new(server)
210
+ rescue Exception
211
+ sync{@sizes[server] -= 1}
212
+ raise
213
+ end
214
+
215
+ # Disconnect all available connections immediately, and schedule currently allocated connections for disconnection
216
+ # as soon as they are returned to the pool. The calling code should NOT
217
+ # have the mutex before calling this.
218
+ def disconnect_connections(conns)
219
+ conns.each{|conn| disconnect_connection(conn)}
220
+ end
221
+
222
+ # Decrement the current size of the pool for the server when disconnecting connections.
223
+ #
224
+ # Calling code should not have the mutex when calling this.
225
+ def disconnect_pool_connection(conn, server)
226
+ sync{@sizes[server] -= 1}
227
+ disconnect_connection(conn)
228
+ end
229
+
230
+ # If there are any threads waiting on the queue, try to create
231
+ # new connections in a separate thread if the pool is not yet at the
232
+ # maximum size.
233
+ #
234
+ # The reason for this method is to handle cases where acquire
235
+ # could not retrieve a connection immediately, and the pool
236
+ # was already at the maximum size. In that case, the acquire will
237
+ # wait on the queue until the timeout. This method is called
238
+ # after disconnecting to potentially add new connections to the
239
+ # pool, so the threads that are currently waiting for connections
240
+ # do not timeout after the pool is no longer full.
241
+ def fill_queue(server)
242
+ queue = sync{@queues[server]}
243
+ if queue.num_waiting > 0
244
+ Thread.new do
245
+ while queue.num_waiting > 0 && (conn = try_make_new(server))
246
+ queue.push(conn)
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ # Whether the given size is less than the maximum size of the pool.
253
+ # In that case, the pool's current size is incremented. If this
254
+ # method returns true, space in the pool for the connection is
255
+ # preallocated, and preallocated_make_new should be called to
256
+ # create the connection.
257
+ #
258
+ # Calling code should have the mutex when calling this.
259
+ def can_make_new?(server, current_size)
260
+ if @max_size > current_size
261
+ @sizes[server] += 1
262
+ end
263
+ end
264
+
265
+ # Try to make a new connection if there is space in the pool.
266
+ # If the pool is already full, look for dead threads/fibers and
267
+ # disconnect the related connections.
268
+ #
269
+ # Calling code should not have the mutex when calling this.
270
+ def try_make_new(server)
271
+ return preallocated_make_new(server) if sync{can_make_new?(server, @sizes[server])}
272
+
273
+ to_disconnect = nil
274
+ do_make_new = false
275
+
276
+ sync do
277
+ current_size = @sizes[server]
278
+ alloc = @allocated[server]
279
+ alloc.keys.each do |t|
280
+ unless t.alive?
281
+ (to_disconnect ||= []) << alloc.delete(t)
282
+ current_size -= 1
283
+ end
284
+ end
285
+
286
+ do_make_new = true if can_make_new?(server, current_size)
287
+ end
288
+
289
+ begin
290
+ preallocated_make_new(server) if do_make_new
291
+ ensure
292
+ if to_disconnect
293
+ to_disconnect.each{|conn| disconnect_pool_connection(conn, server)}
294
+ fill_queue(server)
295
+ end
296
+ end
297
+ end
298
+
299
+ # Assigns a connection to the supplied thread, if one
300
+ # is available.
301
+ #
302
+ # This should return a connection if one is available within the timeout,
303
+ # or raise PoolTimeout if a connection could not be acquired within the timeout.
304
+ #
305
+ # Calling code should not have the mutex when calling this.
306
+ def acquire(thread, server)
307
+ queue = sync{@queues[server]}
308
+ if conn = queue.pop(timeout: 0) || try_make_new(server) || queue.pop(timeout: @timeout)
309
+ sync{@allocated[server][thread] = conn}
310
+ else
311
+ name = db.opts[:name]
312
+ raise ::Sequel::PoolTimeout, "timeout: #{@timeout}, server: #{server}#{", database name: #{name}" if name}"
313
+ end
314
+ end
315
+
316
+ # Returns the connection owned by the supplied thread for the given server,
317
+ # if any. The calling code should NOT already have the mutex before calling this.
318
+ def owned_connection(thread, server)
319
+ sync{@allocated[server][thread]}
320
+ end
321
+
322
+ # If the server given is in the hash, return it, otherwise, return the default server.
323
+ def pick_server(server)
324
+ sync{@servers[server]}
325
+ end
326
+
327
+ # Create the maximum number of connections immediately. This should not be called
328
+ # with a true argument unless no code is currently operating on the database.
329
+ #
330
+ # Calling code should not have the mutex when calling this.
331
+ def preconnect(concurrent = false)
332
+ conn_servers = sync{@servers.keys}.map!{|s| Array.new(@max_size - @sizes[s], s)}.flatten!
333
+
334
+ if concurrent
335
+ conn_servers.map! do |server|
336
+ queue = sync{@queues[server]}
337
+ Thread.new do
338
+ if conn = try_make_new(server)
339
+ queue.push(conn)
340
+ end
341
+ end
342
+ end.each(&:value)
343
+ else
344
+ conn_servers.each do |server|
345
+ if conn = try_make_new(server)
346
+ sync{@queues[server]}.push(conn)
347
+ end
348
+ end
349
+ end
350
+
351
+ nil
352
+ end
353
+
354
+ # Releases the connection assigned to the supplied thread back to the pool.
355
+ #
356
+ # Calling code should not have the mutex when calling this.
357
+ def release(thread, _, server)
358
+ checkin_connection(sync{@allocated[server].delete(thread)}, server)
359
+ nil
360
+ end
361
+
362
+ # Adds a connection to the queue of available connections, returns the connection.
363
+ def checkin_connection(conn, server)
364
+ sync{@queues[server]}.push(conn)
365
+ conn
366
+ end
367
+
368
+ # Yield to the block while inside the mutex.
369
+ #
370
+ # Calling code should not have the mutex when calling this.
371
+ def sync
372
+ @mutex.synchronize{yield}
373
+ end
374
+ end
@@ -274,6 +274,12 @@ class Sequel::ThreadedConnectionPool < Sequel::ConnectionPool
274
274
  end
275
275
 
276
276
  @waiter.signal
277
+
278
+ # Ensure that after signalling the condition, some other thread is given the
279
+ # opportunity to acquire the mutex.
280
+ # See <https://github.com/socketry/async/issues/99> for more context.
281
+ sleep(0)
282
+
277
283
  nil
278
284
  end
279
285
 
@@ -81,7 +81,7 @@ class Sequel::TimedQueueConnectionPool < Sequel::ConnectionPool
81
81
  # connection can be acquired, a Sequel::PoolTimeout is raised.
82
82
  def hold(server=nil)
83
83
  t = Sequel.current
84
- if conn = sync{@allocated[t]}
84
+ if conn = owned_connection(t)
85
85
  return yield(conn)
86
86
  end
87
87
 
@@ -223,8 +223,14 @@ class Sequel::TimedQueueConnectionPool < Sequel::ConnectionPool
223
223
  end
224
224
  end
225
225
 
226
+ # Returns the connection owned by the supplied thread,
227
+ # if any. The calling code should NOT already have the mutex before calling this.
228
+ def owned_connection(thread)
229
+ sync{@allocated[thread]}
230
+ end
231
+
226
232
  # Create the maximum number of connections immediately. This should not be called
227
- # with a true argument unles no code is currently operating on the database.
233
+ # with a true argument unless no code is currently operating on the database.
228
234
  #
229
235
  # Calling code should not have the mutex when calling this.
230
236
  def preconnect(concurrent = false)
@@ -245,7 +251,14 @@ class Sequel::TimedQueueConnectionPool < Sequel::ConnectionPool
245
251
  #
246
252
  # Calling code should not have the mutex when calling this.
247
253
  def release(thread)
248
- @queue.push(sync{@allocated.delete(thread)})
254
+ checkin_connection(sync{@allocated.delete(thread)})
255
+ nil
256
+ end
257
+
258
+ # Adds a connection to the queue of available connections, returns the connection.
259
+ def checkin_connection(conn)
260
+ @queue.push(conn)
261
+ conn
249
262
  end
250
263
 
251
264
  # Yield to the block while inside the mutex.