genghis 1.0.4 → 1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.rdoc +21 -13
  2. data/lib/genghis.rb +169 -40
  3. metadata +8 -8
@@ -2,18 +2,18 @@
2
2
 
3
3
  == What is Genghis?
4
4
  * A configuration framework for mongoDB
5
- * A resilience framework when using MongoDB in replica pairs
5
+ * A resilience framework when using MongoDB in replica sets
6
6
 
7
7
  == Getting started
8
8
 
9
9
  === Configuration
10
-
10
+ Genghis 1.2 is for use with MongoDB 1.6 or above; for MongoDB < 1.6, use Genghis 1.0.4
11
11
 
12
12
  When invoked from rails, Genghis looks for a file called mongodb.yml in the RAILS_ROOT/config directory.
13
13
  the format of this file closely mimics that of the database.yml file you know well.
14
14
 
15
15
  development:
16
- servers: localhost:27017
16
+ server: mongodb://localhost
17
17
  databases:
18
18
  paperclip : 'paperclip_files'
19
19
  mongo_mapper : 'mongo_mapper'
@@ -21,22 +21,28 @@ the format of this file closely mimics that of the database.yml file you know we
21
21
  pool_size: 7
22
22
  timeout: 2
23
23
 
24
- In this case, we have a single mongodb server at localhost:2701. The database defines an alias for each
24
+ In this case, we have a single mongodb server at localhost:27017. The database defines an alias for each
25
25
  actual mongo database in your mongodb server. You can then look up the databases by the alias later.
26
26
  The connection_options hash is passed directly to the mongo connection constructor.
27
27
 
28
- === Paired configuration
28
+
29
+ === Replica Sets
29
30
 
30
31
 
31
- If you are using replica pairs, the configuration varies somewhat.
32
+ If you are using replica sets, the configuration varies somewhat.
32
33
 
33
34
  development:
34
- servers:
35
- -_host:27017
36
- - right_host:27017
35
+ replica_set:
36
+ - mongodb://replica1
37
+ - mongodb://replica2
37
38
  databases:
38
39
  paperclip : 'paperclip_files'
39
40
  ...
41
+ authorization:
42
+ paperclip:
43
+ username: pc
44
+ password: secretpassword
45
+ ...
40
46
  connection_options:
41
47
  max_retries: 7
42
48
  pool_size: 5
@@ -63,6 +69,8 @@ the following:
63
69
  Database names can then be used to configure mongo mapper or any other frameworks you have
64
70
 
65
71
  MongoMapper.database = Genghis.databases['mongo_mapper']
72
+ or
73
+ MongoMapper.database = Genghis.database[:mongo_mapper]
66
74
 
67
75
  Similarly you can retrieve the actual mongo database
68
76
 
@@ -74,14 +82,14 @@ Similarly you can retrieve the actual mongo database
74
82
 
75
83
 
76
84
  While MongoDB provides impressive levels of stability and failover, its driver design leaves failover
77
- up to the implementor. This leaves your application subject to connection exceptions that can happen
85
+ up to the implementer. This leaves your application subject to connection exceptions that can happen
78
86
  at any time, possibly littering your code with ugly and difficult to maintain reconnect logic.
79
87
  Genghis's resilience framework solves this for you.
80
88
 
81
89
  === Setup
82
90
 
83
91
 
84
- To make an object resilient, you must first have a replica pair of MongoDB servers. After you have
92
+ To make an object resilient, you must first have a replica set of MongoDB servers. After you have
85
93
  that set up, you're ready to make your objects robust.
86
94
 
87
95
  The following examples will assume you have a mongo_mapper object with the following definition:
@@ -107,7 +115,7 @@ Then you need to need to enlist Genghis's guardian class, which is a protective
107
115
  key :bar, String
108
116
  key :baz, Integer
109
117
  end
110
- end
118
+ end
111
119
 
112
120
  class Foo < Genghis::Guardian
113
121
  protects Unsafe::Foo
@@ -122,7 +130,7 @@ watching over its shoulder, protecting you from any connection related problems.
122
130
 
123
131
  Let's say that while you are executing an update and a connection error occurs. Genghis's guardian
124
132
  realizes something has gone wrong and invalidates the current connection. It then tries to make a
125
- new connection to the other server in the replica pair. If that succeeds, it then re-tries the code
133
+ new connection to the other server in the replica set. If that succeeds, it then re-tries the code
126
134
  that was executing when the failure occurs. It then keeps using this connection.
127
135
 
128
136
  If the second connection fails, it then reverts to the first server in the list. Genghis will flit
@@ -1,25 +1,52 @@
1
1
  require 'yaml'
2
2
  require 'mongo'
3
+ require 'uri'
3
4
 
4
5
  class Genghis
5
6
  include Mongo
6
7
 
8
+ def self.hosts
9
+ @@hosts
10
+ end
11
+
12
+ def self.hosts=(host_list)
13
+ @@hosts = host_list
14
+ end
15
+
7
16
  def self.config=(path)
8
17
  @@config_file = path
9
18
  end
10
19
 
11
20
  def self.environment=(environment = :development)
12
- yaml = YAML.load_file(config_file)
13
- @@config = yaml[environment.to_s]
21
+ yaml = YAML.load_file(config_file)
22
+ @connection = nil
23
+ @@config = yaml[environment.to_s]
14
24
  @@config.each do |k, v|
15
- self.class.instance_eval do
16
- define_method(k.to_sym){v}
25
+ if k == 'server'
26
+ self.hosts = parse_mongo_urls([v])
27
+ elsif k == 'replica_set'
28
+ self.hosts = parse_mongo_urls(v)
29
+ else
30
+ self.class.instance_eval do
31
+ v = HashWithConsistentAccess.new(v) if v.is_a?(::Hash)
32
+ define_method(k.to_sym) { v }
33
+ end
17
34
  end
18
35
  end
36
+ parse_connection_options unless @@config['connection_options'].nil?
37
+ nil
38
+ end
39
+
40
+ def self.on_failure(&block)
41
+ @failure_method = block if block_given?
42
+ end
43
+
44
+ def self.failure_callback
45
+ @failure_method
19
46
  end
20
47
 
21
48
  def self.connection
22
- @connection || safe_create_connection
49
+ @connection ||= safe_create_connection
23
50
  end
24
51
 
25
52
  def self.database(db_alias)
@@ -32,6 +59,23 @@ class Genghis
32
59
 
33
60
  private
34
61
 
62
+ def self.parse_connection_options
63
+ @@connection_options ||= symbolize_keys(default_connection_options.merge(@@config['connection_options']))
64
+ @@retries ||= @@connection_options.delete(:max_retries)
65
+ @@connection_options
66
+ end
67
+
68
+ def self.parse_mongo_urls(urls)
69
+ urls.collect do |url|
70
+ uri = URI.parse(url)
71
+ {:host => uri.host,
72
+ :port => uri.port || 27017,
73
+ :username => uri.user,
74
+ :password => uri.password,
75
+ }
76
+ end
77
+ end
78
+
35
79
  def self.config_file
36
80
  if defined? @@config_file
37
81
  @@config_file
@@ -41,36 +85,53 @@ class Genghis
41
85
  end
42
86
  end
43
87
 
88
+ def self.max_retries=(num)
89
+ @@retries = num
90
+ end
91
+
44
92
  def self.max_retries
45
93
  connection_options
46
- @@retries || 5
94
+ @@retries ||= 5
95
+ end
96
+
97
+ def self.sleep_time=(num)
98
+ @@sleep_time = num
99
+ end
100
+
101
+ def self.sleep_time
102
+ @@sleep_time || 1
47
103
  end
48
104
 
49
105
  def self.connection_options
50
- @@connection_options ||= symbolize_keys((@@config['connection_options']) || default_connection_options)
51
- @@retries ||= @@connection_options.delete(:max_retries)
52
- @@connection_options
106
+ @@connection_options ||= parse_connection_options
53
107
  end
54
108
 
55
109
  def self.default_connection_options
56
110
  {:max_retries => 5,
57
- :pool_size => 5,
58
- :timeout => 5,
59
- :use_slave => false
111
+ :pool_size => 5,
112
+ :timeout => 5,
113
+ :slave_ok => false
60
114
  }
61
115
  end
62
116
 
63
117
  def self.safe_create_connection
64
118
  opts = connection_options
