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.
Files changed (44) hide show
  1. data/.gitignore +2 -0
  2. data/.rspec +4 -0
  3. data/CHANGES.md +8 -0
  4. data/Gemfile +2 -0
  5. data/README.md +209 -0
  6. data/Rakefile +3 -0
  7. data/benchmarks/messaging.rb +67 -0
  8. data/benchmarks/receiver.rb +37 -0
  9. data/dcell.gemspec +10 -3
  10. data/lib/celluloid/README +8 -0
  11. data/lib/celluloid/zmq.rb +28 -0
  12. data/lib/celluloid/zmq/mailbox.rb +13 -0
  13. data/lib/celluloid/zmq/reactor.rb +74 -0
  14. data/lib/dcell.rb +92 -2
  15. data/lib/dcell/actor_proxy.rb +4 -0
  16. data/lib/dcell/application.rb +6 -0
  17. data/lib/dcell/celluloid_ext.rb +57 -0
  18. data/lib/dcell/directory.rb +23 -0
  19. data/lib/dcell/global.rb +23 -0
  20. data/lib/dcell/mailbox_proxy.rb +61 -0
  21. data/lib/dcell/messages.rb +67 -0
  22. data/lib/dcell/node.rb +120 -0
  23. data/lib/dcell/registries/redis_adapter.rb +86 -0
  24. data/lib/dcell/registries/zk_adapter.rb +122 -0
  25. data/lib/dcell/responses.rb +16 -0
  26. data/lib/dcell/router.rb +71 -0
  27. data/lib/dcell/rspec.rb +1 -0
  28. data/lib/dcell/server.rb +80 -0
  29. data/lib/dcell/version.rb +1 -1
  30. data/spec/celluloid/zmq/mailbox_spec.rb +6 -0
  31. data/spec/dcell/actor_proxy_spec.rb +60 -0
  32. data/spec/dcell/celluloid_ext_spec.rb +21 -0
  33. data/spec/dcell/directory_spec.rb +8 -0
  34. data/spec/dcell/global_spec.rb +21 -0
  35. data/spec/dcell/node_spec.rb +23 -0
  36. data/spec/dcell/registries/redis_adapter_spec.rb +6 -0
  37. data/spec/dcell/registries/zk_adapter_spec.rb +11 -0
  38. data/spec/spec_helper.rb +16 -0
  39. data/spec/support/helpers.rb +40 -0
  40. data/spec/support/registry_examples.rb +35 -0
  41. data/spec/test_node.rb +33 -0
  42. data/tasks/rspec.task +7 -0
  43. data/tasks/zookeeper.task +58 -0
  44. metadata +111 -7
