pools 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +16 -0
- data/Rakefile +10 -0
- data/lib/pools.rb +4 -0
- data/lib/pools/connection_pool.rb +236 -0
- data/lib/pools/handler.rb +62 -0
- data/lib/pools/middleware.rb +17 -0
- data/lib/pools/pooled.rb +39 -0
- data/lib/redis/pooled.rb +43 -0
- data/lib/redis/pooled_store.rb +40 -0
- metadata +77 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) Michael Rykov
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
data/lib/pools.rb
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
##
|
2
|
+
# This file adapted from activerecord gem
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'thread'
|
6
|
+
require 'monitor'
|
7
|
+
require 'set'
|
8
|
+
require 'active_support/core_ext/module/synchronization'
|
9
|
+
|
10
|
+
module Pools
|
11
|
+
# Raised when a connection could not be obtained within the connection
|
12
|
+
# acquisition timeout period.
|
13
|
+
ConnectionNotEstablished = Class.new(StandardError)
|
14
|
+
ConnectionTimeoutError = Class.new(ConnectionNotEstablished)
|
15
|
+
|
16
|
+
# Connection pool base class for managing Active Record database
|
17
|
+
# connections.
|
18
|
+
#
|
19
|
+
# == Introduction
|
20
|
+
#
|
21
|
+
# A connection pool synchronizes thread access to a limited number of
|
22
|
+
# database connections. The basic idea is that each thread checks out a
|
23
|
+
# database connection from the pool, uses that connection, and checks the
|
24
|
+
# connection back in. ConnectionPool is completely thread-safe, and will
|
25
|
+
# ensure that a connection cannot be used by two threads at the same time,
|
26
|
+
# as long as ConnectionPool's contract is correctly followed. It will also
|
27
|
+
# handle cases in which there are more threads than connections: if all
|
28
|
+
# connections have been checked out, and a thread tries to checkout a
|
29
|
+
# connection anyway, then ConnectionPool will wait until some other thread
|
30
|
+
# has checked in a connection.
|
31
|
+
#
|
32
|
+
# == Obtaining (checking out) a connection
|
33
|
+
#
|
34
|
+
# Connections can be obtained and used from a connection pool in several
|
35
|
+
# ways:
|
36
|
+
#
|
37
|
+
# 1. Simply use ActiveRecord::Base.connection as with Active Record 2.1 and
|
38
|
+
# earlier (pre-connection-pooling). Eventually, when you're done with
|
39
|
+
# the connection(s) and wish it to be returned to the pool, you call
|
40
|
+
# ActiveRecord::Base.clear_active_connections!. This will be the
|
41
|
+
# default behavior for Active Record when used in conjunction with
|
42
|
+
# Action Pack's request handling cycle.
|
43
|
+
# 2. Manually check out a connection from the pool with
|
44
|
+
# ActiveRecord::Base.connection_pool.checkout. You are responsible for
|
45
|
+
# returning this connection to the pool when finished by calling
|
46
|
+
# ActiveRecord::Base.connection_pool.checkin(connection).
|
47
|
+
# 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which
|
48
|
+
# obtains a connection, yields it as the sole argument to the block,
|
49
|
+
# and returns it to the pool after the block completes.
|
50
|
+
#
|
51
|
+
# Connections in the pool are actually AbstractAdapter objects (or objects
|
52
|
+
# compatible with AbstractAdapter's interface).
|
53
|
+
#
|
54
|
+
# == Options
|
55
|
+
#
|
56
|
+
# There are two connection-pooling-related options that you can add to
|
57
|
+
# your database connection configuration:
|
58
|
+
#
|
59
|
+
# * +pool+: number indicating size of connection pool (default 5)
|
60
|
+
# * +wait_timeout+: number of seconds to block and wait for a connection
|
61
|
+
# before giving up and raising a timeout error (default 5 seconds).
|
62
|
+
class ConnectionPool
|
63
|
+
attr_reader :options, :connections
|
64
|
+
|
65
|
+
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
|
66
|
+
# object which describes database connection information (e.g. adapter,
|
67
|
+
# host name, username, password, etc), as well as the maximum size for
|
68
|
+
# this ConnectionPool.
|
69
|
+
#
|
70
|
+
# The default ConnectionPool maximum size is 5.
|
71
|
+
def initialize(pooled, options)
|
72
|
+
@pooled = pooled
|
73
|
+
@options = options
|
74
|
+
|
75
|
+
# The cache of reserved connections mapped to threads
|
76
|
+
@reserved_connections = {}
|
77
|
+
|
78
|
+
# The mutex used to synchronize pool access
|
79
|
+
@connection_mutex = Monitor.new
|
80
|
+
@queue = @connection_mutex.new_cond
|
81
|
+
|
82
|
+
# default 5 second timeout unless on ruby 1.9
|
83
|
+
@timeout = options[:wait_timeout] || 5
|
84
|
+
|
85
|
+
# default max pool size to 5
|
86
|
+
@size = (options[:pool] && options[:pool].to_i) || 5
|
87
|
+
|
88
|
+
@connections = []
|
89
|
+
@checked_out = []
|
90
|
+
end
|
91
|
+
|
92
|
+
# Retrieve the connection associated with the current thread, or call
|
93
|
+
# #checkout to obtain one if necessary.
|
94
|
+
#
|
95
|
+
# #connection can be called any number of times; the connection is
|
96
|
+
# held in a hash keyed by the thread id.
|
97
|
+
def connection
|
98
|
+
@reserved_connections[current_connection_id] ||= checkout
|
99
|
+
end
|
100
|
+
|
101
|
+
# Signal that the thread is finished with the current connection.
|
102
|
+
# #release_connection releases the connection-thread association
|
103
|
+
# and returns the connection to the pool.
|
104
|
+
def release_connection(with_id = current_connection_id)
|
105
|
+
conn = @reserved_connections.delete(with_id)
|
106
|
+
checkin conn if conn
|
107
|
+
end
|
108
|
+
|
109
|
+
# If a connection already exists yield it to the block. If no connection
|
110
|
+
# exists checkout a connection, yield it to the block, and checkin the
|
111
|
+
# connection when finished.
|
112
|
+
def with_connection
|
113
|
+
connection_id = current_connection_id
|
114
|
+
fresh_connection = true unless @reserved_connections[connection_id]
|
115
|
+
yield connection
|
116
|
+
ensure
|
117
|
+
release_connection(connection_id) if fresh_connection
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns true if a connection has already been opened.
|
121
|
+
def connected?
|
122
|
+
!@connections.empty?
|
123
|
+
end
|
124
|
+
|
125
|
+
# Disconnects all connections in the pool, and clears the pool.
|
126
|
+
def disconnect!
|
127
|
+
@reserved_connections.each do |name,conn|
|
128
|
+
checkin conn
|
129
|
+
end
|
130
|
+
@reserved_connections = {}
|
131
|
+
@connections.each do |conn|
|
132
|
+
@pooled.__disconnect(conn)
|
133
|
+
end
|
134
|
+
@connections = []
|
135
|
+
end
|
136
|
+
|
137
|
+
# Verify active connections and remove and disconnect connections
|
138
|
+
# associated with stale threads.
|
139
|
+
def verify_active_connections! #:nodoc:
|
140
|
+
clear_stale_cached_connections!
|
141
|
+
@connections.each do |connection|
|
142
|
+
@pooled.__disconnect(connection)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Return any checked-out connections back to the pool by threads that
|
147
|
+
# are no longer alive.
|
148
|
+
def clear_stale_cached_connections!
|
149
|
+
keys = @reserved_connections.keys - Thread.list.find_all { |t|
|
150
|
+
t.alive?
|
151
|
+
}.map { |thread| thread.object_id }
|
152
|
+
keys.each do |key|
|
153
|
+
checkin @reserved_connections[key]
|
154
|
+
@reserved_connections.delete(key)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Check-out a database connection from the pool, indicating that you want
|
159
|
+
# to use it. You should call #checkin when you no longer need this.
|
160
|
+
#
|
161
|
+
# This is done by either returning an existing connection, or by creating
|
162
|
+
# a new connection. If the maximum number of connections for this pool has
|
163
|
+
# already been reached, but the pool is empty (i.e. they're all being used),
|
164
|
+
# then this method will wait until a thread has checked in a connection.
|
165
|
+
# The wait time is bounded however: if no connection can be checked out
|
166
|
+
# within the timeout specified for this pool, then a ConnectionTimeoutError
|
167
|
+
# exception will be raised.
|
168
|
+
#
|
169
|
+
# Returns: an AbstractAdapter object.
|
170
|
+
#
|
171
|
+
# Raises:
|
172
|
+
# - ConnectionTimeoutError: no connection can be obtained from the pool
|
173
|
+
# within the timeout period.
|
174
|
+
def checkout
|
175
|
+
# Checkout an available connection
|
176
|
+
@connection_mutex.synchronize do
|
177
|
+
loop do
|
178
|
+
conn = if @checked_out.size < @connections.size
|
179
|
+
checkout_existing_connection
|
180
|
+
elsif @connections.size < @size
|
181
|
+
checkout_new_connection
|
182
|
+
end
|
183
|
+
return conn if conn
|
184
|
+
|
185
|
+
@queue.wait(@timeout)
|
186
|
+
|
187
|
+
if(@checked_out.size < @connections.size)
|
188
|
+
next
|
189
|
+
else
|
190
|
+
clear_stale_cached_connections!
|
191
|
+
if @size == @checked_out.size
|
192
|
+
raise ConnectionTimeoutError, "could not obtain a pooled connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it."
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Check-in a database connection back into the pool, indicating that you
|
201
|
+
# no longer need this connection.
|
202
|
+
#
|
203
|
+
# +conn+: an AbstractAdapter object, which was obtained by earlier by
|
204
|
+
# calling +checkout+ on this pool.
|
205
|
+
def checkin(conn)
|
206
|
+
@connection_mutex.synchronize do
|
207
|
+
@checked_out.delete conn
|
208
|
+
@queue.signal
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
synchronize :verify_active_connections!, :connected?, :disconnect!,
|
213
|
+
:with => :@connection_mutex
|
214
|
+
|
215
|
+
private
|
216
|
+
def current_connection_id #:nodoc:
|
217
|
+
Thread.current.object_id
|
218
|
+
end
|
219
|
+
|
220
|
+
def checkout_new_connection
|
221
|
+
c = @pooled.__connection
|
222
|
+
@connections << c
|
223
|
+
checkout_connection(c)
|
224
|
+
end
|
225
|
+
|
226
|
+
def checkout_existing_connection
|
227
|
+
c = (@connections - @checked_out).first
|
228
|
+
checkout_connection(c)
|
229
|
+
end
|
230
|
+
|
231
|
+
def checkout_connection(c)
|
232
|
+
@checked_out << c
|
233
|
+
c
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
##
|
2
|
+
# This file adapted from activerecord gem
|
3
|
+
#
|
4
|
+
|
5
|
+
module Pools
|
6
|
+
class Handler
|
7
|
+
attr_reader :pools
|
8
|
+
|
9
|
+
def initialize(pools = {})
|
10
|
+
@pools = pools
|
11
|
+
end
|
12
|
+
|
13
|
+
# Add a new connection pool to the mix
|
14
|
+
def add(pool, name = nil)
|
15
|
+
@pools[name || pool.object_id] = pool
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns any connections in use by the current thread back to the
|
19
|
+
# pool, and also returns connections to the pool cached by threads
|
20
|
+
# that are no longer alive.
|
21
|
+
def clear_active_connections!
|
22
|
+
@pools.each_value {|pool| pool.release_connection }
|
23
|
+
end
|
24
|
+
|
25
|
+
def clear_all_connections!
|
26
|
+
@pools.each_value {|pool| pool.disconnect! }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Verify active connections.
|
30
|
+
def verify_active_connections! #:nodoc:
|
31
|
+
@pools.each_value {|pool| pool.verify_active_connections! }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns true if a connection that's accessible to this class has
|
35
|
+
# already been opened.
|
36
|
+
def connected?(name)
|
37
|
+
conn = retrieve_connection_pool(name)
|
38
|
+
conn && conn.connected?
|
39
|
+
end
|
40
|
+
|
41
|
+
# Remove the connection for this class. This will close the active
|
42
|
+
# connection and the defined connection (if they exist). The result
|
43
|
+
# can be used as an argument for establish_connection, for easily
|
44
|
+
# re-establishing the connection.
|
45
|
+
def remove_connection(name)
|
46
|
+
pool = retrieve_connection_pool(name)
|
47
|
+
return nil unless pool
|
48
|
+
|
49
|
+
@pools.delete_if { |key, value| value == pool }
|
50
|
+
pool.disconnect!
|
51
|
+
end
|
52
|
+
|
53
|
+
def retrieve_connection_pool(name)
|
54
|
+
pool = @pools[name]
|
55
|
+
return pool if pool
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.handler
|
60
|
+
@@pool_handler ||= Handler.new
|
61
|
+
end
|
62
|
+
end
|
data/lib/pools/pooled.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'active_support/core_ext/array/extract_options'
|
2
|
+
require 'active_support/concern'
|
3
|
+
|
4
|
+
module Pools
|
5
|
+
module Pooled
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
attr_reader :connection_pool
|
8
|
+
|
9
|
+
def initialize(*args)
|
10
|
+
options = args.extract_options!
|
11
|
+
@connection_pool = ConnectionPool.new(self, options)
|
12
|
+
Pools.handler.add(@connection_pool, options[:pool_name])
|
13
|
+
end
|
14
|
+
|
15
|
+
def with_connection(&block)
|
16
|
+
@connection_pool.with_connection(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def __connection
|
20
|
+
# Override in parent
|
21
|
+
end
|
22
|
+
|
23
|
+
def __disconnect(connection)
|
24
|
+
# Override in parent
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
def connection_methods(*methods)
|
29
|
+
methods.each do |method|
|
30
|
+
define_method(method) do |*params|
|
31
|
+
with_connection do |client|
|
32
|
+
client.send(method, *params)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/redis/pooled.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
class Pooled
|
5
|
+
include ::Pools::Pooled
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@redis_options = options
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def __connection
|
13
|
+
Redis.connect(@redis_options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def __disconnect(client)
|
17
|
+
client.quit if client
|
18
|
+
end
|
19
|
+
|
20
|
+
# Method not supported:
|
21
|
+
# Subscribe/Unsubscribe methods and the following...
|
22
|
+
# :auth, :select, :discard, :quit, :watch, :unwatch
|
23
|
+
# :exec, :multi, :disconnect
|
24
|
+
|
25
|
+
connection_methods :info, :config, :flushdb, :flushall, :save,
|
26
|
+
:bgsave, :bgrewriteaof, :get, :getset, :mget, :append, :substr,
|
27
|
+
:strlen, :hgetall, :hget, :hdel, :hkeys, :keys, :randomkey,
|
28
|
+
:echo, :ping, :lastsave, :dbsize, :exists, :llen, :lrange,
|
29
|
+
:ltrim, :lindex, :linsert, :lset, :lrem, :rpush, :rpushx,
|
30
|
+
:lpush, :lpushx, :rpop, :blpop, :brpop, :rpoplpush, :lpop,
|
31
|
+
:smembers, :sismember, :sadd, :srem, :smove, :sdiff, :sdiffstore,
|
32
|
+
:sinter, :sinterstore, :sunion, :sunionstore, :spop, :scard,
|
33
|
+
:srandmember, :zadd, :zrank, :zrevrank, :zincrby, :zcard,
|
34
|
+
:zrange, :zrangebyscore, :zcount, :zrevrange, :zremrangebyscore,
|
35
|
+
:zremrangebyrank, :zscore, :zrem, :zinterstore, :zunionstore,
|
36
|
+
:move, :setnx, :del, :rename, :renamenx, :expire, :persist,
|
37
|
+
:ttl, :expireat, :hset, :hsetnx, :hmset, :mapped_hmset, :hmget,
|
38
|
+
:mapped_hmget, :hlen, :hvals, :hincrby, :hexists, :monitor,
|
39
|
+
:debug, :sync, :[], :[]=, :set, :setex, :mset, :mapped_mset,
|
40
|
+
:msetnx, :mapped_msetnx, :mapped_mget, :sort, :incr, :incrby,
|
41
|
+
:decr, :decrby, :type, :publish, :id
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'redis-store'
|
2
|
+
require 'redis/pooled'
|
3
|
+
|
4
|
+
class Redis
|
5
|
+
class PooledStore < Pooled
|
6
|
+
include Store::Ttl, Store::Interface
|
7
|
+
|
8
|
+
def initialize(options = { })
|
9
|
+
super
|
10
|
+
_extend_marshalling options
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.rails3? #:nodoc:
|
14
|
+
defined?(::Rails) && ::Rails::VERSION::MAJOR == 3
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
with_connection do |c|
|
19
|
+
"Redis::Pooled => #{c.host}:#{c.port} against DB #{c.db}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def _extend_marshalling(options) # Copied from Store
|
25
|
+
@marshalling = !(options[:marshalling] === false)
|
26
|
+
extend Store::Marshalling if @marshalling
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class << Store
|
31
|
+
def new(*args)
|
32
|
+
if args.size == 1 && args.first.is_a?(Hash) && args.first[:pool]
|
33
|
+
PooledStore.new(*args)
|
34
|
+
else
|
35
|
+
super(*args)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.3
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Michael Rykov
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-03-13 00:00:00 -08:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: activesupport
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ~>
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 3.0.5
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
description: |
|
28
|
+
Generalized connection pooling
|
29
|
+
|
30
|
+
email: mrykov@gmail
|
31
|
+
executables: []
|
32
|
+
|
33
|
+
extensions: []
|
34
|
+
|
35
|
+
extra_rdoc_files: []
|
36
|
+
|
37
|
+
files:
|
38
|
+
- README.md
|
39
|
+
- Rakefile
|
40
|
+
- LICENSE
|
41
|
+
- lib/pools.rb
|
42
|
+
- lib/pools/connection_pool.rb
|
43
|
+
- lib/pools/handler.rb
|
44
|
+
- lib/pools/middleware.rb
|
45
|
+
- lib/pools/pooled.rb
|
46
|
+
- lib/redis/pooled.rb
|
47
|
+
- lib/redis/pooled_store.rb
|
48
|
+
has_rdoc: true
|
49
|
+
homepage: http://github.com/rykov/pools
|
50
|
+
licenses: []
|
51
|
+
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: "0"
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.5.1
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: Generalized connection pooling
|
76
|
+
test_files: []
|
77
|
+
|