zeevex_cluster 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +22 -0
  3. data/Rakefile +44 -0
  4. data/doc/BUGS-zookeeper.txt +60 -0
  5. data/doc/TODO.txt +85 -0
  6. data/lib/zeevex_cluster/base.rb +95 -0
  7. data/lib/zeevex_cluster/coordinator/base_key_val_store.rb +85 -0
  8. data/lib/zeevex_cluster/coordinator/memcached.rb +118 -0
  9. data/lib/zeevex_cluster/coordinator/mysql.rb +396 -0
  10. data/lib/zeevex_cluster/coordinator/redis.rb +101 -0
  11. data/lib/zeevex_cluster/coordinator.rb +29 -0
  12. data/lib/zeevex_cluster/election.rb +102 -0
  13. data/lib/zeevex_cluster/message.rb +52 -0
  14. data/lib/zeevex_cluster/nil_logger.rb +7 -0
  15. data/lib/zeevex_cluster/serializer/json_hash.rb +67 -0
  16. data/lib/zeevex_cluster/serializer.rb +27 -0
  17. data/lib/zeevex_cluster/static.rb +67 -0
  18. data/lib/zeevex_cluster/strategy/base.rb +92 -0
  19. data/lib/zeevex_cluster/strategy/cas.rb +403 -0
  20. data/lib/zeevex_cluster/strategy/static.rb +55 -0
  21. data/lib/zeevex_cluster/strategy/unclustered.rb +9 -0
  22. data/lib/zeevex_cluster/strategy/zookeeper.rb +163 -0
  23. data/lib/zeevex_cluster/strategy.rb +12 -0
  24. data/lib/zeevex_cluster/synchronized.rb +46 -0
  25. data/lib/zeevex_cluster/unclustered.rb +11 -0
  26. data/lib/zeevex_cluster/util/logging.rb +7 -0
  27. data/lib/zeevex_cluster/util.rb +15 -0
  28. data/lib/zeevex_cluster/version.rb +3 -0
  29. data/lib/zeevex_cluster.rb +29 -0
  30. data/script/election.rb +46 -0
  31. data/script/memc.rb +13 -0
  32. data/script/mysql.rb +25 -0
  33. data/script/redis.rb +14 -0
  34. data/script/repl +10 -0
  35. data/script/repl.rb +8 -0
  36. data/script/ser.rb +11 -0
  37. data/script/static.rb +34 -0
  38. data/script/testall +2 -0
  39. data/spec/cluster_static_spec.rb +49 -0
  40. data/spec/cluster_unclustered_spec.rb +32 -0
  41. data/spec/coordinator/coordinator_memcached_spec.rb +102 -0
  42. data/spec/message_spec.rb +38 -0
  43. data/spec/serializer/json_hash_spec.rb +68 -0
  44. data/spec/shared_master_examples.rb +20 -0
  45. data/spec/shared_member_examples.rb +39 -0
  46. data/spec/shared_non_master_examples.rb +8 -0
  47. data/spec/spec_helper.rb +14 -0
  48. data/zeevex_cluster.gemspec +43 -0
  49. metadata +298 -0
