dcell 0.0.0 → 0.0.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.
- 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
|