data/.gitignore CHANGED
@@ -2,3 +2,5 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ tmp/*
6
+ zookeeper/*
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --format documentation
3
+ --backtrace
4
+ --default_path spec
data/CHANGES.md ADDED
@@ -0,0 +1,8 @@
1
+ 0.0.1
2
+ -----
3
+
4
+ * Initial release
5
+
6
+ 0.0.0
7
+ -----
8
+ * Vapoware release to claim the "dcell" gem name >:D
data/Gemfile CHANGED
@@ -1,4 +1,6 @@
1
1
  source "http://rubygems.org"
2
2
 
3
+ gem 'celluloid', :git => 'git://github.com/tarcieri/celluloid'
4
+
3
5
  # Specify your gem's dependencies in dcell.gemspec
4
6
  gemspec
data/README.md ADDED
@@ -0,0 +1,209 @@
1
+ DCell
2
+ =====
3
+
4
+ DCell is a simple and easy way to build distributed applications in Ruby.
5
+ Somewhat similar to DRb, DCell lets you easily expose Ruby objects as network
6
+ services, and call them remotely just like you would any other Ruby object.
7
+ However, unlike DRb all objects in the system are concurrent. You can create
8
+ and register several available services on a given node, obtain handles to
9
+ them, and easily pass these handles around the network just like any other
10
+ objects.
11
+
12
+ DCell is a distributed extension to Celluloid, which provides concurrent
13
+ objects for Ruby with many of the features of Erlang, such as the ability
14
+ to supervise objects and restart them when they crash, and also link to
15
+ other objects and receive event notifications of when they crash. This makes
16
+ it easier to build robust, fault-tolerant distributed systems.
17
+
18
+ You can read more about Celluloid at: http://celluloid.github.com
19
+
20
+ Supported Platforms
21
+ -------------------
22
+
23
+ DCell works on Ruby 1.9.2/1.9.3, JRuby 1.6 (in 1.9 mode), and Rubinius 2.0.
24
+
25
+ To use JRuby in 1.9 mode, you'll need to pass the "--1.9" command line
26
+ option to the JRuby executable, or set the "JRUBY_OPTS=--1.9" environment
27
+ variable:
28
+
29
+ export JRUBY_OPTS=--1.9
30
+
31
+ (Note: I'd recommend putting the above in your .bashrc/.zshrc/etc in
32
+ general. 1.9 is the future, time to embrace it)
33
+
34
+ Celluloid works on Rubinius in either 1.8 or 1.9 mode.
35
+
36
+ All components, including the 0MQ bindings, Redis, and Zookeeper adapters
37
+ are all certified to work on the above platforms. The 0MQ binding is FFI.
38
+ The Redis adapter is pure Ruby. The Zookeeper adapter uses an MRI-style
39
+ native extension but also supplies a pure-Java backend for JRuby.
40
+
41
+ Prerequisites
42
+ -------------
43
+
44
+ DCell requires 0MQ. On OS X, this is available through Homebrew by running:
45
+
46
+ brew install zeromq
47
+
48
+ DCell keeps the state of all connected nodes and global configuration data
49
+ in a service it calls the "registry". There are presently two supported
50
+ registry services:
51
+
52
+ * Redis (Fast and Loose): Redis is a persistent data structures server.
53
+ It's simple and easy to use for development and prototyping, but lacks a
54
+ good distribution story.
55
+
56
+ * Zookeeper (Serious Business): Zookeeper is a high-performance coordination
57
+ service for distributed applications. It exposes common services such as
58
+ naming, configuration management, synchronization, and group management.
59
+ Unfortunately, it has slightly more annoying client-side dependencies and is
60
+ more difficult to deploy than Redis.
61
+
62
+ You may pick either one of these services to use as DCell's registry. The
63
+ default is Redis.
64
+
65
+ To install a local copy of Redis on OS X with Homebrew, run:
66
+
67
+ brew install redis
68
+
69
+ To install a local copy Zookeeper for testing purposes, run:
70
+
71
+ rake zookeeper:install
72
+
73
+ and to start it run:
74
+
75
+ rake zookeeper:start
76
+
77
+ Configuration
78
+ -------------
79
+
80
+ The simplest way to configure and start DCell is with the following:
81
+
82
+ require 'dcell'
83
+
84
+ DCell.start
85
+
86
+ This configures DCell with all the default options, however there are many
87
+ options you can override, e.g.:
88
+
89
+ DCell.start :id => "node42", :addr => "tcp://127.0.0.1:2042"
90
+
91
+ DCell identifies each node with a unique node ID, that defaults to your
92
+ hostname. Each node needs to be reachable over 0MQ, and the addr option
93
+ specifies the 0MQ address where the host can be reached. When giving a tcp://
94
+ URL, you *must* specify an IP address and not a hostname.
95
+
96
+ To join a cluster you'll need to provide the location of the registry server.
97
+ This can be done through the "registry" configuration key:
98
+
99
+ DCell.start :id => "node24", :addr => "tcp://127.0.0.1:2042",
100
+ :registry => {
101
+ :adapter => 'redis',
102
+ :host => 'mycluster.example.org',
103
+ :port => 6379
104
+ }
105
+
106
+ When configuring DCell to use Redis, use the following options:
107
+
108
+ - **adapter**: "redis" (*optional, alternatively "zk"*)
109
+ - **host**: hostname or IP address of the Redis server (*optional, default localhost*)
110
+ - **port**: port of the Redis server (*optional, default 6379*)
111
+ - **password**: password to the Redis server (*optional*)
112
+
113
+ Usage
114
+ -----
115
+
116
+ You've now configured a single node in a DCell cluster. You can obtain the
117
+ DCell::Node object representing the local node by calling DCell.me:
118
+
119
+ >> DCell.start
120
+ => #<Celluloid::Supervisor(DCell::Application):0xed6>
121
+ >> DCell.me
122
+ => #<DCell::Node[cryptosphere.local] @addr="tcp://127.0.0.1:7777">
123
+
124
+ DCell::Node objects are the entry point for locating actors on the system.
125
+ DCell.me returns the local node. Other nodes can be obtained by their
126
+ node IDs:
127
+
128
+ >> node = DCell::Node["cryptosphere.local"]
129
+ => #<DCell::Node[cryptosphere.local] @addr="tcp://127.0.0.1:7777">
130
+
131
+ DCell::Node.all returns all connected nodes in the cluster:
132
+
133
+ >> DCell::Node.all
134
+ => [#<DCell::Node[test_node] @addr="tcp://127.0.0.1:21264">, #<DCell::Node[cryptosphere.local] @addr="tcp://127.0.0.1:7777">]
135
+
136
+ DCell::Node is a Ruby Enumerable. You can iterate across all nodes with
137
+ DCell::Node.each.
138
+
139
+ Once you've obtained a node, you can look up services it exports and call them
140
+ just like you'd invoke methods on any other Ruby object:
141
+
142
+ >> node = DCell::Node["cryptosphere.local"]
143
+ => #<DCell::Node[cryptosphere.local] @addr="tcp://127.0.0.1:7777">
144
+ >> time_server = node[:time_server]
145
+ => #<Celluloid::Actor(TimeServer:0xee8)>
146
+ >> time_server.time
147
+ => "The time is: 2011-11-10 20:23:47 -0800"
148
+
149
+ You can also find all available services on a node with DCell::Node#all:
150
+
151
+ >> node = DCell::Node["cryptosphere.local"]
152
+ => #<DCell::Node[cryptosphere.local] @addr="tcp://127.0.0.1:7777">
153
+ >> node.all
154
+ => [:time_server]
155
+
156
+ Registering Actors
157
+ ------------------
158
+
159
+ All services exposed by DCell must take the form of registered Celluloid actors.
160
+ What follows is an extremely brief introduction to creating and registering
161
+ actors, but for more information, you should definitely [read the Celluloid
162
+ documentation](http://celluloid.github.com).
163
+
164
+ DCell exposes all Celluloid actors you've registered directly onto the network.
165
+ The best way to register an actor is by supervising it. Below is an example of
166
+ how to create an actor and register it on the network:
167
+
168
+ class TimeServer
169
+ include Celluloid
170
+
171
+ def time
172
+ "The time is: #{Time.now}"
173
+ end
174
+ end
175
+
176
+ Now that we've defined the TimeServer, we're going to supervise it and register
177
+ it in the local registry:
178
+
179
+ >> TimeServer.supervise_as :time_server
180
+ => #<Celluloid::Supervisor(TimeServer):0xee4>
181
+
182
+ Supervising actors means that if they crash, they're automatically restarted
183
+ and registered under the same name. We can access registered actors by using
184
+ Celluloid::Actor#[]:
185
+
186
+ >> Celluloid::Actor[:time_server]
187
+ => #<Celluloid::Actor(TimeServer:0xee8)>
188
+ >> Celluloid::Actor[:time_server].time
189
+ => "The time is: 2011-11-10 20:17:48 -0800"
190
+
191
+ This same actor is now available using the DCell::Node#[] syntax:
192
+
193
+ >> node = DCell.me
194
+ => #<DCell::Node[cryptosphere.local] @addr="tcp://127.0.0.1:1870">
195
+ >> node[:time_server].time
196
+ => "The time is: 2011-11-10 20:28:27 -0800"
197
+
198
+ Globals
199
+ -------
200
+
201
+ DCell provides a registry global for storing configuration data and actors you
202
+ wish to publish globally to the entire cluster:
203
+
204
+ >> actor = Celluloid::Actor[:dcell_server]
205
+ => #<Celluloid::Actor(DCell::Server:0xf2e) @addr="tcp://127.0.0.1:7777">
206
+ >> DCell::Global[:sweet_server] = actor
207
+ => #<Celluloid::Actor(DCell::Server:0xf2e) @addr="tcp://127.0.0.1:7777">
208
+ >> DCell::Global[:sweet_server]
209
+ => #<Celluloid::Actor(DCell::Server:0xf2e) @addr="tcp://127.0.0.1:7777">
data/Rakefile CHANGED
@@ -1 +1,4 @@
1
1
  require "bundler/gem_tasks"
2
+ Dir["tasks/**/*.task"].each { |task| load task }
3
+
4
+ task :default => :spec
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'benchmark'
4
+
5
+ require 'rubygems'
6
+ require 'bundler'
7
+ Bundler.setup
8
+
9
+ require 'dcell'
10
+ DCell.setup
11
+ DCell.run!
12
+
13
+ RECEIVER_PORT = 12345
14
+
15
+ $receiver_pid = Process.spawn Gem.ruby, File.expand_path("../receiver.rb", __FILE__)
16
+ STDERR.print "Waiting for test node to start up..."
17
+
18
+ socket = nil
19
+ 30.times do
20
+ begin
21
+ socket = TCPSocket.open("127.0.0.1", RECEIVER_PORT)
22
+ break if socket
23
+ rescue Errno::ECONNREFUSED
24
+ STDERR.print "."
25
+ sleep 1
26
+ end
27
+ end
28
+
29
+ if socket
30
+ STDERR.puts " done!"
31
+ socket.close
32
+ else
33
+ STDERR.puts " FAILED!"
34
+ raise "couldn't connect to test node!"
35
+ end
36
+
37
+ class AsyncPerformanceTest
38
+ include Celluloid
39
+
40
+ def initialize(progenator, n = 10000)
41
+ @n = n
42
+ @receiver = progenator.spawn_async_receiver(n, current_actor)
43
+ end
44
+
45
+ def run
46
+ @n.times { @receiver.increment! }
47
+ wait :complete
48
+ end
49
+
50
+ def complete
51
+ signal :complete
52
+ end
53
+ end
54
+
55
+ receiver = DCell::Node['benchmark_receiver']
56
+ progenator = receiver[:progenator]
57
+
58
+ test = AsyncPerformanceTest.new progenator
59
+ time = Benchmark.measure { test.run }.real
60
+ messages_per_second = 1 / time * 10000
61
+
62
+ puts "messages_per_second: #{"%0.2f" % messages_per_second}"
63
+
64
+ Process.kill 9, $receiver_pid
65
+ Process.wait $receiver_pid rescue nil
66
+
67
+ exit 0
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup
4
+
5
+ require 'dcell'
6
+ DCell.setup :id => 'benchmark_receiver', :addr => 'tcp://127.0.0.1:12345'
7
+
8
+ class AsyncReceiver
9
+ include Celluloid
10
+ attr_reader :count
11
+
12
+ def initialize(n, actor)
13
+ @n, @actor = n, actor
14
+ @count = 0
15
+ end
16
+
17
+ def increment
18
+ @count += 1
19
+ @actor.complete! if @count == @n
20
+ @count
21
+ end
22
+ end
23
+
24
+ class Progenator
25
+ include Celluloid
26
+
27
+ def spawn_async_receiver(n, actor)
28
+ AsyncReceiver.new(n, actor)
29
+ end
30
+ end
31
+
32
+ class BenchmarkApplication < Celluloid::Application
33
+ supervise DCell::Application
34
+ supervise Progenator, :as => :progenator
35
+ end
36
+
37
+ BenchmarkApplication.run
data/dcell.gemspec CHANGED
@@ -10,12 +10,19 @@ Gem::Specification.new do |s|
10
10
  s.homepage = "http://github.com/tarcieri/dcell"
