sequel 5.68.0 → 5.70.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e682467c455e044847fffb313b6d8a669e0dc89ca8ea330468841de642b84227
4
- data.tar.gz: cd0687fd059a6b6d38cb31279d5cdacccd4a0a309f1155171f12551d43eacb0b
3
+ metadata.gz: 780e8bd3203a4aa27f70c94dd2ddd29e28826ec7b74b86296a2571d40ff2a74a
4
+ data.tar.gz: f600cfad2e5401f2c1150838ecacac0cd34ba38da4a69bd3afd580dbadaee35f
5
5
  SHA512:
6
- metadata.gz: 3c16f027251aa67e04cab0973a57da21682e083c7362f37bdbf84937266ca6878e200fb7c44a0583b8ac6fab62d1fd4b55744929760fc63a9d1f850452db5df6
7
- data.tar.gz: 60eee01297c47ee9c18ae76b6974b6eb3e492e58c7f408ee9c32476f1ae0ae5848ffc47cb80889f7070b02088e7e66509b64e460a1ef61c90ffb5a59da6a3c09
6
+ metadata.gz: b22192e34739b2158a543f18f51b52b44063b86b02c6968a8f8e1ff2deec8338fa4f81176a4f5f2b27ace6fcefe9d3511ccaa73c43e2b660743a3a736f303080
7
+ data.tar.gz: 4540e3f53f53d6fe8b7a22212ab7d6678502e87bf67a5081c22d1da02ea8ea2090589e384fe17e4ca454c0c4c45358f1aaed1772d27e7732f7f68071e5914ea6
data/CHANGELOG CHANGED
@@ -1,3 +1,29 @@
1
+ === 5.70.0 (2023-07-01)
2
+
3
+ * Make static_cache plugin better handle cases where forbid_lazy_load plugin is already loaded (jeremyevans)
4
+
5
+ * Fix ShardedThreadedConnectionPool#remove_server to disconnect all connections if removing multiple servers (jeremyevans)
6
+
7
+ * Support SEQUEL_DEFAULT_CONNECTION_POOL environment variable for choosing connection pool when :pool_class Database option is not set (jeremyevans)
8
+
9
+ * Add sharded_timed_queue connection pool (jeremyevans)
10
+
11
+ * Make connection_{validator,expiration} and async_thread_pool extensions work with timed_queue connection pool (jeremyevans)
12
+
13
+ * Make connection_{validator,expiration} extensions raise error when used with single threaded pools (HoneyryderChuck, jeremyevans) (#2049)
14
+
15
+ * Workaround possible resource starvation in threaded connection pool (ioquatix) (#2048)
16
+
17
+ === 5.69.0 (2023-06-01)
18
+
19
+ * Avoid unsupported flag warning when using the mysql adapter with ruby-mysql 3+ (jeremyevans)
20
+
21
+ * Make mysql adapter work with ruby-mysql 4+ (jeremyevans)
22
+
23
+ * Add Model::DatasetModule#model accessor (davekaro) (#2040)
24
+
25
+ * Add trilogy adapter (jeremyevans)
26
+
1
27
  === 5.68.0 (2023-05-01)
2
28
 
3
29
  * Add validation_helpers_generic_type_messages plugin for more useful type validation failure messages (jeremyevans) (#2028)
data/README.rdoc CHANGED
@@ -13,7 +13,7 @@ toolkit for Ruby.
13
13
  database sharding.
14
14
  * Sequel currently has adapters for ADO, Amalgalite,
15
15
  IBM_DB, JDBC, MySQL, Mysql2, ODBC, Oracle,
16
- PostgreSQL, SQLAnywhere, SQLite3, and TinyTDS.
16
+ PostgreSQL, SQLAnywhere, SQLite3, TinyTDS, and Trilogy.
17
17
 
18
18
  == Resources
19
19
 
@@ -279,7 +279,7 @@ if either the :sslca or :sslkey option is given.
279
279
 
280
280
  === mysql2
281
281
 
282
- This is a newer MySQL adapter that does typecasting in C, so it is often faster than the
282
+ This is a MySQL adapter that does typecasting in C, so it is often faster than the
283
283
  mysql adapter. The options given are passed to Mysql2::Client.new, see the mysql2 documentation
284
284
  for details on what options are supported. The :timeout, :auto_is_null, :sql_mode, and :disable_split_materialized
285
285
  options supported by the mysql adapter are also supported for mysql2 adapter (and any other adapters connecting to
@@ -423,3 +423,10 @@ Other Sequel specific options:
423
423
  This should be specified as an integer. If you plan on setting large
424
424
  text or blob values via tinytds, you should use this option or modify
425
425
  your freetds.conf file.
426
+
427
+ === trilogy
428
+
429
+ This is a MySQL adapter that does typecasting in C, and does not require any mysql client libraries installed.
430
+ The options given are passed to Trilogy.new, see the trilogy documentation for details on what options are
431
+ supported. The :timeout, :auto_is_null, :sql_mode, and :disable_split_materialized
432
+ options supported by the mysql adapter are also supported for trilogy adapter.
@@ -0,0 +1,26 @@
1
+ = New Features
2
+
3
+ * An adapter has been added for the trilogy MySQL driver. One large
4
+ advantage over mysql2 is that trilogy does not require any MySQL
5
+ client libraries installed on the machine. The trilogy adapter
6
+ has basically the same issues/skipped specs as the mysql2 adapter,
7
+ but it also does not support an application_timezone different
8
+ than the database_timezone.
9
+
10
+ * Model dataset modules now have a model accessor, allowing for
11
+ code such as:
12
+
13
+ class Foo < Sequel::Model
14
+ dataset_module do
15
+ where :kept, Sequel[model.table_name][:discarded_at] => nil
16
+ end
17
+ end
18
+
19
+ = Improvements
20
+
21
+ * The mysql adapter now works with ruby-mysql 4 (the pure-ruby
22
+ MySQL driver). Note that multi-results support does not work
23
+ with ruby-mysql 4 (it doesn't work with mysql2, trilogy, or
24
+ other Sequel adapters in general).
25
+
26
+ * Warnings for unsupported flags are now avoided on ruby-mysql 3.
@@ -0,0 +1,35 @@
1
+ = New Features
2
+
3
+ * A sharded_timed_queue connection pool has been added. This offers
4
+ most of the same features as the sharded_threaded connection pool,
5
+ but uses the new Queue#pop :timeout features added in Ruby 3.2 to
6
+ allow for a simpler and possibly faster and more robust
7
+ implementation.
8
+
9
+ * If a :pool_class option is not specified when creating a Database,
10
+ Sequel will now look at the SEQUEL_DEFAULT_CONNECTION_POOL
11
+ environment variable to determine the connection pool class to use.
12
+ This allows you to set SEQUEL_DEFAULT_CONNECTION_POOL=timed_queue
13
+ on Ruby 3.2 to test with the timed_queue connection pool without
14
+ making any code changes. If the :servers Database option is given,
15
+ Sequel will automatically use the sharded version of the connection
16
+ pool specified by SEQUEL_DEFAULT_CONNECTION_POOL.
17
+
18
+ = Other Improvements
19
+
20
+ * The connection_validator, connection_expiration, and
21
+ async_thread_pool extensions now work with the timed_queue and
22
+ sharded_timed_queue connection pools.
23
+
24
+ * The sharded_threaded connection pool now disconnects connections
25
+ for all specified servers instead of just the last specified server
26
+ when using remove_server.
27
+
28
+ * The static_cache plugin now recognizes when the forbid_lazy_load
29
+ plugin is already loaded, and does not return instances that
30
+ forbid lazy load for methods that return a single object, such as
31
+ Database.{[],cache_get_pk,first}.
32
+
33
+ * Sequel now displays an informative error message if attempting to
34
+ load the connection_validator or connection_expiration extensions
35
+ when using the single threaded connection pool.
data/doc/sharding.rdoc CHANGED
@@ -39,7 +39,9 @@ is the simplest configuration:
39
39
  servers: {read_only: {host: 'replica_server'}})
40
40
 
41
41
  This will use the replica_server for SELECT queries and primary_server for
42
- other queries.
42
+ other queries. The :read_only key in the :servers hash is special in that
43
+ it sets the default database for Dataset methods that use SELECT queries
44
+ (which are generally read queries that do not modify the database).
43
45
 
44
46
  If you want to ensure your queries are going to a specific database, you
45
47
  can force this for a given query by using the .server method and passing
@@ -29,6 +29,21 @@ module Sequel
29
29
  end
30
30
  MYSQL_TYPES.freeze
31
31
 
32
+ RUBY_MYSQL_3 = !Mysql.respond_to?(:init)
33
+ RUBY_MYSQL_4 = RUBY_MYSQL_3 && ::Mysql::VERSION.to_i >= 4
34
+
35
+ if RUBY_MYSQL_3
36
+ class Adapter < ::Mysql
37
+ alias real_connect connect
38
+ alias use_result store_result
39
+ if RUBY_MYSQL_4
40
+ def initialize(**opts)
41
+ super(**opts.merge(:cast=>false))
42
+ end
43
+ end
44
+ end
45
+ end
46
+
32
47
  class Database < Sequel::Database
33
48
  include Sequel::MySQL::DatabaseMethods
34
49
  include Sequel::MySQL::MysqlMysql2::DatabaseMethods
@@ -72,7 +87,7 @@ module Sequel
72
87
  def connect(server)
73
88
  opts = server_opts(server)
74
89
 
75
- if Mysql.respond_to?(:init)
90
+ if !RUBY_MYSQL_3
76
91
  conn = Mysql.init
77
92
  conn.options(Mysql::READ_DEFAULT_GROUP, opts[:config_default_group] || "client")
78
93
  conn.options(Mysql::OPT_LOCAL_INFILE, opts[:config_local_infile]) if opts.has_key?(:config_local_infile)
@@ -88,8 +103,8 @@ module Sequel
88
103
  conn.options(Mysql::OPT_CONNECT_TIMEOUT, connect_timeout)
89
104
  end
90
105
  else
91
- # ruby-mysql 3 API
92
- conn = Mysql.new
106
+ # ruby-mysql 3+ API
107
+ conn = Adapter.new
93
108
  # no support for default group
94
109
  conn.local_infile = opts[:config_local_infile] if opts.has_key?(:config_local_infile)
95
110
  if encoding = opts[:encoding] || opts[:charset]
@@ -101,10 +116,7 @@ module Sequel
101
116
  if connect_timeout = opts[:connect_timeout]
102
117
  conn.connect_timeout = connect_timeout
103
118
  end
104
- conn.singleton_class.class_eval do
105
- alias real_connect connect
106
- alias use_result store_result
107
- end
119
+ opts[:compress] = false
108
120
  end
109
121
 
110
122
  conn.ssl_set(opts[:sslkey], opts[:sslcert], opts[:sslca], opts[:sslcapath], opts[:sslcipher]) if opts[:sslca] || opts[:sslkey]
@@ -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.
@@ -32,6 +32,7 @@ class Sequel::ConnectionPool
32
32
  :sharded_threaded => :ShardedThreadedConnectionPool,
33
33
  :sharded_single => :ShardedSingleConnectionPool,
34
34
  :timed_queue => :TimedQueueConnectionPool,
35
+ :sharded_timed_queue => :ShardedTimedQueueConnectionPool,
35
36
  }
36
37
  POOL_CLASS_MAP.to_a.each{|k, v| POOL_CLASS_MAP[k.to_s] = v}
37
38
  POOL_CLASS_MAP.freeze
@@ -42,7 +43,8 @@ class Sequel::ConnectionPool
42
43
  # Return a pool subclass instance based on the given options. If a <tt>:pool_class</tt>
43
44
  # option is provided is provided, use that pool class, otherwise
44
45
  # use a new instance of an appropriate pool subclass based on the
45
- # <tt>:single_threaded</tt> and <tt>:servers</tt> options.
46
+ # +SEQUEL_DEFAULT_CONNECTION_POOL+ environment variable if set, or
47
+ # the <tt>:single_threaded</tt> and <tt>:servers</tt> options, otherwise.
46
48
  def get_pool(db, opts = OPTS)
47
49
  connection_pool_class(opts).new(db, opts)
48
50
  end
@@ -62,9 +64,14 @@ class Sequel::ConnectionPool
62
64
  end
63
65
 
64
66
  pc
67
+ elsif pc = ENV['SEQUEL_DEFAULT_CONNECTION_POOL']
68
+ pc = "sharded_#{pc}" if opts[:servers] && !pc.start_with?('sharded_')
69
+ connection_pool_class(:pool_class=>pc)
65
70
  else
66
71
  pc = if opts[:single_threaded]
67
72
  opts[:servers] ? :sharded_single : :single
73
+ #elsif RUBY_VERSION >= '3.2' # SEQUEL6 or maybe earlier
74
+ # opts[:servers] ? :sharded_timed_queue : :timed_queue
68
75
  else
69
76
  opts[:servers] ? :sharded_threaded : :threaded
70
77
  end
@@ -8,7 +8,7 @@ module Sequel
8
8
  # ---------------------
9
9
 
10
10
  # Array of supported database adapters
11
- ADAPTERS = %w'ado amalgalite ibmdb jdbc mock mysql mysql2 odbc oracle postgres sqlanywhere sqlite tinytds'.map(&:to_sym)
11
+ ADAPTERS = %w'ado amalgalite ibmdb jdbc mock mysql mysql2 odbc oracle postgres sqlanywhere sqlite tinytds trilogy'.map(&:to_sym)
12
12
 
13
13
  # The Database subclass for the given adapter scheme.
14
14
  # Raises Sequel::AdapterNotFound if the adapter
@@ -338,8 +338,9 @@ module Sequel
338
338
  module DatabaseMethods
339
339
  def self.extended(db)
340
340
  db.instance_exec do
341
- unless pool.pool_type == :threaded || pool.pool_type == :sharded_threaded
342
- raise Error, "can only load async_thread_pool extension if using threaded or sharded_threaded connection pool"
341
+ case pool.pool_type
342
+ when :single, :sharded_single
343
+ raise Error, "cannot load async_thread_pool extension if using single or sharded_single connection pool"
343
344
  end
344
345
 
345
346
  num_async_threads = opts[:num_async_threads] ? typecast_value_integer(opts[:num_async_threads]) : (Integer(opts[:max_connections] || 4))
@@ -15,16 +15,16 @@
15
15
  #
16
16
  # DB.pool.connection_expiration_timeout = 3600 # 1 hour
17
17
  #
18
- # Note that this extension only affects the default threaded
19
- # and the sharded threaded connection pool. The single
20
- # threaded and sharded single threaded connection pools are
21
- # not affected. As the only reason to use the single threaded
18
+ # Note that this extension does not work with the single
19
+ # threaded and sharded single threaded connection pools.
20
+ # As the only reason to use the single threaded
22
21
  # pools is for speed, and this extension makes the connection
23
22
  # pool slower, there's not much point in modifying this
24
23
  # extension to work with the single threaded pools. The
25
- # threaded pools work fine even in single threaded code, so if
26
- # you are currently using a single threaded pool and want to
27
- # use this extension, switch to using a threaded pool.
24
+ # non-single threaded pools work fine even in single threaded
25
+ # code, so if you are currently using a single threaded pool
26
+ # and want to use this extension, switch to using another
27
+ # pool.
28
28
  #
29
29
  # Related module: Sequel::ConnectionExpiration
30
30
 
@@ -45,6 +45,11 @@ module Sequel
45
45
 
46
46
  # Initialize the data structures used by this extension.
47
47
  def self.extended(pool)
48
+ case pool.pool_type
49
+ when :single, :sharded_single
50
+ raise Error, "cannot load connection_expiration extension if using single or sharded_single connection pool"
51
+ end
52
+
48
53
  pool.instance_exec do
49
54
  sync do
50
55
  @connection_expiration_timestamps ||= {}
@@ -79,8 +84,9 @@ module Sequel
79
84
  (cet = sync{@connection_expiration_timestamps[conn]}) &&
80
85
  Sequel.elapsed_seconds_since(cet[0]) > cet[1]
81
86
 
82
- if pool_type == :sharded_threaded
83
- sync{allocated(a.last).delete(Sequel.current)}
87
+ case pool_type
88
+ when :sharded_threaded, :sharded_timed_queue
89
+ sync{@allocated[a.last].delete(Sequel.current)}
84
90
  else
85
91
  sync{@allocated.delete(Sequel.current)}
86
92
  end
@@ -34,16 +34,16 @@
34
34
  # web requests to the number to connections in the database
35
35
  # connection pool.
36
36
  #
37
- # Note that this extension only affects the default threaded
38
- # and the sharded threaded connection pool. The single
39
- # threaded and sharded single threaded connection pools are
40
- # not affected. As the only reason to use the single threaded
37
+ # Note that this extension does not work with the single
38
+ # threaded and sharded single threaded connection pools.
39
+ # As the only reason to use the single threaded
41
40
  # pools is for speed, and this extension makes the connection
42
41
  # pool slower, there's not much point in modifying this
43
42
  # extension to work with the single threaded pools. The
44
- # threaded pools work fine even in single threaded code, so if
45
- # you are currently using a single threaded pool and want to
46
- # use this extension, switch to using a threaded pool.
43
+ # non-single threaded pools work fine even in single threaded
44
+ # code, so if you are currently using a single threaded pool
45
+ # and want to use this extension, switch to using another
46
+ # pool.
47
47
  #
48
48
  # Related module: Sequel::ConnectionValidator
49
49
 
@@ -61,6 +61,11 @@ module Sequel
61
61
 
62
62
  # Initialize the data structures used by this extension.
63
63
  def self.extended(pool)
64
+ case pool.pool_type
65
+ when :single, :sharded_single
66
+ raise Error, "cannot load connection_validator extension if using single or sharded_single connection pool"
67
+ end
68
+
64
69
  pool.instance_exec do
65
70
  sync do
66
71
  @connection_timestamps ||= {}
@@ -103,8 +108,9 @@ module Sequel
103
108
  Sequel.elapsed_seconds_since(timer) > @connection_validation_timeout &&
104
109
  !db.valid_connection?(conn)
105
110
 
106
- if pool_type == :sharded_threaded
107
- sync{allocated(a.last).delete(Sequel.current)}
111
+ case pool_type
112
+ when :sharded_threaded, :sharded_timed_queue
113
+ sync{@allocated[a.last].delete(Sequel.current)}
108
114
  else
109
115
  sync{@allocated.delete(Sequel.current)}
110
116
  end
@@ -120,4 +126,3 @@ module Sequel
120
126
 
121
127
  Database.register_extension(:connection_validator){|db| db.pool.extend(ConnectionValidator)}
122
128
  end
123
-
@@ -69,7 +69,8 @@ module Sequel
69
69
  # Also defines the with_server method on the receiver for easy use.
70
70
  def self.extended(db)
71
71
  pool = db.pool
72
- if defined?(ShardedThreadedConnectionPool) && pool.is_a?(ShardedThreadedConnectionPool)
72
+ case pool.pool_type
73
+ when :sharded_threaded, :sharded_timed_queue
73
74
  pool.extend(ThreadedServerBlock)
74
75
  pool.instance_variable_set(:@default_servers, {})
75
76
  else
@@ -8,6 +8,9 @@ module Sequel
8
8
  # automatically creates class methods for public dataset
9
9
  # methods.
10
10
  class DatasetModule < Dataset::DatasetModule
11
+ # The model class related to this dataset module.
12
+ attr_reader :model
13
+
11
14
  # Store the model related to this dataset module.
12
15
  def initialize(model)
13
16
  @model = model
@@ -64,6 +64,9 @@ module Sequel
64
64
  def self.configure(model, opts=OPTS)
65
65
  model.instance_exec do
66
66
  @static_cache_frozen = opts.fetch(:frozen, true)
67
+ if @static_cache_frozen && defined?(::Sequel::Plugins::ForbidLazyLoad::ClassMethods) && is_a?(::Sequel::Plugins::ForbidLazyLoad::ClassMethods)
68
+ extend ForbidLazyLoadClassMethods
69
+ end
67
70
  load_cache
68
71
  end
69
72
  end
@@ -246,6 +249,41 @@ module Sequel
246
249
  end
247
250
  end
248
251
 
252
+ module ForbidLazyLoadClassMethods
253
+ # Do not forbid lazy loading for single object retrieval.
254
+ def cache_get_pk(pk)
255
+ primary_key_lookup(pk)
256
+ end
257
+
258
+ # Use static cache to return first arguments.
259
+ def first(*args)
260
+ if !defined?(yield) && args.empty?
261
+ if o = @all.first
262
+ _static_cache_frozen_copy(o)
263
+ end
264
+ else
265
+ super
266
+ end
267
+ end
268
+
269
+ private
270
+
271
+ # Return a frozen copy of the object that does not have lazy loading
272
+ # forbidden.
273
+ def _static_cache_frozen_copy(o)
274
+ o = call(Hash[o.values])
275
+ o.errors.freeze
276
+ o.freeze
277
+ end
278
+
279
+ # Do not forbid lazy loading for single object retrieval.
280
+ def primary_key_lookup(pk)
281
+ if o = cache[pk]
282
+ _static_cache_frozen_copy(o)
283
+ end
284
+ end
285
+ end
286
+
249
287
  module InstanceMethods
250
288
  # Disallowing destroying the object unless the frozen: false option was used.
251
289
  def before_destroy
@@ -6,7 +6,7 @@ module Sequel
6
6
 
7
7
  # The minor version of Sequel. Bumped for every non-patch level
8
8
  # release, generally around once a month.
9
- MINOR = 68
9
+ MINOR = 70
10
10
 
11
11
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
12
12
  # releases that fix regressions from previous versions.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.68.0
4
+ version: 5.70.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-01 00:00:00.000000000 Z
11
+ date: 2023-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -200,7 +200,9 @@ extra_rdoc_files:
200
200
  - doc/release_notes/5.66.0.txt
201
201
  - doc/release_notes/5.67.0.txt
202
202
  - doc/release_notes/5.68.0.txt
203
+ - doc/release_notes/5.69.0.txt
203
204
  - doc/release_notes/5.7.0.txt
205
+ - doc/release_notes/5.70.0.txt
204
206
  - doc/release_notes/5.8.0.txt
205
207
  - doc/release_notes/5.9.0.txt
206
208
  files:
@@ -296,7 +298,9 @@ files:
296
298
  - doc/release_notes/5.66.0.txt
297
299
  - doc/release_notes/5.67.0.txt
298
300
  - doc/release_notes/5.68.0.txt
301
+ - doc/release_notes/5.69.0.txt
299
302
  - doc/release_notes/5.7.0.txt
303
+ - doc/release_notes/5.70.0.txt
300
304
  - doc/release_notes/5.8.0.txt
301
305
  - doc/release_notes/5.9.0.txt
302
306
  - doc/schema_modification.rdoc
@@ -349,6 +353,7 @@ files:
349
353
  - lib/sequel/adapters/sqlanywhere.rb
350
354
  - lib/sequel/adapters/sqlite.rb
351
355
  - lib/sequel/adapters/tinytds.rb
356
+ - lib/sequel/adapters/trilogy.rb
352
357
  - lib/sequel/adapters/utils/columns_limit_1.rb
353
358
  - lib/sequel/adapters/utils/emulate_offset_with_reverse_and_count.rb
354
359
  - lib/sequel/adapters/utils/emulate_offset_with_row_number.rb
@@ -362,6 +367,7 @@ files:
362
367
  - lib/sequel/connection_pool.rb
363
368
  - lib/sequel/connection_pool/sharded_single.rb
364
369
  - lib/sequel/connection_pool/sharded_threaded.rb
370
+ - lib/sequel/connection_pool/sharded_timed_queue.rb
365
371
  - lib/sequel/connection_pool/single.rb
366
372
  - lib/sequel/connection_pool/threaded.rb
367
373
  - lib/sequel/connection_pool/timed_queue.rb