@@ -0,0 +1,396 @@
1
+ require 'zeevex_cluster/coordinator/base_key_val_store'
2
+
3
+ #
4
+ # example setup for mysql coordinator:
5
+ #
6
+ # grant all privileges on zcluster.* to 'zcluster'@localhost identified by 'zclusterp';
7
+ #
8
+ # drop table kvstore;
9
+ #
10
+ # create table kvstore (keyname varchar(255) not null,
11
+ # value mediumtext,
12
+ # namespace varchar(255) default '',
13
+ # flags integer default 0,
14
+ # created_at datetime,
15
+ # expires_at datetime,
16
+ # updated_at datetime,
17
+ # lock_version integer default 0);
18
+ #
19
+ # create unique index keyname_idx on kvstore (keyname, namespace);
20
+ #
21
+
22
+ #
23
+ # expired key handling hasn't been well-tested
24
+ #
25
+ module ZeevexCluster::Coordinator
26
+ class Mysql < BaseKeyValStore
27
+ ERR_DUPLICATE_KEY = 1062
28
+
29
+ def self.setup
30
+ unless @setup
31
+ require 'mysql2'
32
+ BaseKeyValStore.setup
33
+
34
+ @setup = true
35
+ end
36
+ end
37
+
38
+ def initialize(options = {})
39
+ super
40
+ @table = @options[:table] || 'kvstore'
41
+ @logger = @options[:logger] || Logger.new(STDOUT)
42
+ @namespace = @options.fetch(:namespace, '')
43
+ @client ||= Mysql2::Client.new(:host => options[:server] || 'localhost',
44
+ :port => options[:port] | 3306,
45
+ :database => options[:database] || 'zcluster',
46
+ :username => options[:username],
47
+ :password => options[:password],
48
+ :reconnect => true,
49
+ :symbolize_keys => true,
50
+ :cache_rows => false,
51
+ :application_timezone => :utc,
52
+ :database_timezone => :utc)
53
+ end
54
+
55
+ #
56
+ # Add the value for a key to the DB if there is no existing entry
57
+ # Serializes unless :raw => true
58
+ #
59
+ # Returns true if key was added, false if key already had a value
60
+ #
61
+ def add(key, value, options = {})
62
+ key = to_key(key)
63
+ value = serialize_value(value, is_raw?(options))
64
+ res = do_insert_row({:keyname => key, :value => value, :namespace => @namespace},
65
+ :expiration => options.fetch(:expiration, @expiration))
66
+ res[:affected_rows] == 1
67
+ rescue Mysql2::Error => e
68
+ case e.error_number
69
+ # duplicate key
70
+ # see http://www.briandunning.com/error-codes/?source=MySQL
71
+ when ERR_DUPLICATE_KEY
72
+ false
73
+ else
74
+ raise ZeevexCluster::Coordinator::ConnectionError.new, "Unhandled mysql error: #{e.errno} #{e.message}", e
75
+ end
76
+ rescue
77
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
78
+ end
79
+
80
+ #
81
+ # Set the value for a key, serializing unless :raw => true
82
+ #
83
+ def set(key, value, options = {})
84
+ key = to_key(key)
85
+ value = serialize_value(value, is_raw?(options))
86
+ row = {:keyname => key, :value => value}
87
+
88
+ res = do_upsert_row(row, :expiration => options.fetch(:expiration, @expiration), :skip_locking => true)
89
+ res[:success]
90
+ rescue ::Mysql2::Error
91
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
92
+ end
93
+
94
+ #
95
+ # Delete key from database; true if key existed beforehand
96
+ #
97
+ def delete(key, options = {})
98
+ res = do_delete_row(:keyname => to_key(key))
99
+ res[:success]
100
+ end
101
+
102
+ #
103
+ # Block is passed the current value, and returns the updated value.
104
+ #
105
+ # Block can raise DontChange to simply exit the block without updating.
106
+ #
107
+ # returns nil for no value
108
+ # returns false for failure (somebody else set)
109
+ # returns true for success
110
+ #
111
+ def cas(key, options = {})
112
+ key = to_key(key)
113
+
114
+ orig_row = do_get_first(key)
115
+ return nil unless orig_row
116
+
117
+ expiration = options.fetch(:expiration, @expiration)
118
+
119
+ newval = serialize_value(yield(deserialize_value(orig_row[:value], is_raw?(options))), is_raw?(options))
120
+ updates = {:value => newval}
121
+ res = do_update_row(simple_cond(orig_row), updates, :expiration => options.fetch(:expiration, @expiration))
122
+ case res
123
+ when false then false
124
+ when true then true
125
+ else
126
+ raise ZeevexCluster::Coordinator::ConnectionError, "Unhandled return value from do_update_row - #{res.inspect}"
127
+ end
128
+ rescue ZeevexCluster::Coordinator::DontChange => e
129
+ false
130
+ rescue ::Mysql2::Error
131
+ logger.error "got error in cas: #{$!.inspect}"
132
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
133
+ rescue
134
+ logger.error "got general error in cas: #{$!.inspect}"
135
+ raise
136
+ end
137
+
138
+ #
139
+ # Fetch the value for a key, deserializing unless :raw => true
140
+ #
141
+ def get(key, options = {})
142
+ key = to_key(key)
143
+ row = do_get_first key
144
+ return nil if row.nil?
145
+
146
+ if !is_raw?(options)
147
+ deserialize_value(row[:value])
148
+ else
149
+ row[:value]
150
+ end
151
+ rescue ::Mysql2::Error
152
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
153
+ end
154
+
155
+ # append string vlaue to an entry
156
+ # does NOT serialize
157
+ def append(key, str, options = {})
158
+ newval = Literal.new %{CONCAT(value, #{qval str})}
159
+ do_update_row({:keyname => to_key(key)}, {:value => newval})
160
+ rescue ::Mysql2::Error
161
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
162
+ end
163
+
164
+ # prepend string value to an entry
165
+ # does NOT serialize
166
+ def prepend(key, str, options = {})
167
+ newval = Literal.new %{CONCAT(#{qval str}, value)}
168
+ do_update_row({:keyname => to_key(key)}, {:value => newval})
169
+ rescue ::Mysql2::Error
170
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
171
+ end
172
+
173
+ protected
174
+
175
+ # mysql get wrapper. returns just the resultset as a list of hashes
176
+ # which may be nil or empty list if none matched.
177
+ def do_get(key, options = {})
178
+ conditions = []
179
+ conditions << %{#{qcol 'keyname'} = #{qval key}}
180
+ conditions << %{#{qcol 'namespace'} = #{qval @namespace}}
181
+ query(%{SELECT * from #@table where #{conditions.join(' AND ')};})[:resultset]
182
+ end
183
+
184
+ def do_get_first(*args)
185
+ res = do_get(*args)
186
+ res && res.first
187
+ end
188
+
189
+ def simple_cond(row)
190
+ slice_hash row, :namespace, :keyname, :lock_version
191
+ end
192
+
193
+ def make_comparison(trip)
194
+ trip = case trip.count
195
+ when 1 then [trip[0], 'IS NOT', nil]
196
+ when 2 then [trip[0], '=', trip[1]]
197
+ when 3 then trip
198
+ else raise 'Must have 1-3 arguments'
199
+ end
200
+ %{#{qcol trip[0]} #{trip[1]} #{qval trip[2]}}
201
+ end
202
+
203
+ def make_row_conditions(cond)
204
+ cond = {:namespace => @namespace}.merge(cond)
205
+ make_conditions(cond)
206
+ end
207
+
208
+ def make_conditions(cond)
209
+ case cond
210
+ when String then cond
211
+ when Array then cond.map {|trip| make_comparison(trip) }.join(' AND ')
212
+ when Hash then cond.map {|(k,v)| make_comparison([k, v].flatten) }.join(' AND ')
213
+ else raise "Unknown condition format: #{cond.inspect}"
214
+ end
215
+ end
216
+
217
+ def do_insert_row(row, options = {})
218
+ (row[:keyname] && row[:value]) or raise ArgumentError, 'Must specify at least key and value'
219
+ now = self.now
220
+ row = row.merge(:namespace => @namespace)
221
+ row = {:created_at => now, :updated_at => now}.merge(row) unless options[:skip_timestamps]
222
+ row = {:expires_at => now + options[:expiration]}.merge(row) if options[:expiration]
223
+ res = query %{INSERT INTO #@table (#{row.keys.map {|k| qcol(k)}.join(', ')})
224
+ values (#{row.values.map {|k| qval(k)}.join(', ')});}
225
+ res[:success] = (res[:affected_rows] == 1)
226
+ res
227
+ end
228
+
229
+ def do_upsert_row(row, options = {})
230
+ (row[:keyname] && row[:value]) or raise ArgumentError, 'Must specify at least key and value'
231
+ now = self.now
232
+
233
+ ## what values are set if the row is inserted
234
+ row = row.merge(:namespace => @namespace)
235
+ row = {:created_at => now, :updated_at => now}.merge(row) unless options[:skip_timestamps]
236
+ row = {:expires_at => now + options[:expiration]}.merge(row) if options[:expiration]
237
+
238
+ ## values updated if row already exists
239
+ # these columns shouldn't be set on update
240
+ updatable_row = trim_hash(row, [:created_at, :keyname, :namespace, :lock_version])
241
+ # update of a row should increment the lock version, rather than setting it
242
+ updatable_row.merge!(:lock_version => Literal.new('lock_version + 1')) unless options[:skip_locking]
243
+
244
+ res = query %{INSERT INTO #@table (#{row.keys.map {|k| qcol(k)}.join(', ')})
245
+ values (#{row.values.map {|k| qval(k)}.join(', ')})
246
+ ON DUPLICATE KEY UPDATE
247
+ #{updatable_row.map {|(k,v)| "#{qcol k} = #{qval v}"}.join(', ')};}
248
+
249
+ # see http://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html for WTF affected_rows
250
+ # overloading
251
+ res[:success] = [1,2].include?(res[:affected_rows])
252
+ res[:upsert_type] = case res[:affected_rows]
253
+ when 1 then :insert
254
+ when 2 then :update
255
+ else :none
256
+ end
257
+ res
258
+ end
259
+
260
+ #
261
+ # note, unlike some of the do_* functions, returns a simple boolean for success
262
+ #
263
+ def do_update_row(quals, newattrvals, options = {})
264
+ quals[:keyname] or raise 'Must specify at least the full key in an update'
265
+ conditions = 'WHERE ' + make_row_conditions(quals)
266
+ newattrvals = {:updated_at => now}.merge(newattrvals) unless options[:skip_timestamps]
267
+ newattrvals = {:expires_at => now + options[:expiration]}.merge(newattrvals) if options[:expiration]
268
+ newattrvals.merge!({:lock_version => Literal.new('lock_version + 1')}) unless options[:skip_locking]
269
+ updates = newattrvals.map do |(key, val)|
270
+ "#{qcol key} = #{qval val}"
271
+ end
272
+ statement = %{UPDATE #@table SET #{updates.join(', ')} #{conditions};}
273
+ res = query statement
274
+ res[:affected_rows] == 0 ? false : true
275
+ end
276
+
277
+ def do_delete_row(quals, options = {})
278
+ quals[:keyname] or raise 'Must specify at least the key in a delete'
279
+ conditions = 'WHERE ' + make_row_conditions(quals)
280
+ statement = %{DELETE from #@table #{conditions};}
281
+ res = query statement
282
+ res[:success] = res[:affected_rows] > 0
283
+ res
284
+ end
285
+
286
+ def clear_expired_rows
287
+ statement = %{DELETE FROM #@table WHERE #{qcol 'expires_at'} < #{qnow} and #{qcol 'namespace'} = #{qval @namespace};}
288
+ @client.query statement
289
+ true
290
+ rescue ::Mysql2::Error
291
+ log_exception($!, statement)
292
+ false
293
+ rescue
294
+ logger.error %{Unhandled error in query: #{$!.inspect}\nstatement=[#{statement}]\n#{$!.backtrace.join("\n")}}
295
+ end
296
+
297
+ #
298
+ # chokepoint for *most* queries issued to MySQL, except the one from `clear_expired_rows` as we call it.
299
+ #
300
+ # returns a hash containing values returned from mysql2 API
301
+ #
302
+ def query(statement, options = {})
303
+ unless options[:ignore_expiration]
304
+ clear_expired_rows
305
+ end
306
+ logger.debug "[#{statement}]"
307
+ res = @client.query statement, options
308
+ {:resultset => res, :affected_rows => @client.affected_rows, :last_id => @client.last_id}
309
+ rescue ::Mysql2::Error
310
+ log_exception($!, statement)
311
+ raise
312
+ rescue
313
+ logger.error %{Unhandled error in query: #{$!.inspect}\nstatement=[#{statement}]\n#{$!.backtrace.join("\n")}}
314
+ raise
315
+ end
316
+
317
+ #
318
+ # extract a hash with a subset of keys
319
+ #
320
+ def slice_hash(src, *keys)
321
+ hash = src.class.new
322
+ Array(keys).flatten.each { |k| hash[k] = src[k] if src.has_key?(k) }
323
+ hash
324
+ end
325
+
326
+ #
327
+ # return a new hash with a set of keys removed
328
+ #
329
+ def trim_hash(src, *keys)
330
+ hash = src.class.new
331
+ keys = Array(keys).flatten
332
+ src.keys.each { |k| hash[k] = src[k] unless keys.include?(k) }
333
+ hash
334
+ end
335
+
336
+ def log_exception(e, statement=nil)
337
+ logger.error %{Mysql exception errno=#{e.errno}, sql_state=#{e.sql_state}, message=#{e.message}, statement=[#{statement || 'UNKNOWN'}]\n#{e.backtrace.join("\n")}}
338
+ end
339
+
340
+ # quote quotes in a quotable string.
341
+ def quote_string(s)
342
+ s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
343
+ end
344
+
345
+ # quote a column name
346
+ def qcol(colname)
347
+ %{`#{colname}`}
348
+ end
349
+
350
+ # quote a value - takes the quoting/translation style from the Ruby type of the value itself
351
+ # rather than the column definition as e.g. ActiveRecord might.
352
+ def qval(val)
353
+ case val
354
+ when Literal then val
355
+ when String then %{'#{quote_string val}'}
356
+ when true then '1'
357
+ when false then '0'
358
+ when nil then 'NULL'
359
+ when Bignum then val.to_s('F')
360
+ when Numeric then val.to_s
361
+ when Time then qval(val.utc.strftime('%Y-%m-%d-%H:%M:%S'))
362
+ else val
363
+ end
364
+ end
365
+
366
+ # quoted time value for now
367
+ def qnow
368
+ qval now
369
+ end
370
+
371
+ #
372
+ # now, as a Time, in UTC
373
+ #
374
+ def now
375
+ Time.now.utc
376
+ end
377
+
378
+ #
379
+ # unlike the base implementation, we don't fold the namespace into the key
380
+ # we leave that in a separate column
381
+ #
382
+ def to_key(key)
383
+ if @options[:to_key_proc]
384
+ @options[:to_key_proc].call(key)
385
+ else
386
+ key.to_s
387
+ end
388
+ end
389
+
390
+ #
391
+ # class used to indicate a value to be passed to MySQL unquoted; useful for
392
+ # e.g. arithmetic expressions
393
+ #
394
+ class Literal < String; end
395
+ end
396
+ end
@@ -0,0 +1,101 @@
1
+ require 'zeevex_cluster/coordinator/base_key_val_store'
2
+
3
+ module ZeevexCluster::Coordinator
4
+ class Redis < BaseKeyValStore
5
+ def self.setup
6
+ unless @setup
7
+ require 'redis'
8
+ BaseKeyValStore.setup
9
+ @setup = true
10
+ end
11
+ end
12
+
13
+ def initialize(options = {})
14
+ super
15
+ @client ||= ::Redis.new :host => @server, :port => @port
16
+ end
17
+
18
+ def add(key, value, options = {})
19
+ if @client.setnx(to_key(key), serialize_value(value, is_raw?(options)))
20
+ @client.expire to_key(key), options.fetch(:expiration, @expiration)
21
+ true
22
+ else
23
+ false
24
+ end
25
+ rescue ::Redis::CannotConnectError
26
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
27
+ end
28
+
29
+ def set(key, value, options = {})
30
+ status( @client.setex(to_key(key),
31
+ options.fetch(:expiration, @expiration),
32
+ serialize_value(value, is_raw?(options))) ) == STATUS_OK
33
+ rescue ::Redis::CannotConnectError
34
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
35
+ end
36
+
37
+ def delete(key, options = {})
38
+ @client.del(to_key(key)) == 1
39
+ rescue ::Redis::CannotConnectError
40
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
41
+ end
42
+
43
+ #
44
+ # Block is passed the current value, and returns the updated value.
45
+ #
46
+ # Block can raise DontChange to simply exit the block without updating.
47
+ #
48
+ # returns nil for no value
49
+ # returns false for failure (somebody else set)
50
+ # returns true for success
51
+ #
52
+ def cas(key, options = {})
53
+ key = to_key(key)
54
+ @client.unwatch
55
+ @client.watch key
56
+ orig_val = @client.get key
57
+ return nil if orig_val.nil?
58
+
59
+ expiration = options.fetch(:expiration, @expiration)
60
+
61
+ newval = serialize_value(yield(deserialize_value(orig_val, is_raw?(options))), is_raw?(options))
62
+ res = @client.multi do
63
+ if expiration
64
+ @client.setex key, expiration, newval
65
+ else
66
+ @client.set key, newval
67
+ end
68
+ end
69
+ @client.unwatch
70
+ case res
71
+ when nil then false
72
+ when Array then true
73
+ else raise "Unhandled return value from multi - #{res.inspect}"
74
+ end
75
+ rescue ZeevexCluster::Coordinator::DontChange => e
76
+ false
77
+ rescue ::Redis::CannotConnectError
78
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
79
+ end
80
+
81
+ def get(key, options = {})
82
+ deserialize_value(@client.get(to_key(key)), is_raw?(options))
83
+ rescue ::Redis::CannotConnectError
84
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
85
+ end
86
+
87
+ protected
88
+
89
+ STATUS_OK = 'OK'
90
+
91
+ def status(response)
92
+ case response
93
+ when nil then nil
94
+ when String then response.chomp
95
+ else
96
+ raise ArgumentError, 'This should only be called on results from set / setex, etc.'
97
+ end
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,29 @@
1
+ module ZeevexCluster
2
+ module Coordinator
3
+ # flow control exceptions used in these classes
4
+ class DontChange < StandardError; end
5
+
6
+ # errors throw by these classes
7
+ class CoordinatorError < StandardError
8
+ attr_accessor :chained
9
+ def initialize(message, original = nil)
10
+ @chained = original
11
+ super(message)
12
+ end
13
+ def to_s
14
+ @chained ? super + "; chained = #{@chained.inspect}" : super
15
+ end
16
+ end
17
+
18
+ class ConnectionError < CoordinatorError; end
19
+ class ConsistencyError < CoordinatorError; end
20
+
21
+ def self.create(coordinator_type, options)
22
+ require 'zeevex_cluster/coordinator/' + coordinator_type
23
+ clazz = self.const_get(coordinator_type.capitalize)
24
+ raise ArgumentError, "Unknown coordinator type: #{coordinator_type}" unless clazz
25
+ ZeevexCluster.Synchronized(clazz.new(options))
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,102 @@
1
+ require 'zeevex_cluster/strategy'
2
+ require 'zeevex_cluster/strategy/cas'
3
+
4
+ module ZeevexCluster
5
+ class Election < Base
6
+
7
+ def initialize(options = {})
8
+ super
9
+ raise ArgumentError, 'Must specify :cluster_name' unless options[:cluster_name]
10
+
11
+ if options[:hooks]
12
+ add_hooks(options[:hooks])
13
+ end
14
+
15
+ unless (@strategy = options[:strategy])
16
+ stype = options[:strategy_type] || 'cas'
17
+ @strategy = ZeevexCluster::Strategy.create(stype,
18
+ {:nodename => options.fetch(:nodename, Socket.gethostname),
19
+ :cluster_name => options[:cluster_name],
20
+ :logger => options[:logger]}.
21
+ merge(options[:backend_options]))
22
+ end
23
+ @strategy.add_hook_observer Proc.new { |*args| hook_observer(*args) }
24
+
25
+ after_initialize
26
+ end
27
+
28
+ def master?
29
+ member? && @strategy.am_i_master?
30
+ end
31
+
32
+ ##
33
+ ## Make this node the master, returning true if successful. No-op for now.
34
+ ##
35
+ def make_master!
36
+ return unless member?
37
+ if @strategy.steal_election!
38
+ true
39
+ else
40
+ false
41
+ end
42
+ end
43
+
44
+ ##
45
+ ## Resign from mastership; returns false if this is the only node.
46
+ ##
47
+ ## No-op for now.
48
+ ##
49
+ def resign!(delay = nil)
50
+ @strategy.resign delay
51
+ end
52
+
53
+ def campaign!
54
+ @strategy.start unless @strategy.started?
55
+ # stop sitting out the election
56
+ @strategy.resign 0
57
+ end
58
+
59
+ ##
60
+ ## Return name of master node
61
+ ##
62
+ def master
63
+ member? && @strategy.master_node && @strategy.master_node[:nodename]
64
+ end
65
+
66
+ def join
67
+ return if member?
68
+ @member = true
69
+ @strategy.start unless @strategy.started?
70
+ end
71
+
72
+ def leave
73
+ return unless member?
74
+ resign! if master?
75
+ @strategy.stop if @strategy.started?
76
+ ensure
77
+ @member = false
78
+ end
79
+
80
+ def member?
81
+ @strategy.member?
82
+ end
83
+
84
+ def members
85
+ member? && (@strategy.respond_to?(:members) ? @strategy.members : [@nodename])
86
+ end
87
+
88
+ protected
89
+
90
+ def hook_observer(hook_name, source, *args)
91
+ logger.debug "#{self.class} observed hook: #{hook_name} #{args.inspect}"
92
+ case hook_name
93
+ when :status_change
94
+ run_hook :status_change, args[0], args[1]
95
+ when :cluster_status_change
96
+ run_hook :cluster_status_change, args[0], args[1]
97
+ else
98
+ run_hook "strategy_#{hook_name}".to_sym, *args
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,52 @@
1
+ require 'zeevex_cluster/serializer'
2
+
3
+ module ZeevexCluster
4
+ class Message < Hash
5
+
6
+ include ZeevexCluster::Serializer
7
+
8
+ REQUIRED_KEYS = %w{source sequence sent_at expires_at contents content_type}.
9
+ map {|x| x.to_sym}
10
+ ALLOWED_KEYS = %w{vclock options flags encoding}.
11
+ map {|x| x.to_sym}
12
+ FORBIDDEN_KEYS = ['$primitive', '$type', '$encoding']
13
+
14
+ ALL_KEYS = REQUIRED_KEYS + ALLOWED_KEYS
15
+
16
+ def initialize(hash)
17
+ super()
18
+ hash.keys.each do |x|
19
+ raise ArgumentError, 'Only symbol keys are allowed in Messages' unless x.is_a?(Symbol)
20
+ end
21
+ self.merge! :content_type => 'application/json'
22
+ self.merge! hash
23
+ end
24
+
25
+ def valid?
26
+ vkeys = self.keys
27
+ (REQUIRED_KEYS - vkeys).empty? &&
28
+ (FORBIDDEN_KEYS & vkeys).empty?
29
+ end
30
+
31
+ def respond_to?(meth)
32
+ if ALL_KEYS.include?(meth.to_s.chomp('=').to_sym)
33
+ true
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ protected
40
+
41
+ def method_missing(meth, *args, &block)
42
+ if ALL_KEYS.include?(meth.to_sym)
43
+ self[meth]
44
+ elsif ALL_KEYS.include?(key = meth.to_s.chomp('=').to_sym)
45
+ self[key] = args[0]
46
+ else
47
+ super
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,7 @@
1
+ module ZeevexCluster
2
+ class NilLogger
3
+ def method_missing(symbol, *args)
4
+ nil
5
+ end
6
+ end
7
+ end