11
11
  s.summary = "An asynchronous distributed object framework based on Celluloid"
12
12
  s.description = "DCell is an distributed object framework based on Celluloid built on 0MQ and Zookeeper"
13
-
13
+
14
14
  s.files = `git ls-files`.split("\n")
15
15
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
16
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
17
  s.require_paths = ["lib"]
18
-
19
- s.add_dependency "celluloid"
18
+
19
+ s.add_dependency "celluloid", ">= 0.6.2"
20
+ s.add_dependency "ffi"
20
21
  s.add_dependency "ffi-rzmq"
22
+ s.add_dependency "redis"
23
+ s.add_dependency "redis-namespace"
24
+
25
+ s.add_development_dependency "rake"
26
+ s.add_development_dependency "rspec", ">= 2.7.0"
27
+ #s.add_development_dependency "zk"
21
28
  end
@@ -0,0 +1,8 @@
1
+ Ideally DCell would use Celluloid::IO to monitor 0MQ sockets. Unfortunately,
2
+ ffi-rzmq presently does not present an API for obtaining an IO object from a
3
+ 0MQ socket.
4
+
5
+ This directory contains a temporary workaround Celluloid::ZMQ which uses
6
+ a ZMQ::Poller to monitor for 0MQ activity. This implementation is hardly
7
+ anywhere near ideal but it works and provides a temporary solution until
8
+ ffi-rzmq implements an API for retrieving IO objects.
@@ -0,0 +1,28 @@
1
+ require 'ffi-rzmq'
2
+
3
+ require 'celluloid'
4
+ require 'celluloid/zmq/mailbox'
5
+ require 'celluloid/zmq/reactor'
6
+
7
+ module Celluloid
8
+ # Actors which run alongside 0MQ operations
9
+ # This is a temporary hack (hopefully) until ffi-rzmq exposes IO objects for
10
+ # 0MQ sockets that can be used with Celluloid::IO
11
+ module ZMQ
12
+ def self.included(klass)
13
+ klass.send :include, ::Celluloid
14
+ klass.use_mailbox Celluloid::ZMQ::Mailbox
15
+ end
16
+
17
+ # Wait for the given IO object to become readable
18
+ def wait_readable(socket)
19
+ # Law of demeter be damned!
20
+ current_actor.mailbox.reactor.wait_readable(socket)
21
+ end
22
+
23
+ # Wait for the given IO object to become writeable
24
+ def wait_writeable(socket)
25
+ current_actor.mailbox.reactor.wait_writeable(socket)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ module Celluloid
2
+ module ZMQ
3
+ # A Celluloid mailbox for Actors that wait on 0MQ sockets
4
+ class Mailbox < Celluloid::IO::Mailbox
5
+ def initialize
6
+ @messages = []
7
+ @lock = Mutex.new
8
+ @waker = Celluloid::IO::Waker.new
9
+ @reactor = Reactor.new(@waker)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,74 @@
1
+ module Celluloid
2
+ module ZMQ
3
+ # React to incoming 0MQ and Celluloid events. This is kinda sorta supposed
4
+ # to resemble the Reactor design pattern.
5
+ class Reactor
6
+ def initialize(waker)
7
+ @waker = waker
8
+ @poller = ::ZMQ::Poller.new
9
+ @readers = {}
10
+ @writers = {}
11
+
12
+ # FIXME: The way things are presently implemented is super ghetto
13
+ # The ZMQ::Poller should be able to wait on the waker somehow
14
+ # but I can't get it to work :(
15
+ #result = @poller.register(nil, ::ZMQ::POLLIN, @waker.io.fileno)
16
+ #
17
+ #unless ::ZMQ::Util.resultcode_ok?(result)
18
+ # raise "couldn't register waker with 0MQ poller"
19
+ #end
20
+ end
21
+
22
+ # Wait for the given ZMQ socket to become readable
23
+ def wait_readable(socket)
24
+ monitor_zmq socket, @readers, ::ZMQ::POLLIN
25
+ end
26
+
27
+ # Wait for the given ZMQ socket to become writeable
28
+ def wait_writeable(socket)
29
+ monitor_zmq socket, @writers, ::ZMQ::POLLOUT
30
+ end
31
+
32
+ # Monitor the given ZMQ socket with the given options
33
+ def monitor_zmq(socket, set, type)
34
+ if set.has_key? socket
35
+ raise ArgumentError, "another method is already waiting on #{socket.inspect}"
36
+ else
37
+ set[socket] = Fiber.current
38
+ end
39
+
40
+ @poller.register socket, type
41
+ Fiber.yield
42
+
43
+ @poller.deregister socket, type
44
+ socket
45
+ end
46
+
47
+ # Run the reactor, waiting for events, and calling the given block if
48
+ # the reactor is awoken by the waker
49
+ def run_once
50
+ # FIXME: This approach is super ghetto. Find some way to make the
51
+ # ZMQ::Poller wait on the waker's file descriptor
52
+ if @poller.size == 0
53
+ readable, _ = select [@waker.io]
54
+ yield if readable and readable.include? @waker.io
55
+ else
56
+ if ::ZMQ::Util.resultcode_ok? @poller.poll(100)
57
+ @poller.readables.each do |sock|
58
+ fiber = @readers.delete sock
59
+ fiber.resume if fiber
60
+ end
61
+
62
+ @poller.writables.each do |sock|
63
+ fiber = @writers.delete sock
64
+ fiber.resume if fiber
65
+ end
66
+ end
67
+
68
+ readable, _ = select [@waker.io], [], [], 0
69
+ yield if readable and readable.include? @waker.io
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end