keymap 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ -fs
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in keymap.gemspec
4
+ gemspec
5
+
6
+ gem 'rake'
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Robert Buck
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,47 @@
1
+ # Keymap
2
+
3
+ A NoSQL database abstraction similar in design to ActiveRecord but solely
4
+ focussed on NoSQL databases. These are the goals for this project:
5
+
6
+ 1. Provide a simple abstraction layer over NoSQL databases, allowing easy
7
+ migration from one to another without having to rewrite application code.
8
+
9
+ 2. Provide a natural Ruby integration with NoSQL database, allowing direct
10
+ use of Enumerable operations on returned values (implying returned values
11
+ are always lists or hashes, or are manipulated to be represented in these
12
+ forms).
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ gem 'keymap'
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install keymap
27
+
28
+ ## Usage
29
+
30
+ TBD
31
+
32
+ ## Contributing
33
+
34
+ 1. Fork it
35
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
36
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
37
+ 4. Push to the branch (`git push origin my-new-feature`)
38
+ 5. Create new Pull Request
39
+
40
+ ## Writing New Adapters
41
+
42
+ 1. Create a new adapter file named #{database_name}_adapter.rb, placing it
43
+ in the connection adapters directory.
44
+ 2. Add an entry to the list of supported databases in the Rakefile.
45
+ 3. Add test configuration entries in the spec/support/config.yml file.
46
+ 4. Verify all tests pass when the KEYMAP_CONN=#{database_name} environment
47
+ variable is set.
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+ require 'rake/clean'
6
+ require 'rake/testtask'
7
+ require 'rake/packagetask'
8
+ require 'rdoc/task'
9
+ require 'bundler'
10
+ require 'bundler/gem_tasks'
11
+
12
+ require File.expand_path(File.dirname(__FILE__)) + "/spec/support/config"
13
+ require File.expand_path(File.dirname(__FILE__)) + "/tasks/rspec"
14
+
15
+ Bundler::GemHelper.install_tasks
16
+
17
+ load 'keymap.gemspec'
18
+
19
+ Dir['tasks/**/*.rb'].each { |file| load file }
20
+
21
+ GEM_NAME = "keymap"
22
+ GEM_VERSION = Keymap::VERSION
23
+
24
+ CLEAN.include('pkg')
25
+
26
+ task :default => :spec
27
+
28
+ desc "Push gem packages"
29
+ task :push => :build do
30
+ sh "gem push #{GEM_NAME}*.gem"
31
+ end
32
+
33
+ desc "Installs the gem"
34
+ task :install => :build do
35
+ sh %{gem install pkg/#{GEM_NAME}-#{GEM_VERSION} --no-rdoc --no-ri}
36
+ end
37
+
38
+ task :uninstall do
39
+ sh %{gem uninstall #{GEM_NAME} -x -v #{GEM_VERSION}}
40
+ end
41
+
42
+ desc "Tags git with the latest gem version"
43
+ task :tag do
44
+ sh %{git tag v#{GEM_VERSION}}
45
+ end
46
+
47
+ desc "Release version #{Keymap::VERSION}"
48
+ task :release => [:tag, :push]
49
+
50
+ desc "Provides tasks for each adapter type, e.g. test_redis"
51
+ %w( redis ).each do |adapter|
52
+ Rake::TestTask.new("test_#{adapter}") { |t|
53
+ adapter_short = adapter[/^[a-z0-9]+/]
54
+ t.libs << 'test'
55
+ t.test_files = (Dir.glob("spec/cases/**/*_spec.rb").reject {
56
+ |x| x =~ /\/adapters\//
57
+ } + Dir.glob("spec/functional/adapters/#{adapter_short}/**/*_spec.rb")).sort
58
+
59
+ t.verbose = true
60
+ t.warning = true
61
+ }
62
+
63
+ # Set the connection environment for the adapter
64
+ namespace adapter do
65
+ task :test => "test_#{adapter}"
66
+ task(:env) { ENV['KEYMAPPCONN'] = adapter }
67
+ end
68
+
69
+ # Make sure the adapter test evaluates the env setting task
70
+ task "test_#{adapter}" => "#{adapter}:env"
71
+ end
72
+
73
+ desc "Prints lines of code metrics"
74
+ task :lines do
75
+ lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
76
+
77
+ FileList["lib/keymap/**/*.rb"].each { |file_name|
78
+ next if file_name =~ /vendor/
79
+ f = File.open(file_name)
80
+
81
+ while (line = f.gets)
82
+ lines += 1
83
+ next if line =~ /^\s*$/
84
+ next if line =~ /^\s*#/
85
+ codelines += 1
86
+ end
87
+ puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
88
+
89
+ total_lines += lines
90
+ total_codelines += codelines
91
+
92
+ lines, codelines = 0, 0
93
+ }
94
+
95
+ puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
96
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'keymap/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "keymap"
8
+ spec.version = Keymap::VERSION
9
+ spec.authors = ['Robert Buck']
10
+ spec.email = 'buck.robert.j@gmail.com'
11
+ spec.description = %q{ActiveRecord like Key-Value Store API for Ruby}
12
+ spec.summary = %q{Abstracts choosing a key-value store implementation, and provides a common API.}
13
+ spec.homepage = 'https://github.com/rbuck/keymap'
14
+ spec.date = '2012-10-28'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(tasks|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "erubis", "~> 2.7.0"
22
+ spec.add_dependency "redis", "~> 3.0.2"
23
+ spec.add_dependency "activesupport", "~> 3.2.8"
24
+
25
+ %w(rake rdoc simplecov).each { |gem| spec.add_development_dependency gem }
26
+ %w(rspec rspec-core rspec-expectations rspec-mocks).each { |gem| spec.add_development_dependency gem, "~> 2.11.0" }
27
+
28
+ end
@@ -0,0 +1,32 @@
1
+ require 'active_support'
2
+ require 'keymap/version'
3
+
4
+ module Keymap
5
+
6
+ extend ActiveSupport::Autoload
7
+
8
+ eager_autoload do
9
+ autoload :KeymapError, 'keymap/errors'
10
+ autoload :ConnectionNotEstablished, 'keymap/errors'
11
+ autoload :ConnectionAdapters, 'keymap/connection_adapters/abstract_adapter'
12
+ autoload :Base
13
+ end
14
+
15
+ module ConnectionAdapters
16
+ extend ActiveSupport::Autoload
17
+
18
+ eager_autoload do
19
+ autoload :AbstractAdapter
20
+ autoload :ConnectionManagement, 'keymap/connection_adapters/abstract/connection_pool'
21
+ end
22
+ end
23
+
24
+ def env
25
+ @_env ||= ActiveSupport::StringInquirer.new(ENV["KEYMAP_ENV"] || ENV["KEYMAP_ENV"] || "development")
26
+ end
27
+
28
+ def env=(environment)
29
+ @_env = ActiveSupport::StringInquirer.new(environment)
30
+ end
31
+
32
+ end
@@ -0,0 +1,65 @@
1
+ require 'active_support/dependencies'
2
+ require 'active_support/descendants_tracker'
3
+
4
+ require 'active_support/core_ext/class/attribute'
5
+ require 'active_support/core_ext/class/attribute_accessors'
6
+ require 'active_support/core_ext/hash/indifferent_access'
7
+ require 'active_support/core_ext/module/delegation'
8
+
9
+ module Keymap
10
+
11
+ class Base
12
+ ##
13
+ # :singleton-method:
14
+ # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class,
15
+ # which is then passed on to any new database connections made and which can be retrieved on both
16
+ # a class and instance level by calling +logger+.
17
+ cattr_accessor :logger, :instance_writer => false
18
+
19
+ ##
20
+ # :singleton-method:
21
+ # Contains the database configuration - as is typically stored in config/database.yml -
22
+ # as a Hash.
23
+ #
24
+ # For example, the following database.yml...
25
+ #
26
+ # development:
27
+ # adapter: redis
28
+ # database: db/development.sqlite3
29
+ #
30
+ # production:
31
+ # adapter: redis
32
+ # database: db/production.sqlite3
33
+ #
34
+ # ...would result in Keymap::Base.configurations to look like this:
35
+ #
36
+ # {
37
+ # 'development' => {
38
+ # 'adapter' => 'sqlite3',
39
+ # 'database' => 'db/development.sqlite3'
40
+ # },
41
+ # 'production' => {
42
+ # 'adapter' => 'sqlite3',
43
+ # 'database' => 'db/production.sqlite3'
44
+ # }
45
+ # }
46
+ cattr_accessor :configurations, :instance_writer => false
47
+ @@configurations = {}
48
+
49
+ public
50
+
51
+ def initialize(attributes = nil, options = {})
52
+ yield self if block_given?
53
+ #run_callbacks :initialize
54
+ end
55
+
56
+ private
57
+
58
+ extend ActiveSupport::DescendantsTracker
59
+
60
+ include ActiveSupport::Callbacks
61
+
62
+ end
63
+ end
64
+ require 'keymap/connection_adapters/abstract/connection_specification'
65
+ ActiveSupport.run_load_hooks(:keymap, Keymap::Base)
@@ -0,0 +1,451 @@
1
+ require 'thread'
2
+ require 'monitor'
3
+ require 'set'
4
+
5
+ module Keymap
6
+ # Raised when a connection could not be obtained within the connection
7
+ # acquisition timeout period.
8
+ class ConnectionTimeoutError < ConnectionNotEstablished
9
+ end
10
+
11
+ module ConnectionAdapters
12
+ # Connection pool base class for managing connections.
13
+ #
14
+ # == Introduction
15
+ #
16
+ # A connection pool synchronizes thread access to a limited number of
17
+ # connections. The basic idea is that each thread checks out a
18
+ # connection from the pool, uses that connection, and checks the
19
+ # connection back in. ConnectionPool is completely thread-safe, and will
20
+ # ensure that a connection cannot be used by two threads at the same time,
21
+ # as long as ConnectionPool's contract is correctly followed. It will also
22
+ # handle cases in which there are more threads than connections: if all
23
+ # connections have been checked out, and a thread tries to checkout a
24
+ # connection anyway, then ConnectionPool will wait until some other thread
25
+ # has checked in a connection.
26
+ #
27
+ # == Obtaining (checking out) a connection
28
+ #
29
+ # Connections can be obtained and used from a connection pool in several
30
+ # ways:
31
+ #
32
+ # 1. Simply use Keymap::Base.connection. Eventually, when you're done with
33
+ # the connection(s) and wish it to be returned to the pool, you call
34
+ # Keymap::Base.clear_active_connections!.
35
+ # 2. Manually check out a connection from the pool with
36
+ # Keymap::Base.connection_pool.checkout. You are responsible for
37
+ # returning this connection to the pool when finished by calling
38
+ # Keymap::Base.connection_pool.checkin(connection).
39
+ # 3. Use Keymap::Base.connection_pool.with_connection(&block), which
40
+ # obtains a connection, yields it as the sole argument to the block,
41
+ # and returns it to the pool after the block completes.
42
+ #
43
+ # Connections in the pool are actually AbstractAdapter objects (or objects
44
+ # compatible with AbstractAdapter's interface).
45
+ #
46
+ # == Options
47
+ #
48
+ # There are two connection-pooling-related options that you can add to
49
+ # your database connection configuration:
50
+ #
51
+ # * +pool+: number indicating size of connection pool (default 5)
52
+ # * +checkout _timeout+: number of seconds to block and wait for a
53
+ # connection before giving up and raising a timeout error
54
+ # (default 5 seconds).
55
+ class ConnectionPool
56
+ include MonitorMixin
57
+
58
+ attr_accessor :automatic_reconnect
59
+ attr_reader :spec, :connections
60
+
61
+ # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
62
+ # object which describes database connection information (e.g. adapter,
63
+ # host name, username, password, etc), as well as the maximum size for
64
+ # this ConnectionPool.
65
+ #
66
+ # The default ConnectionPool maximum size is 5.
67
+ def initialize(spec)
68
+ super()
69
+
70
+ @spec = spec
71
+
72
+ # The cache of reserved connections mapped to threads
73
+ @reserved_connections = {}
74
+
75
+ @queue = new_cond
76
+ # 'wait_timeout', the backward-compatible key, conflicts with spec key
77
+ # used by mysql2 for something entirely different, checkout_timeout
78
+ # preferred to avoid conflict and allow independent values.
79
+ @timeout = spec.config[:checkout_timeout] || spec.config[:wait_timeout] || 5
80
+
81
+ # default max pool size to 5
82
+ @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
83
+
84
+ @connections = []
85
+ @automatic_reconnect = true
86
+ end
87
+
88
+ # Retrieve the connection associated with the current thread, or call
89
+ # #checkout to obtain one if necessary.
90
+ #
91
+ # #connection can be called any number of times; the connection is
92
+ # held in a hash keyed by the thread id.
93
+ def connection
94
+ synchronize do
95
+ @reserved_connections[current_connection_id] ||= checkout
96
+ end
97
+ end
98
+
99
+ # Is there an open connection that is being used for the current thread?
100
+ def active_connection?
101
+ synchronize do
102
+ @reserved_connections.fetch(current_connection_id) {
103
+ return false
104
+ }.in_use?
105
+ end
106
+ end
107
+
108
+ # Signal that the thread is finished with the current connection.
109
+ # #release_connection releases the connection-thread association
110
+ # and returns the connection to the pool.
111
+ def release_connection(with_id = current_connection_id)
112
+ conn = synchronize { @reserved_connections.delete(with_id) }
113
+ checkin conn if conn
114
+ end
115
+
116
+ # If a connection already exists yield it to the block. If no connection
117
+ # exists checkout a connection, yield it to the block, and checkin the
118
+ # connection when finished.
119
+ def with_connection
120
+ fresh_connection = false
121
+ connection_id = current_connection_id
122
+ fresh_connection = true unless active_connection?
123
+ yield connection
124
+ ensure
125
+ release_connection(connection_id) if fresh_connection
126
+ end
127
+
128
+ # Returns true if a connection has already been opened.
129
+ def connected?
130
+ synchronize { @connections.any? }
131
+ end
132
+
133
+ # Disconnects all connections in the pool, and clears the pool.
134
+ def disconnect!
135
+ synchronize do
136
+ @reserved_connections = {}
137
+ @connections.each do |conn|
138
+ checkin conn
139
+ conn.disconnect!
140
+ end
141
+ @connections = []
142
+ end
143
+ end
144
+
145
+ # Clears the cache which maps classes.
146
+ def clear_reloadable_connections!
147
+ synchronize do
148
+ @reserved_connections = {}
149
+ @connections.each do |conn|
150
+ checkin conn
151
+ conn.disconnect! if conn.requires_reloading?
152
+ end
153
+ @connections.delete_if do |conn|
154
+ conn.requires_reloading?
155
+ end
156
+ end
157
+ end
158
+
159
+ # Verify active connections and remove and disconnect connections
160
+ # associated with stale threads.
161
+ def verify_active_connections! #:nodoc:
162
+ synchronize do
163
+ clear_stale_cached_connections!
164
+ @connections.each do |connection|
165
+ connection.verify!
166
+ end
167
+ end
168
+ end
169
+
170
+ # Return any checked-out connections back to the pool by threads that
171
+ # are no longer alive.
172
+ def clear_stale_cached_connections!
173
+ keys = @reserved_connections.keys - Thread.list.find_all { |t|
174
+ t.alive?
175
+ }.map { |thread| thread.object_id }
176
+ keys.each do |key|
177
+ conn = @reserved_connections[key]
178
+ ActiveSupport::Deprecation.warn(<<-eowarn) if conn.in_use?
179
+ Database connections will not be closed automatically, please close your
180
+ database connection at the end of the thread by calling `close` on your
181
+ connection. For example: ActiveRecord::Base.connection.close
182
+ eowarn
183
+ checkin conn
184
+ @reserved_connections.delete(key)
185
+ end
186
+ end
187
+
188
+ # Check-out a database connection from the pool, indicating that you want
189
+ # to use it. You should call #checkin when you no longer need this.
190
+ #
191
+ # This is done by either returning an existing connection, or by creating
192
+ # a new connection. If the maximum number of connections for this pool has
193
+ # already been reached, but the pool is empty (i.e. they're all being used),
194
+ # then this method will wait until a thread has checked in a connection.
195
+ # The wait time is bounded however: if no connection can be checked out
196
+ # within the timeout specified for this pool, then a ConnectionTimeoutError
197
+ # exception will be raised.
198
+ #
199
+ # Returns: an AbstractAdapter object.
200
+ #
201
+ # Raises:
202
+ # - ConnectionTimeoutError: no connection can be obtained from the pool
203
+ # within the timeout period.
204
+ def checkout
205
+ synchronize do
206
+ waited_time = 0
207
+
208
+ loop do
209
+ conn = @connections.find { |c| c.lease }
210
+
211
+ unless conn
212
+ if @connections.size < @size
213
+ conn = checkout_new_connection
214
+ conn.lease
215
+ end
216
+ end
217
+
218
+ if conn
219
+ checkout_and_verify conn
220
+ return conn
221
+ end
222
+
223
+ if waited_time >= @timeout
224
+ raise ConnectionTimeoutError, "could not obtain a database connection#{" within #@timeout seconds" if @timeout} (waited #{waited_time} seconds). The max pool size is currently #@size; consider increasing it."
225
+ end
226
+
227
+ # Sometimes our wait can end because a connection is available,
228
+ # but another thread can snatch it up first. If timeout hasn't
229
+ # passed but no connection is avail, looks like that happened --
230
+ # loop and wait again, for the time remaining on our timeout.
231
+ before_wait = Time.now
232
+ @queue.wait([@timeout - waited_time, 0].max)
233
+ waited_time += (Time.now - before_wait)
234
+
235
+ # Will go away in Rails 4, when we don't clean up
236
+ # after leaked connections automatically anymore. Right now, clean
237
+ # up after we've returned from a 'wait' if it looks like it's
238
+ # needed, then loop and try again.
239
+ if active_connections.size >= @connections.size
240
+ clear_stale_cached_connections!
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ # Check-in a database connection back into the pool, indicating that you
247
+ # no longer need this connection.
248
+ #
249
+ # +conn+: an AbstractAdapter object, which was obtained by earlier by
250
+ # calling +checkout+ on this pool.
251
+ def checkin(conn)
252
+ synchronize do
253
+ conn.run_callbacks :checkin do
254
+ conn.expire
255
+ @queue.signal
256
+ end
257
+
258
+ release conn
259
+ end
260
+ end
261
+
262
+ private
263
+
264
+ def release(conn)
265
+ synchronize do
266
+ if @reserved_connections[current_connection_id] == conn
267
+ thread_id = current_connection_id
268
+ else
269
+ thread_id = @reserved_connections.keys.find { |k|
270
+ @reserved_connections[k] == conn
271
+ }
272
+ end
273
+ @reserved_connections.delete thread_id if thread_id
274
+ end
275
+ end
276
+
277
+ def new_connection
278
+ Keymap::Base.send(spec.adapter_method, spec.config)
279
+ end
280
+
281
+ def current_connection_id #:nodoc:
282
+ Keymap::Base.connection_id ||= Thread.current.object_id
283
+ end
284
+
285
+ def checkout_new_connection
286
+ raise ConnectionNotEstablished unless @automatic_reconnect
287
+
288
+ c = new_connection
289
+ c.pool = self
290
+ @connections << c
291
+ c
292
+ end
293
+
294
+ def checkout_and_verify(c)
295
+ c.run_callbacks :checkout do
296
+ c.verify!
297
+ end
298
+ c
299
+ end
300
+
301
+ def active_connections
302
+ @connections.find_all { |c| c.in_use? }
303
+ end
304
+ end
305
+
306
+ # ConnectionHandler is a collection of ConnectionPool objects. It is used
307
+ # for keeping separate connection pools for Active Record models that connect
308
+ # to different databases.
309
+ #
310
+ # For example, suppose that you have 5 models, with the following hierarchy:
311
+ #
312
+ # |
313
+ # +-- Book
314
+ # | |
315
+ # | +-- ScaryBook
316
+ # | +-- GoodBook
317
+ # +-- Author
318
+ # +-- BankAccount
319
+ #
320
+ # Suppose that Book is to connect to a separate database (i.e. one other
321
+ # than the default database). Then Book, ScaryBook and GoodBook will all use
322
+ # the same connection pool. Likewise, Author and BankAccount will use the
323
+ # same connection pool. However, the connection pool used by Author/BankAccount
324
+ # is not the same as the one used by Book/ScaryBook/GoodBook.
325
+ #
326
+ # Normally there is only a single ConnectionHandler instance, accessible via
327
+ # ActiveRecord::Base.connection_handler. Active Record models use this to
328
+ # determine that connection pool that they should use.
329
+ class ConnectionHandler
330
+ attr_reader :connection_pools
331
+
332
+ def initialize(pools = {})
333
+ @connection_pools = pools
334
+ @class_to_pool = {}
335
+ end
336
+
337
+ def establish_connection(name, spec)
338
+ @connection_pools[spec] ||= ConnectionAdapters::ConnectionPool.new(spec)
339
+ @class_to_pool[name] = @connection_pools[spec]
340
+ end
341
+
342
+ # Returns true if there are any active connections among the connection
343
+ # pools that the ConnectionHandler is managing.
344
+ def active_connections?
345
+ connection_pools.values.any? { |pool| pool.active_connection? }
346
+ end
347
+
348
+ # Returns any connections in use by the current thread back to the pool.
349
+ def clear_active_connections!
350
+ @connection_pools.each_value { |pool| pool.release_connection }
351
+ end
352
+
353
+ # Clears the cache which maps classes.
354
+ def clear_reloadable_connections!
355
+ @connection_pools.each_value { |pool| pool.clear_reloadable_connections! }
356
+ end
357
+
358
+ def clear_all_connections!
359
+ @connection_pools.each_value { |pool| pool.disconnect! }
360
+ end
361
+
362
+ # Verify active connections.
363
+ def verify_active_connections! #:nodoc:
364
+ @connection_pools.each_value { |pool| pool.verify_active_connections! }
365
+ end
366
+
367
+ # Locate the connection of the nearest super class. This can be an
368
+ # active or defined connection: if it is the latter, it will be
369
+ # opened and set as the active connection for the class it was defined
370
+ # for (not necessarily the current class).
371
+ def retrieve_connection(klass) #:nodoc:
372
+ pool = retrieve_connection_pool(klass)
373
+ (pool && pool.connection) or raise ConnectionNotEstablished
374
+ end
375
+
376
+ # Returns true if a connection that's accessible to this class has
377
+ # already been opened.
378
+ def connected?(klass)
379
+ conn = retrieve_connection_pool(klass)
380
+ conn && conn.connected?
381
+ end
382
+
383
+ # Remove the connection for this class. This will close the active
384
+ # connection and the defined connection (if they exist). The result
385
+ # can be used as an argument for establish_connection, for easily
386
+ # re-establishing the connection.
387
+ def remove_connection(klass)
388
+ pool = @class_to_pool.delete(klass.name)
389
+ return nil unless pool
390
+
391
+ @connection_pools.delete pool.spec
392
+ pool.automatic_reconnect = false
393
+ pool.disconnect!
394
+ pool.spec.config
395
+ end
396
+
397
+ def retrieve_connection_pool(klass)
398
+ pool = @class_to_pool[klass.name]
399
+ return pool if pool
400
+ return nil if Keymap::Base == klass
401
+ retrieve_connection_pool klass.superclass
402
+ end
403
+ end
404
+
405
+ class ConnectionManagement
406
+ class Proxy # :nodoc:
407
+ attr_reader :body, :testing
408
+
409
+ def initialize(body, testing = false)
410
+ @body = body
411
+ @testing = testing
412
+ end
413
+
414
+ def method_missing(method_sym, *arguments, &block)
415
+ @body.send(method_sym, *arguments, &block)
416
+ end
417
+
418
+ def respond_to?(method_sym, include_private = false)
419
+ @body.respond_to?(method_sym)
420
+ end
421
+
422
+ def each(&block)
423
+ body.each(&block)
424
+ end
425
+
426
+ def close
427
+ body.close if body.respond_to?(:close)
428
+
429
+ # Don't return connection (and perform implicit rollback) if
430
+ # this request is a part of integration test
431
+ Keymap::Base.clear_active_connections! unless testing
432
+ end
433
+ end
434
+
435
+ def initialize(app)
436
+ @app = app
437
+ end
438
+
439
+ def call(env)
440
+ testing = env.key?('rack.test')
441
+
442
+ status, headers, body = @app.call(env)
443
+
444
+ [status, headers, Proxy.new(body, testing)]
445
+ rescue
446
+ Keymap::Base.clear_active_connections! unless testing
447
+ raise
448
+ end
449
+ end
450
+ end
451
+ end