cloud-crowd 0.1.0

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 (66) hide show
  1. data/EPIGRAPHS +17 -0
  2. data/LICENSE +22 -0
  3. data/README +93 -0
  4. data/actions/graphics_magick.rb +43 -0
  5. data/actions/process_pdfs.rb +92 -0
  6. data/actions/word_count.rb +14 -0
  7. data/bin/crowd +5 -0
  8. data/cloud-crowd.gemspec +111 -0
  9. data/config/config.example.ru +17 -0
  10. data/config/config.example.yml +48 -0
  11. data/config/database.example.yml +9 -0
  12. data/examples/graphics_magick_example.rb +44 -0
  13. data/examples/process_pdfs_example.rb +40 -0
  14. data/examples/word_count_example.rb +41 -0
  15. data/lib/cloud-crowd.rb +130 -0
  16. data/lib/cloud_crowd/action.rb +101 -0
  17. data/lib/cloud_crowd/app.rb +117 -0
  18. data/lib/cloud_crowd/asset_store.rb +41 -0
  19. data/lib/cloud_crowd/asset_store/filesystem_store.rb +28 -0
  20. data/lib/cloud_crowd/asset_store/s3_store.rb +40 -0
  21. data/lib/cloud_crowd/command_line.rb +209 -0
  22. data/lib/cloud_crowd/daemon.rb +95 -0
  23. data/lib/cloud_crowd/exceptions.rb +28 -0
  24. data/lib/cloud_crowd/helpers.rb +8 -0
  25. data/lib/cloud_crowd/helpers/authorization.rb +50 -0
  26. data/lib/cloud_crowd/helpers/resources.rb +45 -0
  27. data/lib/cloud_crowd/inflector.rb +19 -0
  28. data/lib/cloud_crowd/models.rb +40 -0
  29. data/lib/cloud_crowd/models/job.rb +176 -0
  30. data/lib/cloud_crowd/models/work_unit.rb +89 -0
  31. data/lib/cloud_crowd/models/worker_record.rb +61 -0
  32. data/lib/cloud_crowd/runner.rb +15 -0
  33. data/lib/cloud_crowd/schema.rb +45 -0
  34. data/lib/cloud_crowd/worker.rb +186 -0
  35. data/public/css/admin_console.css +221 -0
  36. data/public/css/reset.css +42 -0
  37. data/public/images/bullet_green.png +0 -0
  38. data/public/images/bullet_white.png +0 -0
  39. data/public/images/cloud_hand.png +0 -0
  40. data/public/images/header_back.png +0 -0
  41. data/public/images/logo.png +0 -0
  42. data/public/images/queue_fill.png +0 -0
  43. data/public/images/server_error.png +0 -0
  44. data/public/images/sidebar_bottom.png +0 -0
  45. data/public/images/sidebar_top.png +0 -0
  46. data/public/images/worker_info.png +0 -0
  47. data/public/images/worker_info_loading.gif +0 -0
  48. data/public/js/admin_console.js +168 -0
  49. data/public/js/excanvas.js +1 -0
  50. data/public/js/flot.js +1 -0
  51. data/public/js/jquery.js +19 -0
  52. data/test/acceptance/test_app.rb +72 -0
  53. data/test/acceptance/test_failing_work_units.rb +32 -0
  54. data/test/acceptance/test_word_count.rb +49 -0
  55. data/test/blueprints.rb +17 -0
  56. data/test/config/actions/failure_testing.rb +13 -0
  57. data/test/config/config.ru +17 -0
  58. data/test/config/config.yml +7 -0
  59. data/test/config/database.yml +6 -0
  60. data/test/test_helper.rb +19 -0
  61. data/test/unit/test_action.rb +49 -0
  62. data/test/unit/test_configuration.rb +28 -0
  63. data/test/unit/test_job.rb +78 -0
  64. data/test/unit/test_work_unit.rb +55 -0
  65. data/views/index.erb +77 -0
  66. metadata +233 -0
@@ -0,0 +1,15 @@
1
+ # This is the script that kicks off a single CloudCrowd::Daemon. Rely on
2
+ # cloud-crowd.rb for autoloading of all the code we need.
3
+
4
+ require "#{File.dirname(__FILE__)}/../cloud-crowd"
5
+
6
+ FileUtils.mkdir('log') unless File.exists?('log')
7
+
8
+ Daemons.run("#{CloudCrowd::ROOT}/lib/cloud_crowd/daemon.rb", {
9
+ :app_name => "cloud_crowd_worker",
10
+ :dir_mode => :normal,
11
+ :dir => 'log',
12
+ :multiple => true,
13
+ :backtrace => true,
14
+ :log_output => true
15
+ })
@@ -0,0 +1,45 @@
1
+ # Complete schema for CloudCrowd.
2
+ ActiveRecord::Schema.define(:version => 1) do
3
+
4
+ create_table "jobs", :force => true do |t|
5
+ t.integer "status", :null => false
6
+ t.text "inputs", :null => false
7
+ t.string "action", :null => false
8
+ t.text "options", :null => false
9
+ t.text "outputs"
10
+ t.float "time"
11
+ t.string "callback_url"
12
+ t.string "email"
13
+ t.integer "lock_version", :default => 0, :null => false
14
+ t.datetime "created_at"
15
+ t.datetime "updated_at"
16
+ end
17
+
18
+ create_table "work_units", :force => true do |t|
19
+ t.integer "status", :null => false
20
+ t.integer "job_id", :null => false
21
+ t.text "input", :null => false
22
+ t.string "action", :null => false
23
+ t.integer "attempts", :default => 0, :null => false
24
+ t.integer "lock_version", :default => 0, :null => false
25
+ t.integer "worker_record_id"
26
+ t.float "time"
27
+ t.text "output"
28
+ t.datetime "created_at"
29
+ t.datetime "updated_at"
30
+ end
31
+
32
+ create_table "worker_records", :force => true do |t|
33
+ t.string "name", :null => false
34
+ t.string "thread_status", :null => false
35
+ t.datetime "created_at"
36
+ t.datetime "updated_at"
37
+ end
38
+
39
+ add_index "jobs", ["status"], :name => "index_jobs_on_status"
40
+ add_index "work_units", ["job_id"], :name => "index_work_units_on_job_id"
41
+ add_index "work_units", ["status", "worker_record_id", "action"], :name => "index_work_units_on_status_and_worker_record_id_and_action"
42
+ add_index "worker_records", ["name"], :name => "index_worker_records_on_name"
43
+ add_index "worker_records", ["updated_at"], :name => "index_worker_records_on_updated_at"
44
+
45
+ end
@@ -0,0 +1,186 @@
1
+ module CloudCrowd
2
+
3
+ # The Worker, run at intervals by the Daemon, fetches WorkUnits from the
4
+ # central server and dispatches Actions to process them. Workers only fetch
5
+ # units that they are able to handle (for which they have an action in their
6
+ # actions directory). If communication with the central server is interrupted,
7
+ # the WorkUnit will repeatedly attempt to complete its unit -- every
8
+ # Worker::RETRY_WAIT seconds. Any exceptions that take place during
9
+ # the course of the Action will cause the Worker to mark the WorkUnit as
10
+ # having failed.
11
+ class Worker
12
+
13
+ # The time between worker check-ins with the central server, informing
14
+ # it of the current status, and simply that it's still alive.
15
+ CHECK_IN_INTERVAL = 60
16
+
17
+ # Wait five seconds to retry, after internal communcication errors.
18
+ RETRY_WAIT = 5
19
+
20
+ attr_reader :action
21
+
22
+ # Spinning up a worker will create a new AssetStore with a persistent
23
+ # connection to S3. This AssetStore gets passed into each action, for use
24
+ # as it is run.
25
+ def initialize
26
+ @id = $$
27
+ @hostname = Socket.gethostname
28
+ @name = "#{@id}@#{@hostname}"
29
+ @store = AssetStore.new
30
+ @server = CloudCrowd.central_server
31
+ @enabled_actions = CloudCrowd.actions.keys
32
+ log 'started'
33
+ end
34
+
35
+ # Ask the central server for the first WorkUnit in line.
36
+ def fetch_work_unit
37
+ keep_trying_to "fetch a new work unit" do
38
+ unit_json = @server['/work'].post(base_params)
39
+ setup_work_unit(unit_json)
40
+ end
41
+ end
42
+
43
+ # Return output to the central server, marking the current work unit as done.
44
+ def complete_work_unit(result)
45
+ keep_trying_to "complete work unit" do
46
+ data = completion_params.merge({:status => 'succeeded', :output => result})
47
+ unit_json = @server["/work/#{data[:id]}"].put(data)
48
+ log "finished #{display_work_unit} in #{data[:time]} seconds"
49
+ clear_work_unit
50
+ setup_work_unit(unit_json)
51
+ end
52
+ end
53
+
54
+ # Mark the current work unit as failed, returning the exception to central.
55
+ def fail_work_unit(exception)
56
+ keep_trying_to "mark work unit as failed" do
57
+ data = completion_params.merge({:status => 'failed', :output => {'output' => exception.message}.to_json})
58
+ unit_json = @server["/work/#{data[:id]}"].put(data)
59
+ log "failed #{display_work_unit} in #{data[:time]} seconds\n#{exception.message}\n#{exception.backtrace}"
60
+ clear_work_unit
61
+ setup_work_unit(unit_json)
62
+ end
63
+ end
64
+
65
+ # Check in with the central server. Let it know the condition of the work
66
+ # thread, the action and status we're processing, and our hostname and PID.
67
+ def check_in(thread_status)
68
+ keep_trying_to "check in with central" do
69
+ @server["/worker"].put({
70
+ :name => @name,
71
+ :thread_status => thread_status
72
+ })
73
+ end
74
+ end
75
+
76
+ # Inform the central server that this worker is finished. This is the only
77
+ # remote method that doesn't retry on connection errors -- if the worker
78
+ # can't connect to the central server while it's trying to shutdown, it
79
+ # should close, regardless.
80
+ def check_out
81
+ @server["/worker"].put({
82
+ :name => @name,
83
+ :terminated => true
84
+ })
85
+ log 'exiting'
86
+ end
87
+
88
+ # We expect and require internal communication between the central server
89
+ # and the workers to succeed. If it fails for any reason, log it, and then
90
+ # keep trying the same request.
91
+ def keep_trying_to(title)
92
+ begin
93
+ yield
94
+ rescue Exception => e
95
+ log "failed to #{title} -- retry in #{RETRY_WAIT} seconds"
96
+ log e.message
97
+ log e.backtrace
98
+ sleep RETRY_WAIT
99
+ retry
100
+ end
101
+ end
102
+
103
+ # Does this Worker have a job to do?
104
+ def has_work?
105
+ @action_name && @input && @options
106
+ end
107
+
108
+ # Loggable string of the current work unit.
109
+ def display_work_unit
110
+ "unit ##{@options['work_unit_id']} (#{@action_name})"
111
+ end
112
+
113
+ # Executes the current work unit, catching all exceptions as failures.
114
+ def run_work_unit
115
+ begin
116
+ result = nil
117
+ @action = CloudCrowd.actions[@action_name].new(@status, @input, @options, @store)
118
+ Dir.chdir(@action.work_directory) do
119
+ result = case @status
120
+ when PROCESSING then @action.process
121
+ when SPLITTING then @action.split
122
+ when MERGING then @action.merge
123
+ else raise Error::StatusUnspecified, "work units must specify their status"
124
+ end
125
+ end
126
+ complete_work_unit({'output' => result}.to_json)
127
+ rescue Exception => e
128
+ fail_work_unit(e)
129
+ end
130
+ end
131
+
132
+ # Wraps <tt>run_work_unit</tt> to benchmark the execution time, if requested.
133
+ def run
134
+ return run_work_unit unless @options['benchmark']
135
+ status = CloudCrowd.display_status(@status)
136
+ log("ran #{@action_name}/#{status} in " + Benchmark.measure { run_work_unit }.to_s)
137
+ end
138
+
139
+
140
+ private
141
+
142
+ # Common parameters to send back to central.
143
+ def base_params
144
+ @base_params ||= {
145
+ :worker_name => @name,
146
+ :worker_actions => @enabled_actions.join(',')
147
+ }
148
+ end
149
+
150
+ # Common parameters to send back to central upon unit completion,
151
+ # regardless of success or failure.
152
+ def completion_params
153
+ base_params.merge({
154
+ :id => @options['work_unit_id'],
155
+ :time => Time.now - @start_time
156
+ })
157
+ end
158
+
159
+ # Extract our instance variables from a WorkUnit's JSON.
160
+ def setup_work_unit(unit_json)
161
+ return false unless unit_json
162
+ unit = JSON.parse(unit_json)
163
+ @start_time = Time.now
164
+ @action_name, @input, @options, @status = unit['action'], unit['input'], unit['options'], unit['status']
165
+ @options['job_id'] = unit['job_id']
166
+ @options['work_unit_id'] = unit['id']
167
+ @options['attempts'] ||= unit['attempts']
168
+ log "fetched #{display_work_unit}"
169
+ return true
170
+ end
171
+
172
+ # Log a message to the daemon log. Includes PID for identification.
173
+ def log(message)
174
+ puts "Worker ##{@id}: #{message}" unless ENV['RACK_ENV'] == 'test'
175
+ end
176
+
177
+ # When we're done with a unit, clear out our instance variables to make way
178
+ # for the next one. Also, remove all of the unit's temporary storage.
179
+ def clear_work_unit
180
+ @action.cleanup_work_directory
181
+ @action, @action_name, @input, @options, @start_time = nil, nil, nil, nil, nil
182
+ end
183
+
184
+ end
185
+
186
+ end
@@ -0,0 +1,221 @@
1
+ body {
2
+ background: #979797;
3
+ font-family: Arial;
4
+ font-size: 12px;
5
+ color: #252525;
6
+ }
7
+
8
+ .small_caps {
9
+ font: 11px Arial, sans-serif;
10
+ text-transform: uppercase;
11
+ }
12
+
13
+ #header {
14
+ height: 110px;
15
+ position: absolute;
16
+ top: 0; left: 0; right: 0;
17
+ background: url(/images/header_back.png);
18
+ }
19
+ #logo {
20
+ position: absolute;
21
+ left: 37px; top: 9px;
22
+ width: 236px; height: 91px;
23
+ background: url(/images/logo.png);
24
+ }
25
+
26
+ #disconnected {
27
+ position: absolute;
28
+ top: 122px; right: 15px;
29
+ background: #7f7f7f;
30
+ color: #333;
31
+ border: 1px solid #555;
32
+ font-size: 10px;
33
+ line-height: 18px;
34
+ padding: 3px 4px 1px 4px;
35
+ -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px;
36
+ }
37
+ #disconnected .server_error {
38
+ float: left;
39
+ width: 16px; height: 16px;
40
+ background: url(/images/server_error.png);
41
+ opacity: 0.7;
42
+ margin-right: 3px;
43
+ }
44
+
45
+ #queue {
46
+ position: absolute;
47
+ top: 16px; left: 327px; right: 15px;
48
+ height: 77px;
49
+ overflow: hidden;
50
+ }
51
+ #no_jobs {
52
+ text-align: left;
53
+ position: absolute;
54
+ bottom: 8px; right: 8px;
55
+ color: #999;
56
+ display: none;
57
+ }
58
+ #queue.no_jobs #no_jobs {
59
+ display: block;
60
+ }
61
+ #queue_fill {
62
+ position: absolute;
63
+ left: 0; right: 0; top: 0;
64
+ height: 75px;
65
+ border: 1px solid #5c5c5c;
66
+ -moz-border-radius: 10px; -webkit-border-radius: 10px; border-radius: 10px;
67
+ background: transparent url(/images/queue_fill.png) repeat-x 0px -1px;
68
+ }
69
+ #queue.no_jobs #queue_fill {
70
+ opacity: 0.3;
71
+ }
72
+ #queue .job {
73
+ position: relative;
74
+ margin-top: 1px;
75
+ height: 75px;
76
+ background: blue;
77
+ float: left;
78
+ overflow: hidden;
79
+ -moz-border-radius: 10px;
80
+ -webkit-border-radius: 10px;
81
+ }
82
+ #queue .completion {
83
+ position: absolute;
84
+ bottom: -1px;
85
+ height: 30px;
86
+ background: black;
87
+ border: 1px solid white;
88
+ -moz-border-radius: 10px; -webkit-border-radius: 10px;
89
+ opacity: 0.5;
90
+ overflow: hidden;
91
+ }
92
+ #queue .completion.zero {
93
+ border: 0;
94
+ }
95
+ #queue .percent_complete {
96
+ position: absolute;
97
+ bottom: 8px; left: 8px;
98
+ color: #c7c7c7;
99
+ z-index: 10;
100
+ }
101
+ #queue .job_id {
102
+ color: #333;
103
+ font-size: 14px;
104
+ position: absolute;
105
+ top: 8px; left: 8px;
106
+ z-index: 10;
107
+ }
108
+
109
+ #sidebar {
110
+ position: absolute;
111
+ top: 120px; left: 10px; bottom: 10px;
112
+ width: 300px;
113
+ overflow: hidden;
114
+ }
115
+ #sidebar_background {
116
+ position: absolute;
117
+ top: 21px; bottom: 21px;
118
+ width: 298px;
119
+ background: #e0e0e0;
120
+ border: 1px solid #8b8b8b;
121
+ border-top: 0; border-bottom: 0;
122
+ }
123
+ .sidebar_back {
124
+ position: absolute;
125
+ height: 21px; width: 300px;
126
+ }
127
+ #sidebar_top {
128
+ top: 0px;
129
+ background: url(/images/sidebar_top.png);
130
+ }
131
+ #sidebar_bottom {
132
+ bottom: 0px;
133
+ background: url(/images/sidebar_bottom.png);
134
+ }
135
+ #sidebar_header {
136
+ position: absolute;
137
+ top: 5px; left: 8px;
138
+ color: #404040;
139
+ text-shadow: 0px 1px 1px #eee;
140
+ }
141
+ #sidebar_header.no_workers .no_workers,
142
+ #sidebar_header .has_workers {
143
+ display: block;
144
+ }
145
+ #sidebar_header .no_workers,
146
+ #sidebar_header.no_workers .has_workers {
147
+ display: none;
148
+ }
149
+ #workers {
150
+ position: absolute;
151
+ padding: 2px 0;
152
+ top: 21px; left: 0; bottom: 21px;
153
+ width: 298px;
154
+ overflow-y: auto; overflow-x: hidden;
155
+ }
156
+ #workers .worker {
157
+ border: 1px solid transparent;
158
+ margin: 1px 7px;
159
+ padding-left: 18px;
160
+ font-size: 11px;
161
+ line-height: 22px;
162
+ background: url(/images/bullet_white.png) no-repeat left center;
163
+ cursor: pointer;
164
+ }
165
+ #workers .worker.processing,
166
+ #workers .worker.splitting,
167
+ #workers .worker.merging {
168
+ background: url(/images/bullet_green.png) no-repeat left center;
169
+ }
170
+ #workers .worker:hover {
171
+ border: 1px solid #aaa;
172
+ border-radius: 4px; -moz-border-radius: 4px; -webkit-border-radius: 4px;
173
+ background-color: #ccc;
174
+ }
175
+
176
+ #worker_info {
177
+ position: absolute;
178
+ width: 231px; height: 79px;
179
+ margin: -9px 0 0 -20px;
180
+ background: url(/images/worker_info.png);
181
+ overflow: hidden;
182
+ cursor: pointer;
183
+ }
184
+ #worker_info_inner {
185
+ margin: 15px 15px 15px 32px;
186
+ line-height: 15px;
187
+ color: #333;
188
+ text-shadow: 0px 1px 1px #eee;
189
+ }
190
+ #worker_info.loading #worker_info_inner {
191
+ background: url(/images/worker_info_loading.gif) no-repeat right bottom;
192
+ width: 45px; height: 9px;
193
+ }
194
+ #worker_info.awake #worker_details,
195
+ #worker_sleeping {
196
+ display: block;
197
+ }
198
+ #worker_details, #worker_info.loading #worker_details,
199
+ #worker_info.loading #worker_sleeping, #worker_info.awake #worker_sleeping {
200
+ display: none;
201
+ }
202
+
203
+ #graphs {
204
+ position: absolute;
205
+ padding: 17px 15px 15px 17px;
206
+ top: 110px; left: 310px; right: 0px; bottom: 0;
207
+ overflow: hidden;
208
+ overflow-y: auto;
209
+ }
210
+ .graph_container {
211
+ margin-bottom: 25px;
212
+ }
213
+ .graph_title {
214
+ color: #333;
215
+ font-size: 16px;
216
+ text-shadow: 0px 1px 1px #eee;
217
+ margin-bottom: 10px;
218
+ }
219
+ .graph {
220
+ height: 150px;
221
+ }