keymap 0.1.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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +96 -0
- data/keymap.gemspec +28 -0
- data/lib/keymap.rb +32 -0
- data/lib/keymap/base.rb +65 -0
- data/lib/keymap/connection_adapters/abstract/connection_pool.rb +451 -0
- data/lib/keymap/connection_adapters/abstract/connection_specification.rb +159 -0
- data/lib/keymap/connection_adapters/abstract/transaction_management.rb +39 -0
- data/lib/keymap/connection_adapters/abstract_adapter.rb +139 -0
- data/lib/keymap/connection_adapters/redis_adapter.rb +113 -0
- data/lib/keymap/errors.rb +29 -0
- data/lib/keymap/version.rb +3 -0
- data/spec/data/.gitignore +0 -0
- data/spec/functional/abstract_adapter_spec.rb +24 -0
- data/spec/functional/adapter_spec.rb +1 -0
- data/spec/functional/adapters/redis/connection_spec.rb +0 -0
- data/spec/rcov.opts +2 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/config.rb +45 -0
- data/spec/support/config.yml +10 -0
- data/spec/support/connection.rb +17 -0
- data/spec/unit/.gitignore +0 -0
- data/spec/unit/test.rb +89 -0
- data/tasks/rspec.rb +56 -0
- metadata +247 -0
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Keymap
|
4
|
+
class Base
|
5
|
+
class ConnectionSpecification #:nodoc:
|
6
|
+
attr_reader :config, :adapter_method
|
7
|
+
|
8
|
+
def initialize (config, adapter_method)
|
9
|
+
@config, @adapter_method = config, adapter_method
|
10
|
+
end
|
11
|
+
|
12
|
+
##
|
13
|
+
# Builds a ConnectionSpecification from user input
|
14
|
+
class Resolver # :nodoc:
|
15
|
+
|
16
|
+
attr_reader :config, :configurations
|
17
|
+
|
18
|
+
def initialize(config, configurations)
|
19
|
+
@config = config
|
20
|
+
@configurations = configurations
|
21
|
+
end
|
22
|
+
|
23
|
+
def spec
|
24
|
+
case config
|
25
|
+
when nil
|
26
|
+
raise AdapterNotSpecified unless defined?(Keymap.env)
|
27
|
+
resolve_string_connection Keymap.env
|
28
|
+
when Symbol, String
|
29
|
+
resolve_string_connection config.to_s
|
30
|
+
when Hash
|
31
|
+
resolve_hash_connection config
|
32
|
+
else
|
33
|
+
# type code here
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def resolve_string_connection(spec) # :nodoc:
|
40
|
+
hash = configurations.fetch(spec) do |k|
|
41
|
+
connection_url_to_hash(k)
|
42
|
+
end
|
43
|
+
|
44
|
+
raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash
|
45
|
+
|
46
|
+
resolve_hash_connection hash
|
47
|
+
end
|
48
|
+
|
49
|
+
def resolve_hash_connection(spec) # :nodoc:
|
50
|
+
spec = spec.symbolize_keys
|
51
|
+
|
52
|
+
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
|
53
|
+
|
54
|
+
begin
|
55
|
+
require "keymap/connection_adapters/#{spec[:adapter]}_adapter"
|
56
|
+
rescue LoadError => e
|
57
|
+
raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install keymap-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace
|
58
|
+
end
|
59
|
+
|
60
|
+
adapter_method = "#{spec[:adapter]}_connection"
|
61
|
+
|
62
|
+
ConnectionSpecification.new(spec, adapter_method)
|
63
|
+
end
|
64
|
+
|
65
|
+
def connection_url_to_hash(url) # :nodoc:
|
66
|
+
config = URI.parse url
|
67
|
+
adapter = config.scheme
|
68
|
+
spec = {:adapter => adapter,
|
69
|
+
:username => config.user,
|
70
|
+
:password => config.password,
|
71
|
+
:port => config.port,
|
72
|
+
:database => config.path.sub(%r{^/}, ""),
|
73
|
+
:host => config.host}
|
74
|
+
spec.reject! { |_, value| !value }
|
75
|
+
if config.query
|
76
|
+
options = Hash[config.query.split("&").map { |pair| pair.split("=") }].symbolize_keys
|
77
|
+
spec.merge!(options)
|
78
|
+
end
|
79
|
+
spec
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# :singleton-method:
|
87
|
+
# The connection handler
|
88
|
+
class_attribute :connection_handler, :instance_writer => false
|
89
|
+
self.connection_handler = Keymap::ConnectionAdapters::ConnectionHandler.new
|
90
|
+
|
91
|
+
def connection
|
92
|
+
self.class.connection
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.establish_connection(spec = ENV["DATABASE_URL"])
|
96
|
+
resolver = Keymap::Base::ConnectionSpecification::Resolver.new spec, configurations
|
97
|
+
spec = resolver.spec
|
98
|
+
|
99
|
+
unless respond_to?(spec.adapter_method)
|
100
|
+
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
|
101
|
+
end
|
102
|
+
|
103
|
+
remove_connection
|
104
|
+
connection_handler.establish_connection name, spec
|
105
|
+
end
|
106
|
+
|
107
|
+
class << self
|
108
|
+
# Returns the connection currently associated with the class. This can
|
109
|
+
# also be used to "borrow" the connection to do database work unrelated
|
110
|
+
# to any of the specific Active Records.
|
111
|
+
def connection
|
112
|
+
retrieve_connection
|
113
|
+
end
|
114
|
+
|
115
|
+
def connection_id
|
116
|
+
Thread.current['Keymap::Base.connection_id']
|
117
|
+
end
|
118
|
+
|
119
|
+
def connection_id=(connection_id)
|
120
|
+
Thread.current['Keymap::Base.connection_id'] = connection_id
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns the configuration of the associated connection as a hash:
|
124
|
+
#
|
125
|
+
# ActiveRecord::Base.connection_config
|
126
|
+
# # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"}
|
127
|
+
#
|
128
|
+
# Please use only for reading.
|
129
|
+
def connection_config
|
130
|
+
connection_pool.spec.config
|
131
|
+
end
|
132
|
+
|
133
|
+
def connection_pool
|
134
|
+
connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished
|
135
|
+
end
|
136
|
+
|
137
|
+
def retrieve_connection
|
138
|
+
connection_handler.retrieve_connection(self)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns true if Keymap is connected.
|
142
|
+
def connected?
|
143
|
+
connection_handler.connected?(self)
|
144
|
+
end
|
145
|
+
|
146
|
+
def remove_connection(klass = self)
|
147
|
+
connection_handler.remove_connection(klass)
|
148
|
+
end
|
149
|
+
|
150
|
+
def clear_active_connections!
|
151
|
+
connection_handler.clear_active_connections!
|
152
|
+
end
|
153
|
+
|
154
|
+
delegate :clear_reloadable_connections!,
|
155
|
+
:clear_all_connections!, :verify_active_connections!, :to => :connection_handler
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'keymap/errors'
|
2
|
+
|
3
|
+
module Keymap
|
4
|
+
module ConnectionAdapters # :nodoc:
|
5
|
+
module TransactionManagement
|
6
|
+
|
7
|
+
# Runs the given block in a database transaction, and returns the result
|
8
|
+
# of the block.
|
9
|
+
def transaction(options = {})
|
10
|
+
transaction_open = false
|
11
|
+
@_current_transaction_records ||= []
|
12
|
+
begin
|
13
|
+
if block_given?
|
14
|
+
begin_db_transaction
|
15
|
+
transaction_open = true
|
16
|
+
@_current_transaction_records.push([])
|
17
|
+
yield
|
18
|
+
end
|
19
|
+
rescue Exception => database_transaction_rollback
|
20
|
+
if transaction_open && !outside_transaction?
|
21
|
+
transaction_open = false
|
22
|
+
rollback_db_transaction
|
23
|
+
end
|
24
|
+
raise unless database_transaction_rollback.is_a?(Rollback)
|
25
|
+
end
|
26
|
+
ensure
|
27
|
+
if transaction_open
|
28
|
+
begin
|
29
|
+
commit_db_transaction
|
30
|
+
rescue Exception => database_transaction_rollback
|
31
|
+
rollback_db_transaction
|
32
|
+
raise
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
require 'active_support/callbacks'
|
3
|
+
require 'active_support/dependencies/autoload'
|
4
|
+
require 'keymap/connection_adapters/abstract/transaction_management'
|
5
|
+
|
6
|
+
module Keymap
|
7
|
+
|
8
|
+
module ConnectionAdapters # :nodoc:
|
9
|
+
|
10
|
+
extend ActiveSupport::Autoload
|
11
|
+
|
12
|
+
autoload_under 'abstract' do
|
13
|
+
autoload :ConnectionPool
|
14
|
+
autoload :ConnectionHandler, 'keymap/connection_adapters/abstract/connection_pool'
|
15
|
+
autoload :ConnectionManagement, 'keymap/connection_adapters/abstract/connection_pool'
|
16
|
+
autoload :ConnectionSpecification
|
17
|
+
end
|
18
|
+
|
19
|
+
class AbstractAdapter
|
20
|
+
|
21
|
+
include ActiveSupport::Callbacks
|
22
|
+
include TransactionManagement
|
23
|
+
include MonitorMixin
|
24
|
+
|
25
|
+
define_callbacks :checkout, :checkin
|
26
|
+
attr_accessor :pool
|
27
|
+
attr_reader :last_use, :in_use, :logger
|
28
|
+
alias :in_use? :in_use
|
29
|
+
|
30
|
+
def initialize(connection, logger = nil, pool = nil) #:nodoc:
|
31
|
+
super()
|
32
|
+
|
33
|
+
@active = nil
|
34
|
+
@connection = connection
|
35
|
+
@logger = logger
|
36
|
+
@pool = pool
|
37
|
+
|
38
|
+
@in_use = false
|
39
|
+
@last_use = false
|
40
|
+
end
|
41
|
+
|
42
|
+
def lease
|
43
|
+
synchronize do
|
44
|
+
unless in_use
|
45
|
+
@in_use = true
|
46
|
+
@last_use = Time.now
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def expire
|
52
|
+
@in_use = false
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the human-readable name of the adapter. Use mixed case - one
|
56
|
+
# can always use downcase if needed.
|
57
|
+
def adapter_name
|
58
|
+
'Abstract'
|
59
|
+
end
|
60
|
+
|
61
|
+
# CONNECTION MANAGEMENT ====================================
|
62
|
+
|
63
|
+
# Checks whether the connection to the database is still active. This includes
|
64
|
+
# checking whether the kv-store is actually capable of responding, i.e. whether
|
65
|
+
# the connection isn't stale.
|
66
|
+
def active?
|
67
|
+
@active
|
68
|
+
end
|
69
|
+
|
70
|
+
# Disconnects from the database if already connected, and establishes a
|
71
|
+
# new connection with the database.
|
72
|
+
def reconnect!
|
73
|
+
@active = true
|
74
|
+
end
|
75
|
+
|
76
|
+
# Disconnects from the kv-store if already connected. Otherwise, this
|
77
|
+
# method does nothing.
|
78
|
+
def disconnect!
|
79
|
+
@active = false
|
80
|
+
end
|
81
|
+
|
82
|
+
# Reset the state of this connection, directing the kv-store to clear
|
83
|
+
# transactions and other connection-related server-side state. Usually a
|
84
|
+
# implementation-dependent operation.
|
85
|
+
#
|
86
|
+
# The default implementation does nothing; the implementation should be
|
87
|
+
# overridden by concrete adapters.
|
88
|
+
def reset!
|
89
|
+
# this should be overridden by concrete adapters
|
90
|
+
end
|
91
|
+
|
92
|
+
###
|
93
|
+
# Clear any caching the kv-store adapter may be doing. This is kv-store
|
94
|
+
# specific.
|
95
|
+
def clear_cache!
|
96
|
+
# this should be overridden by concrete adapters
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns true if its required to reload the connection between requests for development mode.
|
100
|
+
def requires_reloading?
|
101
|
+
false
|
102
|
+
end
|
103
|
+
|
104
|
+
# Checks whether the connection to the kv-store is still active (i.e. not stale).
|
105
|
+
# This is done under the hood by calling <tt>active?</tt>. If the connection
|
106
|
+
# is no longer active, then this method will reconnect to the kv-store.
|
107
|
+
def verify!
|
108
|
+
reconnect! unless active?
|
109
|
+
end
|
110
|
+
|
111
|
+
# Provides access to the underlying kv-store driver for this adapter. For
|
112
|
+
# example, this method returns a Redis object in case of RedisAdapter.
|
113
|
+
#
|
114
|
+
# This is useful for when you need to call a proprietary method.
|
115
|
+
def raw_connection
|
116
|
+
@connection
|
117
|
+
end
|
118
|
+
|
119
|
+
# Begins the transaction (and turns off auto-committing).
|
120
|
+
def begin_db_transaction()
|
121
|
+
end
|
122
|
+
|
123
|
+
# Commits the transaction (and turns on auto-committing).
|
124
|
+
def commit_db_transaction()
|
125
|
+
end
|
126
|
+
|
127
|
+
# Rolls back the transaction (and turns on auto-committing). Must be
|
128
|
+
# done if the transaction block raises an exception or returns false.
|
129
|
+
def rollback_db_transaction()
|
130
|
+
end
|
131
|
+
|
132
|
+
# Check the connection back in to the connection pool
|
133
|
+
def close
|
134
|
+
pool.checkin self
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'keymap/connection_adapters/abstract_adapter'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
module Keymap
|
5
|
+
|
6
|
+
class Base
|
7
|
+
def self.redis_connection(config)
|
8
|
+
config = config.symbolize_keys
|
9
|
+
config[:port] ||= 6379
|
10
|
+
ConnectionAdapters::RedisAdapter.new(nil, nil, config)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ConnectionAdapters
|
15
|
+
|
16
|
+
class RedisAdapter < AbstractAdapter
|
17
|
+
|
18
|
+
attr_reader :config
|
19
|
+
|
20
|
+
def initialize(connection, pool, config)
|
21
|
+
super(nil)
|
22
|
+
@config = config
|
23
|
+
reconnect!
|
24
|
+
end
|
25
|
+
|
26
|
+
def adapter_name
|
27
|
+
'redis'
|
28
|
+
end
|
29
|
+
|
30
|
+
def active?
|
31
|
+
return false unless @connection
|
32
|
+
@connection.ping == "PONG"
|
33
|
+
end
|
34
|
+
|
35
|
+
def reconnect!
|
36
|
+
disconnect!
|
37
|
+
connect
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
alias :reset! :reconnect!
|
42
|
+
|
43
|
+
# Disconnects from the database if already connected.
|
44
|
+
# Otherwise, this method does nothing.
|
45
|
+
def disconnect!
|
46
|
+
super
|
47
|
+
unless @connection.nil?
|
48
|
+
@connection.quit
|
49
|
+
@connection = nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def connect
|
56
|
+
@connection = Redis.new(config)
|
57
|
+
end
|
58
|
+
|
59
|
+
public
|
60
|
+
|
61
|
+
def begin_db_transaction
|
62
|
+
raw_connection.multi
|
63
|
+
end
|
64
|
+
|
65
|
+
def commit_db_transaction
|
66
|
+
raw_connection.exec
|
67
|
+
end
|
68
|
+
|
69
|
+
def rollback_db_transaction
|
70
|
+
raw_connection.discard
|
71
|
+
end
|
72
|
+
|
73
|
+
# Retrieves the hash whose name is identified by key.
|
74
|
+
def hash (key)
|
75
|
+
coll = KeyStoreHash.new
|
76
|
+
coll.connection = raw_connection
|
77
|
+
coll.key = key
|
78
|
+
coll
|
79
|
+
#
|
80
|
+
# Perhaps there is a more experienced ruby developer who could help with this.
|
81
|
+
# I'd rather do the following but could not figure out how to get it to work:
|
82
|
+
#
|
83
|
+
#def create_getter(connection, key)
|
84
|
+
# lambda { |field| connection.hget key, field }
|
85
|
+
#end
|
86
|
+
#
|
87
|
+
#def create_setter(connection, key)
|
88
|
+
# lambda { |field, value| connection.hset key, field, value }
|
89
|
+
#end
|
90
|
+
#
|
91
|
+
#coll.class.send :define_method, "[]", create_getter(raw_connection, key)
|
92
|
+
#coll.class.send :define_method, "[]=", create_setter(raw_connection, key)
|
93
|
+
#coll
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
class KeyStoreHash < Hash
|
100
|
+
|
101
|
+
attr_accessor :connection, :key
|
102
|
+
|
103
|
+
def [] (field)
|
104
|
+
connection.hget key, field
|
105
|
+
end
|
106
|
+
|
107
|
+
def []=(field, value)
|
108
|
+
connection.hset key, field, value
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|