keymap 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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