65
- if self.servers.is_a? Array
66
- servers = self.servers.collect{|x| parse_host(x)}
67
- puts "Multi called with #{servers.inspect} #{opts.inspect}"
68
-
119
+ if self.hosts.size > 1
120
+ servers = self.hosts.collect { |x| [x[:host], x[:port]] }
69
121
  connection = Connection.multi(servers, opts)
70
122
  else
71
- host, port = parse_host(self.servers)
72
- connection = Connection.new(host, port, opts)
123
+ host = self.hosts.first
124
+ connection = Connection.new(host[:host], host[:port], opts)
125
+ end
126
+
127
+ if self.hosts.first[:username]
128
+ auth = self.hosts.first
129
+ self.databases.each_pair do |k, db|
130
+ connection.add_auth(db.to_s, auth[:username], auth[:password])
131
+ end
132
+ connection.apply_saved_authentication
73
133
  end
134
+
74
135
  connection
75
136
  end
76
137
 
@@ -81,7 +142,7 @@ class Genghis
81
142
  end
82
143
 
83
144
  def self.symbolize_keys(hash)
84
- hash.inject({}){|memo, (k, v)| memo[k.to_sym] = v; memo}
145
+ hash.inject({}) { |memo, (k, v)| memo[k.to_sym] = v; memo }
85
146
  end
86
147
 
87
148
 
@@ -91,6 +152,13 @@ class Genghis
91
152
  mod.class_eval do
92
153
  extend ClassMethods
93
154
  end
155
+
156
+ end
157
+
158
+ module InstanceMethods
159
+ def safe?
160
+ true
161
+ end
94
162
  end
95
163
 
96
164
  module ClassMethods
@@ -98,16 +166,21 @@ class Genghis
98
166
 
99
167
  def protects(clazz)
100
168
  Guardian.add(clazz)
169
+ Guardian.add_protected_class(self, clazz)
101
170
  @protected_class= clazz
102
171
  end
103
172
 
173
+ def protected_class
174
+ @protected_class
175
+ end
176
+
104
177
  def protected?(clazz)
105
178
  @@protected_classes.include? clazz
106
179
  end
107
180
 
108
- def method_missing(method, *args, &block)
181
+ def method_missing(method, * args, & block)
109
182
  protect_from_exception do
110
- Guardian.make_safe(@protected_class.__send__(method, *args, &block))
183
+ Guardian.make_safe(@protected_class.__send__(method, * args, & block))
111
184
  end
112
185
  end
113
186
 
@@ -119,22 +192,23 @@ class Genghis
119
192
  true
120
193
  end
121
194
 
122
- def protect_from_exception(&block)
123
- success = false
195
+ def protect_from_exception(& block)
196
+ success = false
124
197
  max_retries = Genghis.max_retries
125
- retries = 0
126
- rv = nil
198
+ retries = 0
199
+ rv = nil
127
200
  while !success
128
201
  begin
129
- rv = yield
202
+ rv = yield
130
203
  success = true
131
204
  rescue Mongo::ConnectionFailure => ex
132
- Rails.logger.fatal('Mongo has died ', ex)
133
- WebServiceFailed.deliver_mongo_down(ex, Genghis.connection)
205
+ if Genghis.failure_callback
206
+ Genghis.failure_callback.call(ex, Genghis.connection)
207
+ end
134
208
  retries += 1
135
209
  raise ex if retries > max_retries
136
210
  fix_broken_connection
137
- sleep(1)
211
+ sleep(Genghis.sleep_time)
138
212
  end
139
213
  end
140
214
  rv
@@ -142,22 +216,65 @@ class Genghis
142
216
 
143
217
  def fix_broken_connection
144
218
  Genghis.reconnect
145
- MongoMapper.connection = Genghis.connection
146
- MongoMapper.database = Genghis.databases['mongo_mapper']
219
+ if defined?(MongoMapper)
220
+ MongoMapper.connection = Genghis.connection
221
+ MongoMapper.database = Genghis.databases['mongo_mapper']
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ class HashWithConsistentAccess
228
+
229
+ def initialize(proxied={})
230
+ @proxied = proxied.inject({}) do |memo, (k, v)|
231
+ memo[k.to_s] = v
232
+ memo
233
+ end
234
+ end
235
+
236
+ def each_pair(&block)
237
+ @proxied.each_pair do |k, v|
238
+ yield(k.to_s, v)
147
239
  end
148
240
  end
