dcell 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/.rspec +4 -0
- data/CHANGES.md +8 -0
- data/Gemfile +2 -0
- data/README.md +209 -0
- data/Rakefile +3 -0
- data/benchmarks/messaging.rb +67 -0
- data/benchmarks/receiver.rb +37 -0
- data/dcell.gemspec +10 -3
- data/lib/celluloid/README +8 -0
- data/lib/celluloid/zmq.rb +28 -0
- data/lib/celluloid/zmq/mailbox.rb +13 -0
- data/lib/celluloid/zmq/reactor.rb +74 -0
- data/lib/dcell.rb +92 -2
- data/lib/dcell/actor_proxy.rb +4 -0
- data/lib/dcell/application.rb +6 -0
- data/lib/dcell/celluloid_ext.rb +57 -0
- data/lib/dcell/directory.rb +23 -0
- data/lib/dcell/global.rb +23 -0
- data/lib/dcell/mailbox_proxy.rb +61 -0
- data/lib/dcell/messages.rb +67 -0
- data/lib/dcell/node.rb +120 -0
- data/lib/dcell/registries/redis_adapter.rb +86 -0
- data/lib/dcell/registries/zk_adapter.rb +122 -0
- data/lib/dcell/responses.rb +16 -0
- data/lib/dcell/router.rb +71 -0
- data/lib/dcell/rspec.rb +1 -0
- data/lib/dcell/server.rb +80 -0
- data/lib/dcell/version.rb +1 -1
- data/spec/celluloid/zmq/mailbox_spec.rb +6 -0
- data/spec/dcell/actor_proxy_spec.rb +60 -0
- data/spec/dcell/celluloid_ext_spec.rb +21 -0
- data/spec/dcell/directory_spec.rb +8 -0
- data/spec/dcell/global_spec.rb +21 -0
- data/spec/dcell/node_spec.rb +23 -0
- data/spec/dcell/registries/redis_adapter_spec.rb +6 -0
- data/spec/dcell/registries/zk_adapter_spec.rb +11 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/helpers.rb +40 -0
- data/spec/support/registry_examples.rb +35 -0
- data/spec/test_node.rb +33 -0
- data/tasks/rspec.task +7 -0
- data/tasks/zookeeper.task +58 -0
- metadata +111 -7
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'redis-namespace'
|
3
|
+
|
4
|
+
module DCell
|
5
|
+
module Registry
|
6
|
+
class RedisAdapter
|
7
|
+
def initialize(options)
|
8
|
+
# Convert all options to symbols :/
|
9
|
+
options = options.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
|
10
|
+
|
11
|
+
@env = options[:env] || 'production'
|
12
|
+
@namespace = options[:namespace] || "dcell_#{@env}"
|
13
|
+
|
14
|
+
redis = Redis.new options
|
15
|
+
@redis = Redis::Namespace.new @namespace, :redis => redis
|
16
|
+
|
17
|
+
@node_registry = NodeRegistry.new(@redis)
|
18
|
+
@global_registry = GlobalRegistry.new(@redis)
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear_nodes
|
22
|
+
@node_registry.clear
|
23
|
+
end
|
24
|
+
|
25
|
+
def clear_globals
|
26
|
+
@global_registry.clear
|
27
|
+
end
|
28
|
+
|
29
|
+
class NodeRegistry
|
30
|
+
def initialize(redis)
|
31
|
+
@redis = redis
|
32
|
+
end
|
33
|
+
|
34
|
+
def get(node_id)
|
35
|
+
@redis.hget 'nodes', node_id
|
36
|
+
end
|
37
|
+
|
38
|
+
def set(node_id, addr)
|
39
|
+
@redis.hset 'nodes', node_id, addr
|
40
|
+
end
|
41
|
+
|
42
|
+
def nodes
|
43
|
+
@redis.hkeys 'nodes'
|
44
|
+
end
|
45
|
+
|
46
|
+
def clear
|
47
|
+
@redis.del 'nodes'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_node(node_id); @node_registry.get(node_id) end
|
52
|
+
def set_node(node_id, addr); @node_registry.set(node_id, addr) end
|
53
|
+
def nodes; @node_registry.nodes end
|
54
|
+
|
55
|
+
class GlobalRegistry
|
56
|
+
def initialize(redis)
|
57
|
+
@redis = redis
|
58
|
+
end
|
59
|
+
|
60
|
+
def get(key)
|
61
|
+
string = @redis.hget 'globals', key.to_s
|
62
|
+
Marshal.load string
|
63
|
+
end
|
64
|
+
|
65
|
+
# Set a global value
|
66
|
+
def set(key, value)
|
67
|
+
string = Marshal.dump value
|
68
|
+
@redis.hset 'globals', key.to_s, string
|
69
|
+
end
|
70
|
+
|
71
|
+
# The keys to all globals in the system
|
72
|
+
def global_keys
|
73
|
+
@redis.hkeys 'globals'
|
74
|
+
end
|
75
|
+
|
76
|
+
def clear
|
77
|
+
@redis.del 'globals'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_global(key); @global_registry.get(key) end
|
82
|
+
def set_global(key, value); @global_registry.set(key, value) end
|
83
|
+
def global_keys; @global_registry.global_keys end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'zk'
|
2
|
+
|
3
|
+
module DCell
|
4
|
+
module Registry
|
5
|
+
class ZkAdapter
|
6
|
+
PREFIX = "/dcell"
|
7
|
+
DEFAULT_PORT = 2181
|
8
|
+
|
9
|
+
# Create a new connection to Zookeeper
|
10
|
+
#
|
11
|
+
# servers: a list of Zookeeper servers to connect to. Each server in the
|
12
|
+
# list has a host/port configuration
|
13
|
+
def initialize(options)
|
14
|
+
# Stringify keys :/
|
15
|
+
options = options.inject({}) { |h,(k,v)| h[k.to_s] = v; h }
|
16
|
+
|
17
|
+
@env = options['env'] || 'production'
|
18
|
+
@base_path = "#{PREFIX}/#{@env}"
|
19
|
+
|
20
|
+
# Let them specify a single server instead of many
|
21
|
+
server = options['server']
|
22
|
+
if server
|
23
|
+
servers = [server]
|
24
|
+
else
|
25
|
+
servers = options['servers']
|
26
|
+
raise "no Zookeeper servers given" unless servers
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add the default Zookeeper port unless specified
|
30
|
+
servers.map! do |server|
|
31
|
+
if server[/:\d+$/]
|
32
|
+
server
|
33
|
+
else
|
34
|
+
"#{server}:#{DEFAULT_PORT}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
@zk = ZK.new(*servers)
|
39
|
+
@node_registry = NodeRegistry.new(@zk, @base_path)
|
40
|
+
@global_registry = GlobalRegistry.new(@zk, @base_path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def clear_nodes
|
44
|
+
@node_registry.clear
|
45
|
+
end
|
46
|
+
|
47
|
+
def clear_globals
|
48
|
+
@global_registry.clear
|
49
|
+
end
|
50
|
+
|
51
|
+
class NodeRegistry
|
52
|
+
def initialize(zk, base_path)
|
53
|
+
@zk, @base_path = zk, "#{base_path}/nodes"
|
54
|
+
@zk.mkdir_p @base_path
|
55
|
+
end
|
56
|
+
|
57
|
+
def get(node_id)
|
58
|
+
result, _ = @zk.get("#{@base_path}/#{node_id}")
|
59
|
+
result
|
60
|
+
rescue ZK::Exceptions::NoNode
|
61
|
+
end
|
62
|
+
|
63
|
+
def set(node_id, addr)
|
64
|
+
path = "#{@base_path}/#{node_id}"
|
65
|
+
@zk.set path, addr
|
66
|
+
rescue ZK::Exceptions::NoNode
|
67
|
+
@zk.create path, addr
|
68
|
+
end
|
69
|
+
|
70
|
+
def nodes
|
71
|
+
@zk.children @base_path
|
72
|
+
end
|
73
|
+
|
74
|
+
def clear
|
75
|
+
@zk.rm_rf @base_path
|
76
|
+
@zk.mkdir_p @base_path
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def get_node(node_id); @node_registry.get(node_id) end
|
81
|
+
def set_node(node_id, addr); @node_registry.set(node_id, addr) end
|
82
|
+
def nodes; @node_registry.nodes end
|
83
|
+
|
84
|
+
class GlobalRegistry
|
85
|
+
def initialize(zk, base_path)
|
86
|
+
@zk, @base_path = zk, "#{base_path}/globals"
|
87
|
+
@zk.mkdir_p @base_path
|
88
|
+
end
|
89
|
+
|
90
|
+
def get(key)
|
91
|
+
value, _ = @zk.get "#{@base_path}/#{key}"
|
92
|
+
Marshal.load value
|
93
|
+
rescue ZK::Exceptions::NoNode
|
94
|
+
end
|
95
|
+
|
96
|
+
# Set a global value
|
97
|
+
def set(key, value)
|
98
|
+
path = "#{@base_path}/#{key}"
|
99
|
+
string = Marshal.dump value
|
100
|
+
|
101
|
+
@zk.set path, string
|
102
|
+
rescue ZK::Exceptions::NoNode
|
103
|
+
@zk.create path, string
|
104
|
+
end
|
105
|
+
|
106
|
+
# The keys to all globals in the system
|
107
|
+
def global_keys
|
108
|
+
@zk.children(@base_path)
|
109
|
+
end
|
110
|
+
|
111
|
+
def clear
|
112
|
+
@zk.rm_rf @base_path
|
113
|
+
@zk.mkdir_p @base_path
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def get_global(key); @global_registry.get(key) end
|
118
|
+
def set_global(key, value); @global_registry.set(key, value) end
|
119
|
+
def global_keys; @global_registry.global_keys end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module DCell
|
2
|
+
# Responses to calls
|
3
|
+
class Response
|
4
|
+
attr_reader :request_id, :value
|
5
|
+
|
6
|
+
def initialize(request_id, value)
|
7
|
+
@request_id, @value = request_id, value
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Request successful
|
12
|
+
class SuccessResponse < Response; end
|
13
|
+
|
14
|
+
# Request failed
|
15
|
+
class ErrorResponse < Response; end
|
16
|
+
end
|
data/lib/dcell/router.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'weakref'
|
2
|
+
|
3
|
+
module DCell
|
4
|
+
# Route incoming messages to their recipient actors
|
5
|
+
class Router
|
6
|
+
@lock = Mutex.new
|
7
|
+
@table = {}
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# Enter a mailbox into the registry
|
11
|
+
def register(mailbox)
|
12
|
+
id = mailbox.object_id.to_s(16)
|
13
|
+
|
14
|
+
@lock.synchronize do
|
15
|
+
ref = @table[id]
|
16
|
+
unless ref && ref.weakref_alive?
|
17
|
+
@table[id] = WeakRef.new(mailbox)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
id
|
22
|
+
end
|
23
|
+
|
24
|
+
# Find a mailbox by its ID
|
25
|
+
def find(mailbox_id)
|
26
|
+
@lock.synchronize do
|
27
|
+
ref = @table[mailbox_id]
|
28
|
+
return unless ref
|
29
|
+
begin
|
30
|
+
ref.__getobj__
|
31
|
+
rescue WeakRef::RefError
|
32
|
+
# The referenced actor is dead, so prune the registry
|
33
|
+
@table.delete mailbox_id
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Route a message to a given mailbox ID
|
40
|
+
def route(mailbox_id, message)
|
41
|
+
recipient = find mailbox_id
|
42
|
+
|
43
|
+
if recipient
|
44
|
+
recipient << message
|
45
|
+
else
|
46
|
+
Celluloid::Logger.debug("received message for invalid actor: #{mailbox_id.inspect}")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Route a system event to a given mailbox ID
|
51
|
+
def route_system_event(mailbox_id, event)
|
52
|
+
recipient = find mailbox_id
|
53
|
+
|
54
|
+
if recipient
|
55
|
+
recipient.system_event event
|
56
|
+
else
|
57
|
+
Celluloid::Logger.debug("received message for invalid actor: #{mailbox_id.inspect}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Prune all entries that point to dead objects
|
62
|
+
def gc
|
63
|
+
@lock.synchronize do
|
64
|
+
@table.each do |id, ref|
|
65
|
+
@table.delete id unless ref.weakref_alive?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/dcell/rspec.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path('../../../spec/support/registry_examples', __FILE__)
|
data/lib/dcell/server.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module DCell
|
2
|
+
# Servers handle incoming 0MQ traffic
|
3
|
+
class Server
|
4
|
+
include Celluloid::ZMQ
|
5
|
+
|
6
|
+
# Bind to the given 0MQ address (in URL form ala tcp://host:port)
|
7
|
+
def initialize
|
8
|
+
@addr = DCell.addr
|
9
|
+
@socket = DCell.zmq_context.socket(::ZMQ::PULL)
|
10
|
+
|
11
|
+
unless ::ZMQ::Util.resultcode_ok? @socket.setsockopt(::ZMQ::LINGER, 0)
|
12
|
+
@socket.close
|
13
|
+
raise "couldn't set ZMQ::LINGER: #{::ZMQ::Util.error_string}"
|
14
|
+
end
|
15
|
+
|
16
|
+
unless ::ZMQ::Util.resultcode_ok? @socket.bind(@addr)
|
17
|
+
@socket.close
|
18
|
+
raise "couldn't bind to #{@addr}: #{::ZMQ::Util.error_string}"
|
19
|
+
end
|
20
|
+
|
21
|
+
run!
|
22
|
+
end
|
23
|
+
|
24
|
+
# Wait for incoming 0MQ messages
|
25
|
+
def run
|
26
|
+
while true
|
27
|
+
wait_readable @socket
|
28
|
+
message = ''
|
29
|
+
|
30
|
+
rc = @socket.recv_string message
|
31
|
+
if ::ZMQ::Util.resultcode_ok? rc
|
32
|
+
handle_message message
|
33
|
+
else
|
34
|
+
raise "error receiving ZMQ string: #{::ZMQ::Util.error_string}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Shut down the server
|
40
|
+
def finalize
|
41
|
+
@socket.close
|
42
|
+
end
|
43
|
+
|
44
|
+
# Handle incoming messages
|
45
|
+
def handle_message(message)
|
46
|
+
begin
|
47
|
+
message = decode_message message
|
48
|
+
rescue InvalidMessageError => ex
|
49
|
+
Celluloid::Logger.warn("couldn't decode message: #{ex.class}: #{ex}")
|
50
|
+
return
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
message.dispatch
|
55
|
+
rescue Exception => ex
|
56
|
+
Celluloid::Logger.crash("DCell::Server: message dispatch failed", ex)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class InvalidMessageError < StandardError; end # undecodable message
|
61
|
+
|
62
|
+
# Decode incoming messages
|
63
|
+
def decode_message(message)
|
64
|
+
if message[0..1].unpack("CC") == [Marshal::MAJOR_VERSION, Marshal::MINOR_VERSION]
|
65
|
+
begin
|
66
|
+
Marshal.load message
|
67
|
+
rescue => ex
|
68
|
+
raise InvalidMessageError, "invalid message: #{ex}"
|
69
|
+
end
|
70
|
+
else raise InvalidMessageError, "couldn't determine message format: #{message}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Terminate this server
|
75
|
+
def terminate
|
76
|
+
@socket.close
|
77
|
+
super
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/dcell/version.rb
CHANGED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DCell::ActorProxy do
|
4
|
+
before :all do
|
5
|
+
@node = DCell::Node['test_node']
|
6
|
+
@node.id.should == 'test_node'
|
7
|
+
@remote_actor = @node[:test_actor]
|
8
|
+
|
9
|
+
class LocalActor
|
10
|
+
include Celluloid
|
11
|
+
|
12
|
+
attr_reader :crash_reason
|
13
|
+
trap_exit :exit_handler
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@crash_reason = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def exit_handler(actor, reason)
|
20
|
+
@crash_reason = reason
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "makes synchronous calls to remote actors" do
|
26
|
+
@remote_actor.value.should == 42
|
27
|
+
end
|
28
|
+
|
29
|
+
context :linking do
|
30
|
+
before :each do
|
31
|
+
@local_actor = LocalActor.new
|
32
|
+
end
|
33
|
+
|
34
|
+
it "links to remote actors" do
|
35
|
+
@local_actor.link @remote_actor
|
36
|
+
@local_actor.linked_to?(@remote_actor).should be_true
|
37
|
+
@remote_actor.linked_to?(@local_actor).should be_true
|
38
|
+
end
|
39
|
+
|
40
|
+
it "unlinks from remote actors" do
|
41
|
+
@local_actor.link @remote_actor
|
42
|
+
@local_actor.unlink @remote_actor
|
43
|
+
|
44
|
+
@local_actor.linked_to?(@remote_actor).should be_false
|
45
|
+
@remote_actor.linked_to?(@local_actor).should be_false
|
46
|
+
end
|
47
|
+
|
48
|
+
it "traps exit messages from other actors" do
|
49
|
+
@local_actor.link @remote_actor
|
50
|
+
|
51
|
+
expect do
|
52
|
+
@remote_actor.crash
|
53
|
+
end.to raise_exception(RuntimeError)
|
54
|
+
|
55
|
+
sleep 0.1 # hax to prevent a race between exit handling and the next call
|
56
|
+
@local_actor.crash_reason.should be_a(RuntimeError)
|
57
|
+
@local_actor.crash_reason.to_s.should == "the spec purposely crashed me :("
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|