celluloid-presence 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []