celluloid-presence 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 83c3287a94c2f820bab2be50041e10183562379d
4
+ data.tar.gz: 5e94a2416dcaca6535a64291eb525b5117a8d338
5
+ SHA512:
6
+ metadata.gz: c90b5508fc1281b5f3713b87c9a980b89e9e46728db5efdd53ca782fe10393c560d78901a0b0707fcb154d6eb0320b314f4e84edaf81790e8296583210821eff
7
+ data.tar.gz: 979c29216afcd3a3d958f8ee6625cfb91e7bcedb47d45d75ffad5675e0b2de0a9a9ff82d9091552c1e14ebe129bc1e51917dd6fe70f0d10a7aa8d514029356bb
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,51 @@
1
+ h1. Celluloid Presence
2
+
3
+ Start an instance of "ZooKeeper":http://zookeeper.apache.org/ on your server then run:
4
+ * OSX:
5
+ *
6
+
7
+ <pre><code class="ruby">
8
+ require 'rubygems'
9
+ require 'celluloid-presence'
10
+
11
+ Celluloid::Presence::ZkService.init :server => 'localhost'
12
+ Celluloid::Presence::ZkPresence.supervise_as :presence, :service_name => :presence
13
+
14
+ class Messaging
15
+ include Celluloid
16
+ include Celluloid::Notifications
17
+
18
+ def self.finalizer
19
+ :finalize
20
+ end
21
+
22
+ def initialize(service_name)
23
+ @service_name = service_name
24
+ subscribe("#{@service_name}_nodes", :node_update)
25
+ end
26
+
27
+ def node_update(event, nodes)
28
+ p "\nNode list updated!"
29
+ nodes.each do |node|
30
+ p " -> #{Actor[@service_name].get(node)}"
31
+ end
32
+ end
33
+
34
+
35
+ private
36
+
37
+
38
+ def finalize
39
+ Actor[@service_name].terminate
40
+ end
41
+ end
42
+
43
+ Messaging.run(:presence)
44
+ </code></pre>
45
+
46
+
47
+ h2. Start using it now
48
+
49
+ # Read the "Documentation":http://rubydoc.info/gems/celluloid-promise/Celluloid/Presence
50
+ # Then @gem install celluloid-presence@
51
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc "Run all RSpec tests"
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task :default => :spec
10
+ task :test => [:spec]
@@ -0,0 +1,162 @@
1
+ require 'socket'
2
+
3
+ module Celluloid
4
+ module Presence
5
+ class ZkPresence
6
+ include Celluloid
7
+ include Celluloid::Notifications
8
+
9
+ PREFIX ||= '/coPresence'
10
+ NODE_PREFIX ||= 'node-'
11
+
12
+
13
+ def self.finalizer
14
+ :finalize
15
+ end
16
+
17
+
18
+ def initialize(options = {})
19
+ @env = options[:env] || 'production'
20
+ @base_path = options[:service_name].nil? ? "#{PREFIX}/#{@env}/default" : "#{PREFIX}/#{@env}/#{options[:service_name]}"
21
+ @base_path_sym = @base_path.to_sym
22
+ @node_path = nil # This is officially set in connect_setup
23
+ @service_name = options[:service_name] || :default
24
+ @nodes = []
25
+
26
+ if options[:node_address].nil?
27
+ use_ip_v4 = !options[:ip_v6] # IP v6 or IP v4 if using the default node data
28
+ @node_address = proc { ip_address(use_ip_v4) }
29
+ else
30
+ node_address = options[:node_address]
31
+ @node_address = proc { node_address.is_a?(Proc) ? node_address.call : node_address }
32
+ end
33
+ @last_known_address = nil
34
+
35
+ #
36
+ # These callbacks will be executed on a seperate thread
37
+ # Which is why we need to use notifications update state
38
+ #
39
+ zk.register(@base_path)
40
+ subscribe("zk_event_#{@base_path}", :zk_callback)
41
+ subscribe('zk_connected', :on_connected)
42
+ subscribe('zk_connecting', :on_connecting)
43
+
44
+ on_connected if zk.connected?
45
+ end
46
+
47
+
48
+ #
49
+ # Provides the list of known nodes
50
+ #
51
+ attr_reader :nodes
52
+
53
+
54
+ #
55
+ # This informs us of changes to the list of base path's children
56
+ #
57
+ def zk_callback(event_name, event)
58
+ #p "EVENT OCUURED #{event.event_name} #{event.path}"
59
+ if event.node_child? # We are only listening to child events so we only need to handle these
60
+ begin
61
+ @nodes = zk.children(@base_path, :watch => true) # re-set watch
62
+ update_node_information
63
+ rescue ZK::Exceptions::NoNode # Technically this shouldn't happen unless someone deleted our base path
64
+ on_connected if zk.connected?
65
+ end
66
+ end
67
+ end
68
+
69
+ #
70
+ # This informs us of a new connection being established with zookeeper
71
+ #
72
+ def on_connected(event = nil)
73
+ address = @node_address.call
74
+ if @node_path.nil? or not zk.exists?(@node_path) # Create presence node as it doesn't exist
75
+ #p 'node re-created'
76
+ @last_known_address = address
77
+ connect_setup
78
+ elsif @last_known_address != address # Recreate existing presence node as our IP changed
79
+ #p 'node ip_changed, recreating'
80
+ zk.async.ensure(:delete, @node_path)
81
+ @last_known_address = address
82
+ connect_setup
83
+ else # Else our node presence information is accurate, lets get the latest list
84
+ @nodes = zk.children(@base_path, :watch => true)
85
+ end
86
+
87
+ update_node_information # inform listeners of a node list update
88
+ end
89
+
90
+ #
91
+ # This informs us that we've been disconnected from zookeeper
92
+ #
93
+ def on_connecting event
94
+ publish("#{@service_name}_nodes_stale")
95
+ end
96
+
97
+ #
98
+ # Returns the value of a node
99
+ #
100
+ def get(node_id)
101
+ result, _ = zk.get("#{@base_path}/#{node_id}")
102
+ result
103
+ rescue ZK::Exceptions::NoNode
104
+ end
105
+
106
+ #
107
+ # Returns the name of this node
108
+ #
109
+ def name
110
+ @node_path.split('/')[-1]
111
+ end
112
+
113
+
114
+ private
115
+
116
+
117
+ #
118
+ # Shortcut to the zookeeper service
119
+ #
120
+ def zk
121
+ Actor[:zk_service]
122
+ end
123
+
124
+ #
125
+ # creates the presence node
126
+ #
127
+ def connect_setup
128
+ zk.mkdir_p @base_path
129
+ @node_path = zk.create "#{@base_path}/#{NODE_PREFIX}", @last_known_address, :sequence => true, :ephemeral => true
130
+ @nodes = zk.children @base_path, :watch => true
131
+ end
132
+
133
+ #
134
+ # Pushes an updated node list to subscribers
135
+ #
136
+ def update_node_information
137
+ publish("#{@service_name}_nodes", @nodes)
138
+ end
139
+
140
+ #
141
+ # Grabs a valid IP address
142
+ #
143
+ def ip_address(ip_v4)
144
+ ip = if ip_v4
145
+ Socket.ip_address_list.detect {|a| a.ipv4? && !a.ipv4_loopback?}
146
+ else
147
+ Socket.ip_address_list.detect {|a| !a.ipv4? && !(a.ipv6_linklocal? || a.ipv6_loopback?)}
148
+ end
149
+ return ip.ip_address unless ip.nil?
150
+ nil
151
+ end
152
+
153
+ #
154
+ # On termination make sure zookeeper is updated
155
+ #
156
+ def finalize
157
+ zk.async.unregister(@base_path)
158
+ zk.async.ensure(:delete, @node_path) unless @node_path.nil?
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,160 @@
1
+ require 'zk'
2
+
3
+ module Celluloid
4
+ module Presence
5
+ class ZkService
6
+ include Celluloid
7
+ include Celluloid::Notifications
8
+
9
+ DEFAULT_PORT ||= 2181
10
+ @@zk ||= nil # The single zookeeper instance
11
+ @@registry ||= {} # Paths we are interested in receiving events from
12
+ @@registry_count ||= {} # Listener counts (for unregistering)
13
+ @@ensure ||= [] # Tasks we want to ensure are completed (even when disconnected)
14
+ @@connected ||= false # We'll maintain our own connected state
15
+
16
+
17
+ #
18
+ # Optional init function to supervise this actor
19
+ #
20
+ def self.init(options)
21
+ ZkService.supervise_as :zk_service, options
22
+ end
23
+
24
+ # Create a single connection to Zookeeper that persists through Actor crashes
25
+ #
26
+ # servers: a list of Zookeeper servers to connect to. Each server in the
27
+ # list has a host/port configuration
28
+ def initialize(options)
29
+ @@zk ||= begin
30
+ Fanout.supervise_as :notifications_fanout if Actor[:notifications_fanout].nil?
31
+
32
+ # Let them specify a single server instead of many
33
+ servers = options[:server].nil? ? options[:servers] : [options[:server]]
34
+ raise "no Zookeeper servers given" unless servers
35
+
36
+ # Add the default Zookeeper port unless specified
37
+ servers.map! do |server|
38
+ if server[/:\d+$/]
39
+ server
40
+ else
41
+ "#{server}:#{DEFAULT_PORT}"
42
+ end
43
+ end
44
+
45
+ ZK.new(*servers) do |zk|
46
+ zk.on_connecting &proc { Actor[:zk_service].async.on_connecting }
47
+ zk.on_connected &proc { Actor[:zk_service].async.on_connected }
48
+ end
49
+ end.tap do |zk|
50
+ @@connected = zk.connected?
51
+ end
52
+ end
53
+
54
+ #
55
+ # Called once a connection is made to the zookeeper cluster
56
+ # Runs any impending ensures before informing any subscribed actors
57
+ #
58
+ def on_connected
59
+ @@connected = true
60
+ while not @@ensure.empty?
61
+ func = @@ensure.shift
62
+ ensured func[0], *func[1] # func[0] == function name, func[1] == arguments
63
+ end
64
+ publish('zk_connected')
65
+ end
66
+
67
+ #
68
+ # Called on disconnect from zookeeper
69
+ # Might be that the zookeeper node crashed or zookeeper is down, may not effect any other connections
70
+ #
71
+ def on_connecting
72
+ @@connected = false
73
+ publish('zk_connecting')
74
+ end
75
+
76
+ #
77
+ # Publishes any zookeeper events that have been registered
78
+ #
79
+ def event_callback(path, event)
80
+ publish("zk_event_#{path}", event)
81
+ end
82
+
83
+ #
84
+ # Uses a closure to cleanly pull the zookeeper event back into the protection of celluloid
85
+ #
86
+ def register(path)
87
+ path = path.to_sym
88
+ if @@registry[path].nil?
89
+ callback = proc { |event| Actor[:zk_service].async.event_callback(path, event) }
90
+ @@registry[path] = @@zk.register(path.to_s, &callback)
91
+ end
92
+ @@registry_count[path] = (@@registry_count[path] || 0) + 1
93
+ end
94
+
95
+ #
96
+ # unsubscribes from zookeeper events once there are no more listeners
97
+ #
98
+ def unregister(path)
99
+ path = path.to_sym
100
+ if @@registry[path]
101
+ @@registry_count[path] -= 1
102
+ if @@registry_count[path] <= 0
103
+ sub = @@registry.delete(path)
104
+ sub.unsubscribe
105
+ @@registry_count.delete(path)
106
+ end
107
+ end
108
+ end
109
+
110
+ #
111
+ # Our abstracted connected status to avoid race conditions
112
+ #
113
+ def connected?
114
+ @@connected
115
+ end
116
+
117
+ #
118
+ # Ensures important requests are followed through in the case of disconnect / reconnect
119
+ # Ignores request if the node does not exist
120
+ # USE SPARINGLY
121
+ #
122
+ def ensure(func, *args)
123
+ if connected?
124
+ ensured(func, *args)
125
+ else
126
+ @@ensure.push [func, args]
127
+ end
128
+ end
129
+
130
+
131
+ #
132
+ # Proxy the following functions
133
+ #
134
+ # Automatically creates a callable function for each command
135
+ # http://blog.jayfields.com/2007/10/ruby-defining-class-methods.html
136
+ # http://blog.jayfields.com/2008/02/ruby-dynamically-define-method.html
137
+ #
138
+ SAFE_OPS ||= [:get, :children, :create, :mkdir_p, :delete, :stat, :exists?, :set, :rm_rf]
139
+ SAFE_OPS.each do |func|
140
+ define_method func do |*args|
141
+ begin
142
+ @@zk.send func, *args if connected?
143
+ rescue Zookeeper::Exceptions::NotConnected
144
+ end
145
+ end
146
+ end
147
+
148
+
149
+ private
150
+
151
+
152
+ def ensured(func, *args)
153
+ @@zk.send func, *args
154
+ rescue Zookeeper::Exceptions::NotConnected
155
+ @@ensure << [func, args]
156
+ rescue ZK::Exceptions::NoNode
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,6 @@
1
+ module Celluloid
2
+ module Presence
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
6
+
@@ -0,0 +1,3 @@
1
+ require 'celluloid'
2
+ require 'celluloid-presence/service.rb'
3
+ require 'celluloid-presence/presence.rb'
@@ -0,0 +1,152 @@
1
+ require 'celluloid-presence'
2
+
3
+
4
+ module DCell
5
+ module Registry
6
+ class ZkPresenceAdapter
7
+ def initialize(options)
8
+ Celluloid::Presence::ZkService.init options
9
+
10
+ @node_registry = NodeRegistry.new(options)
11
+ @global_registry = GlobalRegistry.new(:zk_service, options)
12
+ end
13
+
14
+
15
+ class NodeRegistry
16
+ include Celluloid
17
+ include Celluloid::Notifications
18
+
19
+ def initialize(options)
20
+ @mutex = Mutex.new
21
+ @current_address = nil
22
+
23
+ Celluloid::Presence::ZkPresence.supervise_as :dcell_presence, {
24
+ :service_name => :dcell_presence,
25
+ :node_address => proc { @mutex.synchronize { @current_address } },
26
+ :env => options[:env] || 'production'
27
+ }
28
+
29
+ @service_name = :dcell_presence
30
+ @directory = {} # node_name => address
31
+ @mapping = {} # zk_path => node_name
32
+
33
+ subscribe("#{@service_name}_nodes", :node_update)
34
+
35
+ @nodes = []
36
+ update_directory(presence.nodes)
37
+ end
38
+
39
+ def get(node_id)
40
+ @directory[node_id]
41
+ end
42
+
43
+ def nodes
44
+ @directory.keys
45
+ end
46
+
47
+ def node_update(event, nodes)
48
+ update_directory(nodes)
49
+ end
50
+
51
+ def set(node_id, addr)
52
+ @mutex.synchronize {
53
+ @current_address = Marshal.dump [node_id, addr]
54
+ }
55
+ presence.on_connected # This causes an address update on zookeeper
56
+ end
57
+
58
+
59
+ private
60
+
61
+
62
+ def update_directory(new_list)
63
+ unsubscribe = @nodes - new_list # TODO:: use sets here instead of arrays for speed
64
+ subscribe = new_list - @nodes
65
+
66
+ unsubscribe.each do |node|
67
+ node_name = @mapping.delete(node)
68
+ @directory.delete(node_name)
69
+ end
70
+
71
+ subscribe.each do |node|
72
+ value = presence.get(node)
73
+ if value.nil?
74
+ @nodes.delete value
75
+ else
76
+ begin
77
+ info = Marshal.load value
78
+ @mapping[node] = info[0]
79
+ @directory[info[0]] = info[1]
80
+ rescue
81
+ @nodes.delete value
82
+ end
83
+ end
84
+ end
85
+
86
+ @nodes = new_list
87
+ end
88
+
89
+ def presence
90
+ ::Celluloid::Actor[@service_name]
91
+ end
92
+ end
93
+
94
+ def clear_nodes; end # This is now self maintaining hence no op
95
+ def get_node(node_id); @node_registry.get(node_id) end
96
+ def nodes; @node_registry.nodes end
97
+ def set_node(node_id, addr); @node_registry.set(node_id, addr) end
98
+
99
+
100
+
101
+ class GlobalRegistry
102
+ PREFIX = "/dcell_global"
103
+
104
+ def initialize(service_name, options)
105
+ options = options.inject({}) { |h,(k,v)| h[k.to_s] = v; h }
106
+
107
+ @service_name = service_name
108
+ @env = options['env'] || 'production'
109
+ @base_path = "#{PREFIX}/#{@env}"
110
+ end
111
+
112
+ def get(key)
113
+ value, _ = zk.get "#{@base_path}/#{key}"
114
+ Marshal.load value
115
+ rescue ZK::Exceptions::NoNode
116
+ end
117
+
118
+ # Set a global value
119
+ def set(key, value)
120
+ path = "#{@base_path}/#{key}"
121
+ string = Marshal.dump value
122
+
123
+ zk.set path, string
124
+ rescue ZK::Exceptions::NoNode
125
+ zk.create path, string
126
+ end
127
+
128
+ # The keys to all globals in the system
129
+ def global_keys
130
+ zk.children(@base_path)
131
+ end
132
+
133
+ def clear
134
+ zk.rm_rf @base_path
135
+ zk.mkdir_p @base_path
136
+ end
137
+
138
+ private
139
+
140
+ def zk
141
+ Celluloid::Actor[@service_name]
142
+ end
143
+ end
144
+
145
+
146
+ def clear_globals; @global_registry.clear end
147
+ def get_global(key); @global_registry.get(key) end
148
+ def set_global(key, value); @global_registry.set(key, value) end
149
+ def global_keys; @global_registry.global_keys end
150
+ end
151
+ end
152
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: celluloid-presence
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephen von Takach
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-03-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: celluloid
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Node presence using ZooKeeper for celluloid services
42
+ email:
43
+ - steve@cotag.me
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/celluloid-presence/presence.rb
49
+ - lib/celluloid-presence/service.rb
50
+ - lib/celluloid-presence/version.rb
51
+ - lib/celluloid-presence.rb
52
+ - lib/dcell/registries/zk_presence_adapter.rb
53
+ - MIT-LICENSE
54
+ - Rakefile
55
+ - README.textile
56
+ homepage: https://github.com/cotag/celluloid-presence
57
+ licenses: []
58
+ metadata: {}
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubyforge_project:
75
+ rubygems_version: 2.0.0
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: Node presence using ZooKeeper for celluloid services
79
+ test_files: []