zeevex_cluster 0.2.1

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.
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