241
+
242
+ def []=(key, value)
243
+ @proxied[key.to_s] = value
244
+ end
245
+
246
+ def [](key)
247
+ @proxied[key.to_s]
248
+ end
249
+
250
+ def key?(key)
251
+ @proxied.key?(key.to_s)
252
+ end
253
+
254
+ def inspect
255
+ @proxied.inspect
256
+ end
149
257
  end
150
258
 
151
259
 
152
260
  class Guardian
153
261
  alias_method :old_class, :class
154
- instance_methods.each { |m| undef_method m unless m =~ /^__|^old/}
262
+ instance_methods.each { |m| undef_method m unless m =~ /^__|^old/ }
155
263
 
156
264
  include ProxyMethods
157
265
 
158
- def initialize(*args)
266
+ def self.add_protected_class(subclass, protected_class)
267
+ @@protected_mappings ||= {}
268
+ @@protected_mappings[protected_class] = subclass
269
+ end
270
+
271
+ def self.protected_mappings
272
+ @@protected_mappings
273
+ end
274
+
275
+ def initialize(* args)
159
276
 
160
- opts = args.extract_options!
277
+ opts = args.last.is_a?(Hash) ? args.last : {}
161
278
  if opts.empty?
162
279
  if args.empty?
163
280
  what = self.old_class.protected_class.new
@@ -179,7 +296,7 @@ class Genghis
179
296
  protected_classes << clazz
180
297
  end
181
298
 
182
- def self.under_protection?(clazz)
299
+ def self.protecting?(clazz)
183
300
  protected_classes.include?(clazz)
184
301
  end
185
302
 
@@ -188,21 +305,32 @@ class Genghis
188
305
  end
189
306
 
190
307
  def self.make_safe(o)
308
+
191
309
  if o.is_a? Array
192
- Guardian.under_protection?(o.first.class) ? ArrayProxy.new(o) : o
310
+ Guardian.protecting?(o.first.class) ? ArrayProxy.new(o) : o
193
311
  else
194
- Guardian.under_protection?(o.class) ? Guardian.new(o) : o
312
+ class_providing_protection = protected_mappings[o.class] || Guardian
313
+ Guardian.protecting?(o.class) ? class_providing_protection.new(o) : o
195
314
  end
196
315
  end
197
316
 
317
+ def unprotected_object
318
+ @protected
319
+ end
320
+
198
321
 
199
- def method_missing(method, *args, &block)
322
+ def method_missing(method, * args, & block)
200
323
  return true if method == :safe?
201
324
  self.old_class.protect_from_exception do
202
- Guardian.make_safe(@protected.__send__(method, *args, &block))
325
+ Guardian.make_safe(@protected.__send__(method, * args, & block))
203
326
  end
204
327
  end
205
328
 
329
+
330
+ class << self
331
+ alias_method :under_protection?, :protecting?
332
+ end
333
+
206
334
  end
207
335
 
208
336
  class ArrayProxy < Guardian
@@ -213,4 +341,5 @@ class Genghis
213
341
  end
214
342
  end
215
343
 
216
- end
344
+
345
+ end
metadata CHANGED
@@ -1,13 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: genghis
3
3
  version: !ruby/object:Gem::Version
4
- hash: 31
4
+ hash: 11
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
- - 0
9
- - 4
10
- version: 1.0.4
8
+ - 2
9
+ version: "1.2"
11
10
  platform: ruby
12
11
  authors:
13
12
  - Steve Cohen
@@ -15,7 +14,7 @@ autorequire: genghis
15
14
  bindir: bin
16
15
  cert_chain: []
17
16
 
18
- date: 2010-08-31 00:00:00 -07:00
17
+ date: 2010-11-30 00:00:00 -08:00
19
18
  default_executable:
20
19
  dependencies:
21
20
  - !ruby/object:Gem::Dependency
@@ -26,11 +25,12 @@ dependencies:
26
25
  requirements:
27
26
  - - ">="
28
27
  - !ruby/object:Gem::Version
29
- hash: 45
28
+ hash: 19
30
29
  segments:
30
+ - 1
31
+ - 1
31
32
  - 0
32
- - 19
33
- version: "0.19"
33
+ version: 1.1.0
34
34
  type: :runtime
35
35
  version_requirements: *id001
36
36
  description: