genghis 1.0.4 → 1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +21 -13
- data/lib/genghis.rb +169 -40
- metadata +8 -8
data/README.rdoc
CHANGED
@@ -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
|
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
|
-
|
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:
|
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
|
-
|
28
|
+
|
29
|
+
=== Replica Sets
|
29
30
|
|
30
31
|
|
31
|
-
If you are using replica
|
32
|
+
If you are using replica sets, the configuration varies somewhat.
|
32
33
|
|
33
34
|
development:
|
34
|
-
|
35
|
-
-
|
36
|
-
-
|
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
|
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
|
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
|
-
|
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
|
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
|
data/lib/genghis.rb
CHANGED
@@ -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
|
13
|
-
|
21
|
+
yaml = YAML.load_file(config_file)
|
22
|
+
@connection = nil
|
23
|
+
@@config = yaml[environment.to_s]
|
14
24
|
@@config.each do |k, v|
|
15
|
-
|
16
|
-
|
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
|
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
|
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 ||=
|
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
|
58
|
-
:timeout
|
59
|
-
:
|
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.
|
66
|
-
servers
|
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
|
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
|
195
|
+
def protect_from_exception(& block)
|
196
|
+
success = false
|
124
197
|
max_retries = Genghis.max_retries
|
125
|
-
retries
|
126
|
-
rv
|
198
|
+
retries = 0
|
199
|
+
rv = nil
|
127
200
|
while !success
|
128
201
|
begin
|
129
|
-
rv
|
202
|
+
rv = yield
|
130
203
|
success = true
|
131
204
|
rescue Mongo::ConnectionFailure => ex
|
132
|
-
|
133
|
-
|
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(
|
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
|
146
|
-
|
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
|
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.
|
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.
|
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.
|
310
|
+
Guardian.protecting?(o.first.class) ? ArrayProxy.new(o) : o
|
193
311
|
else
|
194
|
-
|
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
|
-
|
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:
|
4
|
+
hash: 11
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 1
|
8
|
-
-
|
9
|
-
|
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-
|
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:
|
28
|
+
hash: 19
|
30
29
|
segments:
|
30
|
+
- 1
|
31
|
+
- 1
|
31
32
|
- 0
|
32
|
-
|
33
|
-
version: "0.19"
|
33
|
+
version: 1.1.0
|
34
34
|
type: :runtime
|
35
35
|
version_requirements: *id001
|
36
36
|
description:
|