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
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,22 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :development, :test do
4
+ gem 'pry'
5
+ gem 'pry-remote'
6
+ gem 'pry-doc'
7
+ gem 'pry-nav'
8
+ gem 'pry-buffers'
9
+ gem 'pry-syntax-hacks'
10
+ gem 'pry-git', :platform => :mri
11
+ gem 'jist'
12
+ gem 'ruby18_source_location', :platform => :mri_18
13
+ gem 'mysql2', :platform => :mri
14
+ end
15
+
16
+ group :development, :test do
17
+ gem 'zk', :path => '/Users/Shared/squid/src/github/cluster/zk'
18
+ gem 'zk-group', :path => '/Users/Shared/squid/src/github/cluster/zk-group'
19
+ end
20
+
21
+ # Specify your gem's dependencies in zeevex_cluster.gemspec
22
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+
5
+ require 'rspec/core/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ namespace :spec do
10
+ SPEC_PLATFORMS = ENV.has_key?('SPEC_PLATFORMS') ?
11
+ ENV['SPEC_PLATFORMS'].split(/ +/) :
12
+ %w{1.9.3-p448 2.0.0-p247 1.8.7-p374}
13
+
14
+ desc "Run on three Rubies"
15
+ task :platforms do
16
+ # current = %x[rbenv version | awk '{print $1}']
17
+
18
+ fail = false
19
+ SPEC_PLATFORMS.each do |version|
20
+ puts "Switching to #{version}"
21
+ Bundler.with_clean_env do
22
+ system %{bash -c 'eval "$(rbenv init -)" && rbenv use #{version} && rbenv rehash && ruby -v && bundle exec rake spec'}
23
+ end
24
+ if $?.exitstatus != 0
25
+ fail = true
26
+ break
27
+ end
28
+ end
29
+
30
+ exit (fail ? 1 : 0)
31
+ end
32
+
33
+ desc 'Install gems for all tested rubies'
34
+ task :platform_setup do
35
+ SPEC_PLATFORMS.each do |version|
36
+ puts "Setting up platform #{version}"
37
+ Bundler.with_clean_env do
38
+ system %{bash -c 'eval "$(rbenv init -)" && rbenv use #{version} && rbenv rehash && gem install bundler && bundle install'}
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ task :default => 'spec'
@@ -0,0 +1,60 @@
1
+ When suspending master with Ctrl-Z for a while, the other 2 nodes in a
2
+ 3 node cluster elect a new master and consider the suspended node to
3
+ have exited.
4
+
5
+ Resuming the suspended node shows that it is not fully aware of its altered
6
+ status, though note that zk-group shows it's not a member anymore.
7
+
8
+ [17] MASTER[2] pry(main)>
9
+ $c.@strategy._get_synchronized_object.@elector.leader?
10
+ => true
11
+ [18] MASTER[2] pry(main)>
12
+ [19] MASTER[2] pry(main)> $c.members
13
+ => ["blacktip.esquimaux.org:/dev/ttys005",
14
+ "blacktip.esquimaux.org:/dev/ttys006"]
15
+ [20] MASTER[2] pry(main)> $c.nodename
16
+ => "blacktip.esquimaux.org:/dev/ttys003"
17
+
18
+ Do we need to act on ZK session/connection changes?
19
+
20
+ ZK::Client::Base#methods: assert_we_are_not_on_the_event_dispatch_thread! children delete event_dispatch_thread? event_handler exists? get get_acl inspect register session_id session_passwd set set_acl set_debug_level stat wait_until_connected watcher
21
+ ZK::Client::StateMixin#methods: on_connected on_connecting on_expired_session on_state_change
22
+ Zookeeper::Constants#methods: event_by_value state_by_value
23
+ ZK::Client::Unixisms#methods: block_until_node_deleted find mkdir_p rm_rf
24
+ ZK::Client::Conveniences#methods: defer election_candidate election_observer exclusive_locker locker ping? queue shared_locker with_lock
25
+ ZK::Logging#methods: logger
26
+ ZK::Client::Threaded#methods: associating? client_state close close! closed? connect connected? connecting? create expired_session? on_exception on_threadpool? pause_before_fork_in_parent raw_event_handler reopen resume_after_fork_in_parent state wait_until_closed wait_until_connected_or_dying
27
+ instance variables: @client_state @cnx @cond @connection_timeout @event_handler @fork_subs @host @last_cnx_state @mutex @pid @reconnect @reconnect_thread @retry_duration @threadpool
28
+
29
+ ----
30
+ log after resume:
31
+
32
+ [1] + 28262 continued bundle exec ./script/election.rb zookeeper
33
+ [8] MASTER[3] pry(main)>
34
+ [9] MASTER[3] pry(main)> D, [2012-12-24T14:08:51.765368 #28262] DEBUG -- : EventHandler#process dispatching event: #<Zookeeper::Callbacks::WatcherCallback:0x109b0a1d0 @path="/_zk/groups/foobs", @type=4, @proc=#<Proc:0x00000001098d5630@/Users/robertsanders/.rvm/gems/ree-1.8.7-2012.02@zeevex_cluster/gems/zookeeper-1.4.1/lib/zookeeper/callbacks.rb:24>, @state=3, @completed=false, @context=nil>
35
+ D, [2012-12-24T14:08:51.766028 #28262] DEBUG -- : EventHandler#process dispatching event: #<Zookeeper::Callbacks::WatcherCallback:0x10993b278 @path="", @type=-1, @proc=#<Proc:0x00000001098d5630@/Users/robertsanders/.rvm/gems/ree-1.8.7-2012.02@zeevex_cluster/gems/zookeeper-1.4.1/lib/zookeeper/callbacks.rb:24>, @state=1, @completed=false, @context=nil>
36
+ D, [2012-12-24T14:08:51.766576 #28262] DEBUG -- : called #<ZK::EventHandlerSubscription::Base:0x109d6d5d0 @mutex=#<ZK::Monitor:0x109d6d530 @mon_owner=nil, @mon_waiting_queue=[], @mon_entering_queue=[], @mon_count=0>, @path="/_zk/groups/foobs", @parent=#<ZK::EventHandler:0x109d7b0e0 @thread_opt=:single, @mutex=#<ZK::Monitor:0x109d7abb8 @mon_owner=nil, @mon_waiting_queue=[], @mon_entering_queue=[], @mon_count=0>, @default_watcher_block=#<Proc:0x0000000109f905d8@/Users/Shared/squid/src/github/cluster/zk/lib/zk/event_handler.rb:251>, @orig_pid=28262, @state=:running, @zk=#<ZK::Client::Threaded:2230049920 zk_session_id=0x13bc3f378bd0078 ...>, @outstanding_watches={:child=>#<Set: {}>, :data=>#<Set: {}>}, @callbacks={"/_zkelection/foobs/leader_ack"=>[], :all_node_events=>[], "/_zk/groups/foobs"=>[#<ZK::EventHandlerSubscription::Base:0x109d6d5d0 ...>], :all_state_events=>[], "state_1"=>[], "state_3"=>[#<ZK::EventHandlerSubscription::Base:0x109d6cf40 @mutex=#<ZK::Monitor:0x109d6cea0 @mon_owner=nil, @mon_waiting_queue=[], @mon_entering_queue=[], @mon_count=0>, @path="state_3", @parent=#<ZK::EventHandler:0x109d7b0e0 ...>, @interests=#<Set: {:deleted, :changed, :child, :created}>, @callable=#<Proc:0x0000000109d96520@/Users/Shared/squid/src/github/cluster/zk-group/lib/zk-group/group.rb:90>>]}>, @interests=#<Set: {:child}>, @callable=#<Proc:0x0000000109d96868@/Users/Shared/squid/src/github/cluster/zk-group/lib/zk-group/group.rb:86>> with [#<Zookeeper::Callbacks::WatcherCallback:0x109b0a1d0 @path="/_zk/groups/foobs", @type=4, @proc=#<Proc:0x00000001098d5630@/Users/robertsanders/.rvm/gems/ree-1.8.7-2012.02@zeevex_cluster/gems/zookeeper-1.4.1/lib/zookeeper/callbacks.rb:24>, @state=3, @zk=#<ZK::Client::Threaded:2230049920 zk_session_id=0x13bc3f378bd0078 ...>, @completed=true, @context=nil>] on threadpool
37
+ D, [2012-12-24T14:08:51.766643 #28262] DEBUG -- : session state was invalid, calling reopen
38
+ D, [2012-12-24T14:08:51.766687 #28262] DEBUG -- : EventHandler#process dispatching event: #<Zookeeper::Callbacks::WatcherCallback:0x1099259a0 @path="", @type=-1, @proc=#<Proc:0x00000001098d5630@/Users/robertsanders/.rvm/gems/ree-1.8.7-2012.02@zeevex_cluster/gems/zookeeper-1.4.1/lib/zookeeper/callbacks.rb:24>, @state=-112, @completed=false, @context=nil>
39
+ D, [2012-12-24T14:08:51.766743 #28262] DEBUG -- : reopening, no fork detected
40
+ D, [2012-12-24T14:08:51.771478 #28262] DEBUG -- : EventHandler#process dispatching event: #<Zookeeper::Callbacks::WatcherCallback:0x1098fa188 @path="", @type=-1, @proc=#<Proc:0x00000001098d5630@/Users/robertsanders/.rvm/gems/ree-1.8.7-2012.02@zeevex_cluster/gems/zookeeper-1.4.1/lib/zookeeper/callbacks.rb:24>, @state=3, @completed=false, @context=nil>
41
+ D, [2012-12-24T14:08:51.771547 #28262] DEBUG -- : wait_until_connected_or_dying @last_cnx_state: 3, time_left? true, @client_state: :running
42
+ D, [2012-12-24T14:08:51.771710 #28262] DEBUG -- : reopen returned: :connected
43
+ D, [2012-12-24T14:08:51.771908 #28262] DEBUG -- : broadcast_membership_change! received event #<Zookeeper::Callbacks::WatcherCallback:0x109b0a1d0 @path="/_zk/groups/foobs", @type=4, @proc=#<Proc:0x00000001098d5630@/Users/robertsanders/.rvm/gems/ree-1.8.7-2012.02@zeevex_cluster/gems/zookeeper-1.4.1/lib/zookeeper/callbacks.rb:24>, @state=3, @zk=#<ZK::Client::Threaded:2230049920 zk_session_id=0x13bc3f378bd007b ...>, @completed=true, @context=nil>
44
+ D, [2012-12-24T14:08:51.772264 #28262] DEBUG -- : called #<ZK::EventHandlerSubscription::Base:0x109d6cf40 @mutex=#<ZK::Monitor:0x109d6cea0 @mon_owner=nil, @mon_waiting_queue=[], @mon_entering_queue=[], @mon_count=0>, @path="state_3", @parent=#<ZK::EventHandler:0x109d7b0e0 @thread_opt=:single, @mutex=#<ZK::Monitor:0x109d7abb8 @mon_owner=nil, @mon_waiting_queue=[], @mon_entering_queue=[], @mon_count=0>, @default_watcher_block=#<Proc:0x0000000109f905d8@/Users/Shared/squid/src/github/cluster/zk/lib/zk/event_handler.rb:251>, @orig_pid=28262, @state=:running, @zk=#<ZK::Client::Threaded:2230049920 zk_session_id=0x13bc3f378bd007b ...>, @outstanding_watches={:child=>#<Set: {}>, :data=>#<Set: {}>}, @callbacks={"/_zkelection/foobs/leader_ack"=>[], :all_node_events=>[], "/_zk/groups/foobs"=>[#<ZK::EventHandlerSubscription::Base:0x109d6d5d0 @mutex=#<ZK::Monitor:0x109d6d530 @mon_owner=nil, @mon_waiting_queue=[], @mon_entering_queue=[], @mon_count=0>, @path="/_zk/groups/foobs", @parent=#<ZK::EventHandler:0x109d7b0e0 ...>, @interests=#<Set: {:child}>, @callable=#<Proc:0x0000000109d96868@/Users/Shared/squid/src/github/cluster/zk-group/lib/zk-group/group.rb:86>>], :all_state_events=>[], "state_1"=>[], "state_3"=>[#<ZK::EventHandlerSubscription::Base:0x109d6cf40 ...>], "state_-112"=>[]}>, @interests=#<Set: {:deleted, :changed, :child, :created}>, @callable=#<Proc:0x0000000109d96520@/Users/Shared/squid/src/github/cluster/zk-group/lib/zk-group/group.rb:90>> with [#<Zookeeper::Callbacks::WatcherCallback:0x1098fa188 @path="", @type=-1, @proc=#<Proc:0x00000001098d5630@/Users/robertsanders/.rvm/gems/ree-1.8.7-2012.02@zeevex_cluster/gems/zookeeper-1.4.1/lib/zookeeper/callbacks.rb:24>, @state=3, @zk=#<ZK::Client::Threaded:2230049920 zk_session_id=0x13bc3f378bd007b ...>, @completed=true, @context=nil>] on threadpool
45
+ D, [2012-12-24T14:08:51.772987 #28262] DEBUG -- : last_members: ["m0000000045", "m0000000046", "m0000000047"]
46
+ D, [2012-12-24T14:08:51.773031 #28262] DEBUG -- : @known_members: ["m0000000046", "m0000000047"]
47
+ D, [2012-12-24T14:08:51.774129 #28262] DEBUG -- : ZK: membership change from ZK::Group: from ["blacktip.esquimaux.org:/dev/ttys003", "blacktip.esquimaux.org:/dev/ttys005", "blacktip.esquimaux.org:/dev/ttys006"] to ["blacktip.esquimaux.org:/dev/ttys005", "blacktip.esquimaux.org:/dev/ttys006"]
48
+ D, [2012-12-24T14:08:51.774173 #28262] DEBUG -- : <running hook membership_change([["blacktip.esquimaux.org:/dev/ttys003", "blacktip.esquimaux.org:/dev/ttys005", "blacktip.esquimaux.org:/dev/ttys006"], ["blacktip.esquimaux.org:/dev/ttys005", "blacktip.esquimaux.org:/dev/ttys006"]])>
49
+ D, [2012-12-24T14:08:51.774221 #28262] DEBUG -- : ZeevexCluster::Election observed hook: membership_change [["blacktip.esquimaux.org:/dev/ttys003", "blacktip.esquimaux.org:/dev/ttys005", "blacktip.esquimaux.org:/dev/ttys006"], ["blacktip.esquimaux.org:/dev/ttys005", "blacktip.esquimaux.org:/dev/ttys006"]]
50
+ D, [2012-12-24T14:08:51.774276 #28262] DEBUG -- : <running hook strategy_membership_change([["blacktip.esquimaux.org:/dev/ttys003", "blacktip.esquimaux.org:/dev/ttys005", "blacktip.esquimaux.org:/dev/ttys006"], ["blacktip.esquimaux.org:/dev/ttys005", "blacktip.esquimaux.org:/dev/ttys006"]])>
51
+ D, [2012-12-24T14:08:51.774410 #28262] DEBUG -- : broadcast_membership_change! received event #<Zookeeper::Callbacks::WatcherCallback:0x1098fa188 @path="", @type=-1, @proc=#<Proc:0x00000001098d5630@/Users/robertsanders/.rvm/gems/ree-1.8.7-2012.02@zeevex_cluster/gems/zookeeper-1.4.1/lib/zookeeper/callbacks.rb:24>, @state=3, @zk=#<ZK::Client::Threaded:2230049920 zk_session_id=0x13bc3f378bd007b ...>, @completed=true, @context=nil>
52
+ D, [2012-12-24T14:08:51.775273 #28262] DEBUG -- : last_members: ["m0000000046", "m0000000047"]
53
+ D, [2012-12-24T14:08:51.775313 #28262] DEBUG -- : @known_members: ["m0000000046", "m0000000047"]
54
+ D, [2012-12-24T14:08:51.775342 #28262] DEBUG -- : membership data did not actually change, not notifying
55
+ [9] MASTER[3] pry(main)>
56
+ [10] MASTER[2] pry(main)>
57
+
58
+
59
+ -----------------------------
60
+
data/doc/TODO.txt ADDED
@@ -0,0 +1,85 @@
1
+ Goals:
2
+
3
+ - Stable leader election
4
+ - Membership w/node info
5
+ - Simple configuration management
6
+ - Leases and Locks
7
+ - Message queue (low performance target)
8
+
9
+ ----
10
+
11
+ * Mutex around every method that might have cross-thread access,
12
+ including:
13
+ * coordinator objects
14
+
15
+ * Sort out the strategy thing - is it really useful? Is CAS really the kind of strategy? Can coordinators
16
+ be used for more than one kind of strategy?
17
+
18
+ ** TESTS!
19
+ * CAS should be run against a mock backend
20
+ * Cluster should be run against a mock strategy
21
+
22
+ * Other user code integration, including event runloops perhaps?
23
+
24
+ ** Backends
25
+ * All
26
+ * In membership or cluster info, check for (sem)version compatibility
27
+ * Redis
28
+ * Use queues and PUB/SUB for messaging
29
+ * Use e.g. atomic hashes instead of CAS writes for group membership to scale better?
30
+ * MySQL backend
31
+ * auto-create memory table if it doesn't exist
32
+ * Use MySQL locks somehow to implement non-poll messaging?
33
+ * memcached
34
+ * implement queue-like structure for messaging (using append/prepend, CAS for reads)
35
+ * Easy way to notify a node it should check for various conditions
36
+ * switch to dalli as client
37
+ * disable memcache-client's backoff feature (waits N seconds after
38
+ conn error before allowing next contact)
39
+ * Moneto (ruby key/value store abstraction - needs CAS, though)
40
+ * AR backend
41
+ * MongoDB backend
42
+ - replica sets and/or write concerns for replication / HA?
43
+ - tailable cursors for queues
44
+ * ZK (ruby Zookeeper wrapper) backend - would be very thin, as ZK provides all that this
45
+ lib plans to
46
+ * Local/in-process/filesystem backend for testing
47
+
48
+ * More core functionality / cluster primitives
49
+ * Global Cluster info struct, and version checking
50
+ * Group and single node messaging
51
+ * Leases - can reuse most of leader election code for this; in fact leader election
52
+ can be a wrapper around a lease
53
+ * Locks (auto-released when member exits / times out?)
54
+
55
+ ** General user code API
56
+ * Health check callback into user code - when failed, notify members, resign, and leave
57
+ * Clearer policy for cross-thread callbacks
58
+ * Separate out membership from leader election
59
+ * Cluster configuration file format
60
+
61
+ ** Leader election
62
+ * Replace fixed @stale_time with lease duration specified by candidate
63
+ * Different members will notice master state changes at different times, depending on their polling.
64
+ Should they coordinate or predict when scheduled changes are happening?
65
+ * Callback for no master, and suspect master
66
+
67
+ * Implement the state machines in terms of actual state machine gems instead of spaghetti
68
+
69
+ ** Membership
70
+ * Callbacks into user code for group membership changes - member joined, suspect, left
71
+ * Fast failure of member - attempt to query when suspect
72
+ * Kick live member?
73
+ * After a long partition (including ^Z in which the client isn't running at all), the lib and
74
+ user code should (locally) leave the cluster and rejoin from scratch - otherwise other nodes
75
+ will see it as having left, but it may think it never did
76
+ - Similarly, if it goes to update its membership record and finds it missing, then it should
77
+ consider itself kicked
78
+
79
+
80
+
81
+
82
+ Dreamlist:
83
+
84
+ * Consistent causal ordering of all callbacks / messages on all members?
85
+
@@ -0,0 +1,95 @@
1
+ require 'socket'
2
+ require 'hookem'
3
+
4
+ module ZeevexCluster
5
+ class Base
6
+ include ZeevexCluster::Util::Logging
7
+ include Hookem
8
+
9
+ attr_accessor :nodename, :options
10
+
11
+ def initialize(options = {})
12
+ @options = {:nodename => Socket.gethostname,
13
+ :autojoin => true}.merge(options)
14
+ @logger = @options[:logger]
15
+
16
+ _initialize_hook_module
17
+
18
+ if @options[:hooks]
19
+ add_hooks @options[:hooks]
20
+ end
21
+ end
22
+
23
+ def join
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def leave
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def master?
32
+ raise NotImplementedError
33
+ end
34
+
35
+ def member?
36
+ raise NotImplementedError
37
+ end
38
+
39
+ ##
40
+ ## Make this node the master, returning true if successful.
41
+ ##
42
+ def make_master!
43
+ raise NotImplementedError
44
+ end
45
+
46
+ ##
47
+ ## Make this node the master if not already the master
48
+ ## if provided a block, run that IFF we are the master
49
+ ##
50
+ def ensure_master(&block)
51
+ make_master! unless master?
52
+ if block
53
+ run_if_master &block
54
+ end
55
+ end
56
+
57
+ ##
58
+ ## Run the code block only if this node is the master
59
+ ##
60
+ def run_if_master(&block)
61
+ if master?
62
+ block.call
63
+ else
64
+ false
65
+ end
66
+ end
67
+
68
+ ##
69
+ ## Resign from mastership; returns false if this is the only node.
70
+ ##
71
+ def resign!
72
+ raise NotImplementedError
73
+ end
74
+
75
+ ##
76
+ ## Return name of master node
77
+ ##
78
+ def master
79
+ raise NotImplementedError
80
+ end
81
+
82
+ ##
83
+ ## Return this node's name
84
+ ##
85
+ def nodename
86
+ options[:nodename]
87
+ end
88
+
89
+ protected
90
+
91
+ def after_initialize
92
+ join if options.fetch(:autojoin, true)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,85 @@
1
+ require 'zeevex_cluster/coordinator'
2
+
3
+ class ZeevexCluster::Coordinator::BaseKeyValStore
4
+ include ZeevexCluster::Util::Logging
5
+
6
+ def self.setup
7
+ unless @setup
8
+ require 'memcache'
9
+ require 'zeevex_cluster/serializer/json_hash'
10
+ @setup = true
11
+ end
12
+ end
13
+
14
+ def initialize(options = {})
15
+ self.class.setup
16
+ @options = options
17
+ if (!options[:server] && !options[:client]) || !options[:expiration]
18
+ raise ArgumentError, "Must supply [:server or :client] and :expiration"
19
+ end
20
+ if options[:client]
21
+ @client = options[:client]
22
+ else
23
+ @server = options[:server]
24
+ @port = options[:port] || 11211
25
+ end
26
+ @expiration = options[:expiration] || 60
27
+
28
+ @logger = options[:logger]
29
+
30
+ @serializer = options[:serializer] || ZeevexCluster::Serializer::JsonHash.new
31
+
32
+ @retries = options.fetch(:retries, 20)
33
+ @retry_wait = options.fetch(:retry_wait, 2)
34
+ @retry_bo = options.fetch(:retry_bo, 1.5)
35
+ end
36
+
37
+ [:add, :set, :cas, :get].each do |name|
38
+ define_method "#{name}_with_retry", lambda { |*args, &block|
39
+ with_connection_retry name, {}, *args, &block
40
+ }
41
+ end
42
+
43
+ def with_connection_retry(method, options = {}, *args, &block)
44
+ retry_left = options.fetch(:retries, @retries)
45
+ retry_wait = options.fetch(:retry_wait, @retry_wait)
46
+ begin
47
+ send "do_#{method}", *args, &block
48
+ rescue ZeevexCluster::Coordinator::ConnectionError
49
+ if retry_left > 0
50
+ logger.debug "retrying after #{retry_wait} seconds"
51
+ retry_left -= 1
52
+ sleep retry_wait
53
+ retry_wait = retry_wait * options.fetch('retry_bo', @retry_bo)
54
+ retry
55
+ else
56
+ logger.error 'Ran out of connection retries, re-raising'
57
+ raise
58
+ end
59
+ end
60
+ end
61
+
62
+ protected
63
+
64
+ def serialize_value(obj, raw = false)
65
+ raw ? obj : @serializer.serialize(obj)
66
+ end
67
+
68
+ def deserialize_value(str, raw = false)
69
+ raw ? str : @serializer.deserialize(str)
70
+ end
71
+
72
+ def is_raw?(options)
73
+ (@options && @options[:raw]) || (options && options[:raw])
74
+ end
75
+
76
+ def to_key(key)
77
+ if @options[:namespace]
78
+ "#{@options[:namespace]}:#{key}"
79
+ elsif @options[:to_key_proc]
80
+ @options[:to_key_proc].call(key)
81
+ else
82
+ key.to_s
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,118 @@
1
+ require 'zeevex_cluster/coordinator/base_key_val_store'
2
+
3
+ module ZeevexCluster::Coordinator
4
+ class Memcached < BaseKeyValStore
5
+ def self.setup
6
+ unless @setup
7
+ require 'memcache'
8
+ BaseKeyValStore.setup
9
+
10
+ @setup = true
11
+ end
12
+ end
13
+
14
+ def initialize(options = {})
15
+ super
16
+ @client ||= MemCache.new "#@server:#@port"
17
+ end
18
+
19
+ def add(key, value, options = {})
20
+ status( @client.add(to_key(key), serialize_value(value, options[:raw]),
21
+ options.fetch(:expiration, @expiration), raw?) ) == STORED
22
+ rescue MemCache::MemCacheError
23
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
24
+ end
25
+
26
+ def set(key, value, options = {})
27
+ status( @client.set(to_key(key), serialize_value(value, options[:raw]),
28
+ options.fetch(:expiration, @expiration), raw?) ) == STORED
29
+ rescue MemCache::MemCacheError
30
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
31
+ end
32
+
33
+ def delete(key, options = {})
34
+ status( @client.delete(to_key(key)) ) == DELETED
35
+ end
36
+
37
+ #
38
+ # Block is passed the current value, and returns the updated value.
39
+ #
40
+ # Block can raise DontChange to simply exit the block without updating.
41
+ #
42
+ # returns nil for no value
43
+ # returns false for failure (somebody else set)
44
+ # returns true for success
45
+ #
46
+ def cas(key, options = {}, &block)
47
+ res = @client.cas(to_key(key), options.fetch(:expiration, @expiration), raw?) do |inval|
48
+ serialize_value(yield(deserialize_value(inval, options[:raw])), options[:raw])
49
+ end
50
+ case status(res)
51
+ when nil then nil
52
+ when EXISTS then false
53
+ when STORED then true
54
+ else raise "Unhandled status code: #{res}"
55
+ end
56
+ rescue ZeevexCluster::Coordinator::DontChange
57
+ false
58
+ rescue MemCache::MemCacheError
59
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
60
+ end
61
+
62
+ def get(key, options = {})
63
+ val = @client.get(to_key(key), raw?)
64
+ if val && !options[:raw]
65
+ deserialize_value(val)
66
+ else
67
+ val
68
+ end
69
+ rescue MemCache::MemCacheError
70
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
71
+ end
72
+
73
+ def append(key, val, options = {})
74
+ val = serialize_value(val, options[:raw])
75
+ key = to_key(key)
76
+ status( @client.append(key, val) ) == STORED ||
77
+ status( @client.add(key, val, options.fetch(:expiration, @expiration), true) ) == STORED ||
78
+ status( @client.append(key, val) ) == STORED
79
+ rescue MemCache::MemCacheError
80
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
81
+ end
82
+
83
+ def prepend(key, val, options = {})
84
+ val = serialize_value(val, options[:raw])
85
+ key = to_key(key)
86
+ status( @client.prepend(key, val) ) == STORED ||
87
+ status( @client.add(key, val, options.fetch(:expiration, @expiration), true) ) == STORED ||
88
+ status( @client.prepend(key, val) ) == STORED
89
+ rescue MemCache::MemCacheError
90
+ raise ZeevexCluster::Coordinator::ConnectionError.new 'Connection error', $!
91
+ end
92
+
93
+ def push_to_queue(key, object, options = {})
94
+
95
+ end
96
+
97
+ protected
98
+
99
+ STORED = 'STORED'
100
+ EXISTS = 'EXISTS'
101
+ NOT_STORED = 'NOT_STORED'
102
+ NOT_FOUND = 'NOT_FOUND'
103
+ DELETED = 'DELETED'
104
+
105
+ def status(response)
106
+ case response
107
+ when nil, true, false then response
108
+ when String then response.chomp
109
+ else
110
+ raise ArgumentError, "This should only be called on results from cas, add, set, etc. - got result #{response.inspect}"
111
+ end
112
+ end
113
+
114
+ def raw?
115
+ true
116
+ end
117
+ end
118
+ end