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/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: []