stormy-cloud 0.0.8
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/dashboard/dash.rb +62 -0
- data/dashboard/index.html +124 -0
- data/dashboard/public/images/apple-touch-icon-114x114.png +0 -0
- data/dashboard/public/images/apple-touch-icon-72x72.png +0 -0
- data/dashboard/public/images/apple-touch-icon.png +0 -0
- data/dashboard/public/images/favicon.ico +0 -0
- data/dashboard/public/jquery.js +4 -0
- data/dashboard/public/stylesheets/base.css +269 -0
- data/dashboard/public/stylesheets/layout.css +58 -0
- data/dashboard/public/stylesheets/skeleton.css +242 -0
- data/lib/stormy-cloud.rb +174 -0
- data/lib/transport.rb +269 -0
- data/lib/transports/tcp.rb +40 -0
- metadata +58 -0
data/lib/transport.rb
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'digest'
|
|
3
|
+
require 'msgpack'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'set'
|
|
6
|
+
require 'thread'
|
|
7
|
+
|
|
8
|
+
# Define the basic outline of a transport, and provide serialization and stuff
|
|
9
|
+
# so that code doesn't have to be duplicated in every transport definition.
|
|
10
|
+
class StormyCloudTransport
|
|
11
|
+
attr_reader :secret, :stormy_cloud, :queue, :task_count
|
|
12
|
+
attr_accessor :mode
|
|
13
|
+
|
|
14
|
+
# This method should be "extended" by the specific transports, i.e. they
|
|
15
|
+
# should first call super and then perform any transport-specific
|
|
16
|
+
# instantiation.
|
|
17
|
+
def initialize(stormy_cloud)
|
|
18
|
+
# Generate a secret that will be used to shutdown the server.
|
|
19
|
+
@secret = SecureRandom.hex(32)
|
|
20
|
+
# A hash of identifier -> time of last server access.
|
|
21
|
+
@clients = {}
|
|
22
|
+
# Are we operating in server mode or client mode?
|
|
23
|
+
@mode = :server
|
|
24
|
+
# Save `stormy_cloud`.
|
|
25
|
+
@stormy_cloud = stormy_cloud
|
|
26
|
+
# Create a new empty queue for storing sub tasks.
|
|
27
|
+
@queue = Queue.new
|
|
28
|
+
# A list of tasks that are currently being worked on.
|
|
29
|
+
@assigned = []
|
|
30
|
+
# A mutex to synchronize access to the queue, assigned list and completed
|
|
31
|
+
# list.
|
|
32
|
+
@queue_mutex = Mutex.new
|
|
33
|
+
# A set of completed tasks.
|
|
34
|
+
# TODO: This needs to be synchronized with a file for persistence.
|
|
35
|
+
@completed = Set.new
|
|
36
|
+
# Statistics.
|
|
37
|
+
@task_count = 0
|
|
38
|
+
@completed_count = 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A unique identifier derived from the secret.
|
|
42
|
+
def identifier
|
|
43
|
+
Digest::MD5.hexdigest("Omikron" + @secret + "9861")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Run the split method on the stormy cloud, and save the results into a
|
|
47
|
+
# queue.
|
|
48
|
+
def split
|
|
49
|
+
tasks = @stormy_cloud.split
|
|
50
|
+
@task_count = tasks.length
|
|
51
|
+
tasks.each {|x| @queue.push x }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Remove tasks from the queue until we encounter one which is not on the
|
|
55
|
+
# completed list. Add this task to the assigned list, and spawn a thread
|
|
56
|
+
# to move it back into the queue after a timeout.
|
|
57
|
+
# If the queue is empty, return a random task from the assigned list.
|
|
58
|
+
# If both the queue and the assigned list are empty, return nil.
|
|
59
|
+
def get_task
|
|
60
|
+
@queue_mutex.synchronize do
|
|
61
|
+
if @queue.empty?
|
|
62
|
+
|
|
63
|
+
if @assigned.length == 0
|
|
64
|
+
return nil
|
|
65
|
+
else
|
|
66
|
+
task = @assigned.sample
|
|
67
|
+
_spawn_remover(task)
|
|
68
|
+
return task
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
else
|
|
72
|
+
|
|
73
|
+
task = @queue.pop
|
|
74
|
+
if @completed.include? task
|
|
75
|
+
return get
|
|
76
|
+
else
|
|
77
|
+
_spawn_remover(task)
|
|
78
|
+
@assigned.push task
|
|
79
|
+
return task
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Spawn a thread that will wait for some time and then remove the task from
|
|
87
|
+
# the assigned list and add it back to the queue if it is still in the
|
|
88
|
+
# assigned list.
|
|
89
|
+
def _spawn_remover(task)
|
|
90
|
+
Thread.new do
|
|
91
|
+
sleep @stormy_cloud.config(:wait)
|
|
92
|
+
@queue_mutex.synchronize do
|
|
93
|
+
if @assigned.include? task
|
|
94
|
+
@assigned.delete(task)
|
|
95
|
+
@queue.push(task)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Accept the results of a task from a node. If the task is still in the
|
|
102
|
+
# assigned list, get it out of there, put it in the completed set and
|
|
103
|
+
# call the reduce method.
|
|
104
|
+
# If the task is not on the assigned list and is also not on the completed
|
|
105
|
+
# list, add it to the completed list and call the reduce method.
|
|
106
|
+
# If the task is already in the completed set, do nothing.
|
|
107
|
+
# If the job is complete, call finally.
|
|
108
|
+
def put_task(task, result)
|
|
109
|
+
@queue_mutex.synchronize do
|
|
110
|
+
return if @completed.include? task
|
|
111
|
+
|
|
112
|
+
if @assigned.include? task
|
|
113
|
+
@assigned.delete task
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if not @completed.include? task
|
|
117
|
+
@completed.add task
|
|
118
|
+
@stormy_cloud.reduce(task, result)
|
|
119
|
+
@completed_count += 1
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if complete?
|
|
124
|
+
@stormy_cloud.finally
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Is the job complete? This is true if the queue is empty and the assigned
|
|
129
|
+
# list is empty.
|
|
130
|
+
def complete?
|
|
131
|
+
@queue.empty? and @assigned.empty?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# This method is used by the server to handle communication with clients.
|
|
135
|
+
# It should not be overridden by specific transports. It accepts a string
|
|
136
|
+
# which is a serialized command sent by the server, and returns another
|
|
137
|
+
# serialized string which is the response that should be sent to the client.
|
|
138
|
+
# All "commands" are basically arrays in which the first element is the
|
|
139
|
+
# action and subsequent elements are parameters. _Every_ command will have
|
|
140
|
+
# the identifier as its first parameter.
|
|
141
|
+
#
|
|
142
|
+
# The following actions are supported:
|
|
143
|
+
# HELLO(identifier) -> get the server's identifier.
|
|
144
|
+
# GET(identifier) -> get a new task from the server.
|
|
145
|
+
# PUT(identifier, task, result)
|
|
146
|
+
# -> return the result of a task to the server.
|
|
147
|
+
#
|
|
148
|
+
def handle(string)
|
|
149
|
+
valid_commands = ["HELLO", "GET", "PUT"]
|
|
150
|
+
command = unserialize(string)
|
|
151
|
+
|
|
152
|
+
if not (command.kind_of?(Array) and valid_commands.include? command[0])
|
|
153
|
+
# The command is invalid.
|
|
154
|
+
return serialize("INVALID COMMAND")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Update the time of last access.
|
|
158
|
+
@clients[command[1]] = Time.now
|
|
159
|
+
|
|
160
|
+
if command[0] == "HELLO"
|
|
161
|
+
|
|
162
|
+
if command.length == 2
|
|
163
|
+
return serialize(identifier)
|
|
164
|
+
else
|
|
165
|
+
return serialize("INVALID COMMAND")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
elsif command[0] == "GET"
|
|
169
|
+
|
|
170
|
+
return serialize(get_task)
|
|
171
|
+
|
|
172
|
+
elsif command[0] == "PUT"
|
|
173
|
+
|
|
174
|
+
task = command[2]
|
|
175
|
+
result = command[3]
|
|
176
|
+
|
|
177
|
+
if command.length != 4
|
|
178
|
+
return serialize("INVALID COMMAND")
|
|
179
|
+
else
|
|
180
|
+
put_task(task, result)
|
|
181
|
+
return serialize("OKAY")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Initialize a server that will accept commands from nodes and forward them
|
|
188
|
+
# to the handle method. The server should operate in a loop that is broken
|
|
189
|
+
# when @mode is no longer :server. This method should be nonblocking.
|
|
190
|
+
# This method should be implemented by the specific transport.
|
|
191
|
+
def initialize_server
|
|
192
|
+
raise NotImplementedError
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# The server should be killed when this method is called.
|
|
196
|
+
# This method should be implemented by the specific transport.
|
|
197
|
+
def kill_server
|
|
198
|
+
raise NotImplementedError
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# A wrapper around the raw_send_message method that does serialization and
|
|
202
|
+
# unserialization.
|
|
203
|
+
def send_message(object)
|
|
204
|
+
unserialize(raw_send_message(serialize(object)))
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Send a raw string to the server, and return the response.
|
|
208
|
+
# This method should be implemented by the specific transport.
|
|
209
|
+
def raw_send_message(string)
|
|
210
|
+
raise NotImplementedError
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Serialize a request into a string by doing MsgPack + Base64 encoding.
|
|
214
|
+
# This can be overridden by the specific transport if the protocol used is
|
|
215
|
+
# such that this mode of serialization is disadvantageous.
|
|
216
|
+
def serialize(object)
|
|
217
|
+
Base64::encode64(object.to_msgpack)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Unserialize an object which has been serialized using the `serialize`
|
|
221
|
+
# method.
|
|
222
|
+
def unserialize(string)
|
|
223
|
+
MessagePack.unpack(Base64::decode64(string))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Return all variables relevant to the job status.
|
|
227
|
+
def status
|
|
228
|
+
{
|
|
229
|
+
clients: @clients,
|
|
230
|
+
assigned: @assigned,
|
|
231
|
+
task_count: @task_count,
|
|
232
|
+
completed_count: @completed_count,
|
|
233
|
+
assigned_count: @assigned.length,
|
|
234
|
+
name: @stormy_cloud.name
|
|
235
|
+
}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Block until the job is complete -- meant for usage by the server.
|
|
239
|
+
def block_until_complete
|
|
240
|
+
while not complete?
|
|
241
|
+
sleep 1
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def result
|
|
246
|
+
@stormy_cloud.result
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Actually run the task.
|
|
250
|
+
# First, we check whether we are currently the server by asking the server
|
|
251
|
+
# for its identifier and seeing if it matches ours.
|
|
252
|
+
def run
|
|
253
|
+
if @mode == :server
|
|
254
|
+
split
|
|
255
|
+
initialize_server
|
|
256
|
+
block_until_complete
|
|
257
|
+
else
|
|
258
|
+
@mode = :client
|
|
259
|
+
# Do clienty things.
|
|
260
|
+
loop do
|
|
261
|
+
task = send_message(["GET", identifier])
|
|
262
|
+
break if task == nil
|
|
263
|
+
result = @stormy_cloud.map(task)
|
|
264
|
+
send_message(["PUT", identifier, task, result])
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require_relative "../transport.rb"
|
|
2
|
+
require 'socket'
|
|
3
|
+
|
|
4
|
+
class StormyCloudTCPTransport < StormyCloudTransport
|
|
5
|
+
def initialize_server
|
|
6
|
+
@server = TCPServer.new @stormy_cloud.config(:port)
|
|
7
|
+
@server_thread = Thread.new do
|
|
8
|
+
loop do
|
|
9
|
+
Thread.start(@server.accept) do |client|
|
|
10
|
+
client.puts handle(client.gets)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def kill_server
|
|
17
|
+
@server_thread.kill
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def raw_send_message(string)
|
|
21
|
+
begin
|
|
22
|
+
s = TCPSocket.new(@stormy_cloud.server, @stormy_cloud.config(:port))
|
|
23
|
+
s.puts string
|
|
24
|
+
res = s.gets
|
|
25
|
+
s.close
|
|
26
|
+
res
|
|
27
|
+
rescue
|
|
28
|
+
@errors ||= 0
|
|
29
|
+
@errors += 1
|
|
30
|
+
if @errors <= 5
|
|
31
|
+
STDERR.puts "An error occurred while contacting the server, trying again."
|
|
32
|
+
sleep 1
|
|
33
|
+
raw_send_message(string)
|
|
34
|
+
else
|
|
35
|
+
STDERR.puts "Could not contact the server, exiting."
|
|
36
|
+
exit
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: stormy-cloud
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.8
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Vikhyat Korrapati
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-06-18 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: StormyCloud makes writing distributed applications in Ruby a piece of
|
|
15
|
+
cake.
|
|
16
|
+
email: c@vikhyat.net
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- lib/stormy-cloud.rb
|
|
22
|
+
- lib/transport.rb
|
|
23
|
+
- lib/transports/tcp.rb
|
|
24
|
+
- dashboard/dash.rb
|
|
25
|
+
- dashboard/index.html
|
|
26
|
+
- dashboard/public/images/apple-touch-icon-114x114.png
|
|
27
|
+
- dashboard/public/images/apple-touch-icon-72x72.png
|
|
28
|
+
- dashboard/public/images/apple-touch-icon.png
|
|
29
|
+
- dashboard/public/images/favicon.ico
|
|
30
|
+
- dashboard/public/jquery.js
|
|
31
|
+
- dashboard/public/stylesheets/base.css
|
|
32
|
+
- dashboard/public/stylesheets/layout.css
|
|
33
|
+
- dashboard/public/stylesheets/skeleton.css
|
|
34
|
+
homepage: https://github.com/vikhyat/StormyCloud
|
|
35
|
+
licenses: []
|
|
36
|
+
post_install_message:
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- lib
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
none: false
|
|
42
|
+
requirements:
|
|
43
|
+
- - ! '>='
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
|
+
none: false
|
|
48
|
+
requirements:
|
|
49
|
+
- - ! '>='
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubyforge_project:
|
|
54
|
+
rubygems_version: 1.8.24
|
|
55
|
+
signing_key:
|
|
56
|
+
specification_version: 3
|
|
57
|
+
summary: Ridiculously simple distributed applications.
|
|
58
|
+
test_files: []
|