genghis 1.0.4 → 1.2
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/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:
|