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.
- data/EPIGRAPHS +17 -0
- data/LICENSE +22 -0
- data/README +93 -0
- data/actions/graphics_magick.rb +43 -0
- data/actions/process_pdfs.rb +92 -0
- data/actions/word_count.rb +14 -0
- data/bin/crowd +5 -0
- data/cloud-crowd.gemspec +111 -0
- data/config/config.example.ru +17 -0
- data/config/config.example.yml +48 -0
- data/config/database.example.yml +9 -0
- data/examples/graphics_magick_example.rb +44 -0
- data/examples/process_pdfs_example.rb +40 -0
- data/examples/word_count_example.rb +41 -0
- data/lib/cloud-crowd.rb +130 -0
- data/lib/cloud_crowd/action.rb +101 -0
- data/lib/cloud_crowd/app.rb +117 -0
- data/lib/cloud_crowd/asset_store.rb +41 -0
- data/lib/cloud_crowd/asset_store/filesystem_store.rb +28 -0
- data/lib/cloud_crowd/asset_store/s3_store.rb +40 -0
- data/lib/cloud_crowd/command_line.rb +209 -0
- data/lib/cloud_crowd/daemon.rb +95 -0
- data/lib/cloud_crowd/exceptions.rb +28 -0
- data/lib/cloud_crowd/helpers.rb +8 -0
- data/lib/cloud_crowd/helpers/authorization.rb +50 -0
- data/lib/cloud_crowd/helpers/resources.rb +45 -0
- data/lib/cloud_crowd/inflector.rb +19 -0
- data/lib/cloud_crowd/models.rb +40 -0
- data/lib/cloud_crowd/models/job.rb +176 -0
- data/lib/cloud_crowd/models/work_unit.rb +89 -0
- data/lib/cloud_crowd/models/worker_record.rb +61 -0
- data/lib/cloud_crowd/runner.rb +15 -0
- data/lib/cloud_crowd/schema.rb +45 -0
- data/lib/cloud_crowd/worker.rb +186 -0
- data/public/css/admin_console.css +221 -0
- data/public/css/reset.css +42 -0
- data/public/images/bullet_green.png +0 -0
- data/public/images/bullet_white.png +0 -0
- data/public/images/cloud_hand.png +0 -0
- data/public/images/header_back.png +0 -0
- data/public/images/logo.png +0 -0
- data/public/images/queue_fill.png +0 -0
- data/public/images/server_error.png +0 -0
- data/public/images/sidebar_bottom.png +0 -0
- data/public/images/sidebar_top.png +0 -0
- data/public/images/worker_info.png +0 -0
- data/public/images/worker_info_loading.gif +0 -0
- data/public/js/admin_console.js +168 -0
- data/public/js/excanvas.js +1 -0
- data/public/js/flot.js +1 -0
- data/public/js/jquery.js +19 -0
- data/test/acceptance/test_app.rb +72 -0
- data/test/acceptance/test_failing_work_units.rb +32 -0
- data/test/acceptance/test_word_count.rb +49 -0
- data/test/blueprints.rb +17 -0
- data/test/config/actions/failure_testing.rb +13 -0
- data/test/config/config.ru +17 -0
- data/test/config/config.yml +7 -0
- data/test/config/database.yml +6 -0
- data/test/test_helper.rb +19 -0
- data/test/unit/test_action.rb +49 -0
- data/test/unit/test_configuration.rb +28 -0
- data/test/unit/test_job.rb +78 -0
- data/test/unit/test_work_unit.rb +55 -0
- data/views/index.erb +77 -0
- metadata +233 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby -rubygems
|
2
|
+
|
3
|
+
require 'restclient'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# This example demonstrates the GraphicsMagick action by taking in a list of
|
7
|
+
# five images, and producing annotated, blurred, and black and white versions
|
8
|
+
# of each image. See actions/graphics_magick.rb
|
9
|
+
|
10
|
+
RestClient.post('http://localhost:9173/jobs',
|
11
|
+
{:job => {
|
12
|
+
|
13
|
+
'action' => 'graphics_magick',
|
14
|
+
|
15
|
+
'inputs' => [
|
16
|
+
'http://www.sci-fi-o-rama.com/wp-content/uploads/2008/10/dan_mcpharlin_the_land_of_sleeping_things.jpg',
|
17
|
+
'http://www.sci-fi-o-rama.com/wp-content/uploads/2009/07/dan_mcpharlin_wired_spread01.jpg',
|
18
|
+
'http://www.sci-fi-o-rama.com/wp-content/uploads/2009/07/dan_mcpharlin_wired_spread03.jpg',
|
19
|
+
'http://www.sci-fi-o-rama.com/wp-content/uploads/2009/07/dan_mcpharlin_wired_spread02.jpg',
|
20
|
+
'http://www.sci-fi-o-rama.com/wp-content/uploads/2009/02/dan_mcpharlin_untitled.jpg'
|
21
|
+
],
|
22
|
+
|
23
|
+
'options' => {
|
24
|
+
'steps' => [{
|
25
|
+
'name' => 'annotated',
|
26
|
+
'command' => 'convert',
|
27
|
+
'options' => '-font helvetica -fill red -draw "font-size 35; text 75,75 CloudCrowd!"',
|
28
|
+
'extension' => 'jpg'
|
29
|
+
},{
|
30
|
+
'name' => 'blurred',
|
31
|
+
'command' => 'convert',
|
32
|
+
'options' => '-blur 10x5',
|
33
|
+
'extension' => 'png'
|
34
|
+
},{
|
35
|
+
'name' => 'bw',
|
36
|
+
'input' => 'blurred',
|
37
|
+
'command' => 'convert',
|
38
|
+
'options' => '-monochrome',
|
39
|
+
'extension' => 'jpg'
|
40
|
+
}]
|
41
|
+
}
|
42
|
+
|
43
|
+
}.to_json}
|
44
|
+
)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby -rubygems
|
2
|
+
|
3
|
+
require 'restclient'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# This example demonstrates a fairly complicated PDF-processing action, designed
|
7
|
+
# to extract the PDF's text, and produce GIF versions of each page. The action
|
8
|
+
# (actions/process_pdfs.rb) shows an example of using all three steps,
|
9
|
+
# split, process, and merge.
|
10
|
+
|
11
|
+
RestClient.post('http://localhost:9173/jobs',
|
12
|
+
{:job => {
|
13
|
+
|
14
|
+
'action' => 'process_pdfs',
|
15
|
+
|
16
|
+
'inputs' => [
|
17
|
+
'http://tigger.uic.edu/~victor/personal/futurism.pdf',
|
18
|
+
'http://www.jonasmekas.com/Catalog_excerpt/The%20Avant-Garde%20From%20Futurism%20to%20Fluxus.pdf',
|
19
|
+
'http://www.dzignism.com/articles/Futurist.Manifesto.pdf',
|
20
|
+
'http://benfry.com/phd/dissertation-050312b-acrobat.pdf'
|
21
|
+
],
|
22
|
+
|
23
|
+
'options' => {
|
24
|
+
|
25
|
+
'batch_size' => 7,
|
26
|
+
|
27
|
+
'images' => [{
|
28
|
+
'name' => '700',
|
29
|
+
'options' => '-resize 700x -density 220 -depth 4 -unsharp 0.5x0.5+0.5+0.03',
|
30
|
+
'extension' => 'gif'
|
31
|
+
},{
|
32
|
+
'name' => '1000',
|
33
|
+
'options' => '-resize 1000x -density 220 -depth 4 -unsharp 0.5x0.5+0.5+0.03',
|
34
|
+
'extension' => 'gif'
|
35
|
+
}]
|
36
|
+
|
37
|
+
}
|
38
|
+
|
39
|
+
}.to_json}
|
40
|
+
)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby -rubygems
|
2
|
+
|
3
|
+
require 'restclient'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# Let's count all the words in Shakespeare.
|
7
|
+
|
8
|
+
RestClient.post('http://localhost:9173/jobs',
|
9
|
+
{:job => {
|
10
|
+
|
11
|
+
'action' => 'word_count',
|
12
|
+
|
13
|
+
'inputs' => [
|
14
|
+
'http://www.gutenberg.org/dirs/etext97/1ws3010.txt', # All's Well That Ends Well
|
15
|
+
'http://www.gutenberg.org/dirs/etext99/1ws3511.txt', # Anthony and Cleopatra
|
16
|
+
'http://www.gutenberg.org/dirs/etext97/1ws2510.txt', # As You Like It
|
17
|
+
'http://www.gutenberg.org/dirs/etext97/1ws0610.txt', # The Comedy of Errors
|
18
|
+
'http://www.gutenberg.org/dirs/etext99/1ws3911.txt', # Cymbeline
|
19
|
+
'http://www.gutenberg.org/dirs/etext00/0ws2610.txt', # Hamlet
|
20
|
+
'http://www.gutenberg.org/dirs/etext00/0ws1910.txt', # Henry IV
|
21
|
+
'http://www.gutenberg.org/dirs/etext99/1ws2411.txt', # Julius Caesar
|
22
|
+
'http://www.gutenberg.org/dirs/etext98/2ws3310.txt', # King Lear
|
23
|
+
'http://www.gutenberg.org/dirs/etext99/1ws1211j.txt', # Love's Labour's Lost
|
24
|
+
'http://www.gutenberg.org/dirs/etext98/2ws3410.txt', # Macbeth
|
25
|
+
'http://www.gutenberg.org/dirs/etext98/2ws1810.txt', # The Merchant of Venice
|
26
|
+
'http://www.gutenberg.org/dirs/etext99/1ws1711.txt', # Midsummer Night's Dream
|
27
|
+
'http://www.gutenberg.org/dirs/etext98/3ws2210.txt', # Much Ado About Nothing
|
28
|
+
'http://www.gutenberg.org/dirs/etext00/0ws3210.txt', # Othello
|
29
|
+
'http://www.gutenberg.org/dirs/etext98/2ws1610.txt', # Romeo and Juliet
|
30
|
+
'http://www.gutenberg.org/dirs/etext98/2ws1010.txt', # The Taming of the Shrew
|
31
|
+
'http://www.gutenberg.org/dirs/etext99/1ws4111.txt', # The Tempest
|
32
|
+
'http://www.gutenberg.org/dirs/etext00/0ws0910.txt', # Titus Andronicus
|
33
|
+
'http://www.gutenberg.org/dirs/etext99/1ws2911.txt', # Troilus and Cressida
|
34
|
+
'http://www.gutenberg.org/dirs/etext98/3ws2810.txt', # Twelfth Night
|
35
|
+
'http://www.gutenberg.org/files/1539/1539.txt' # The Winter's Tale
|
36
|
+
]
|
37
|
+
|
38
|
+
}.to_json}
|
39
|
+
)
|
40
|
+
|
41
|
+
# With 23 Workers running, and over Wifi, it counted all the words in 5.5 secs.
|
data/lib/cloud-crowd.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
# The Grand Central of code loading...
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
|
4
|
+
|
5
|
+
# Common Gems:
|
6
|
+
require 'rubygems'
|
7
|
+
gem 'activerecord'
|
8
|
+
gem 'daemons'
|
9
|
+
gem 'json'
|
10
|
+
gem 'rest-client'
|
11
|
+
gem 'right_aws'
|
12
|
+
gem 'sinatra'
|
13
|
+
|
14
|
+
# Autoloading for all the pieces which may or may not be needed:
|
15
|
+
autoload :ActiveRecord, 'activerecord'
|
16
|
+
autoload :Benchmark, 'benchmark'
|
17
|
+
autoload :Daemons, 'daemons'
|
18
|
+
autoload :Digest, 'digest'
|
19
|
+
autoload :ERB, 'erb'
|
20
|
+
autoload :FileUtils, 'fileutils'
|
21
|
+
autoload :JSON, 'json'
|
22
|
+
autoload :RestClient, 'restclient'
|
23
|
+
autoload :RightAws, 'right_aws'
|
24
|
+
autoload :Sinatra, 'sinatra'
|
25
|
+
autoload :Socket, 'socket'
|
26
|
+
autoload :YAML, 'yaml'
|
27
|
+
|
28
|
+
# Common code which should really be required in every circumstance.
|
29
|
+
require 'cloud_crowd/exceptions'
|
30
|
+
|
31
|
+
module CloudCrowd
|
32
|
+
|
33
|
+
# Autoload all the CloudCrowd classes which may not be required.
|
34
|
+
autoload :App, 'cloud_crowd/app'
|
35
|
+
autoload :Action, 'cloud_crowd/action'
|
36
|
+
autoload :AssetStore, 'cloud_crowd/asset_store'
|
37
|
+
autoload :Helpers, 'cloud_crowd/helpers'
|
38
|
+
autoload :Inflector, 'cloud_crowd/inflector'
|
39
|
+
autoload :Job, 'cloud_crowd/models'
|
40
|
+
autoload :Worker, 'cloud_crowd/worker'
|
41
|
+
autoload :WorkUnit, 'cloud_crowd/models'
|
42
|
+
autoload :WorkerRecord, 'cloud_crowd/models'
|
43
|
+
|
44
|
+
# Root directory of the CloudCrowd gem.
|
45
|
+
ROOT = File.expand_path(File.dirname(__FILE__) + '/..')
|
46
|
+
|
47
|
+
# Keep the version in sync with the gemspec.
|
48
|
+
VERSION = '0.1.0'
|
49
|
+
|
50
|
+
# A Job is processing if its WorkUnits in the queue to be handled by workers.
|
51
|
+
PROCESSING = 1
|
52
|
+
|
53
|
+
# A Job has succeeded if all of its WorkUnits have finished successfully.
|
54
|
+
SUCCEEDED = 2
|
55
|
+
|
56
|
+
# A Job has failed if even a single one of its WorkUnits has failed (they may
|
57
|
+
# be attempted multiple times on failure, however).
|
58
|
+
FAILED = 3
|
59
|
+
|
60
|
+
# A Job is splitting if it's in the process of dividing its inputs up into
|
61
|
+
# multiple WorkUnits.
|
62
|
+
SPLITTING = 4
|
63
|
+
|
64
|
+
# A Job is merging if it's busy collecting all of its successful WorkUnits
|
65
|
+
# back together into the final result.
|
66
|
+
MERGING = 5
|
67
|
+
|
68
|
+
# A work unit is considered to be complete if it succeeded or if it failed.
|
69
|
+
COMPLETE = [SUCCEEDED, FAILED]
|
70
|
+
|
71
|
+
# A work unit is considered incomplete if it's being processed, split up or
|
72
|
+
# merged together.
|
73
|
+
INCOMPLETE = [PROCESSING, SPLITTING, MERGING]
|
74
|
+
|
75
|
+
# Mapping of statuses to their display strings.
|
76
|
+
DISPLAY_STATUS_MAP = ['unknown', 'processing', 'succeeded', 'failed', 'splitting', 'merging']
|
77
|
+
|
78
|
+
class << self
|
79
|
+
attr_reader :config
|
80
|
+
|
81
|
+
# Configure CloudCrowd by passing in the path to <tt>config.yml</tt>.
|
82
|
+
def configure(config_path)
|
83
|
+
@config_path = File.expand_path(File.dirname(config_path))
|
84
|
+
@config = YAML.load_file(config_path)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Configure the CloudCrowd central database (and connect to it), by passing
|
88
|
+
# in a path to <tt>database.yml</tt>. The file should use the standard
|
89
|
+
# ActiveRecord connection format.
|
90
|
+
def configure_database(config_path)
|
91
|
+
configuration = YAML.load_file(config_path)
|
92
|
+
ActiveRecord::Base.establish_connection(configuration)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Get a reference to the central server, including authentication,
|
96
|
+
# if configured.
|
97
|
+
def central_server
|
98
|
+
return @central_server if @central_server
|
99
|
+
params = [CloudCrowd.config[:central_server]]
|
100
|
+
params += [CloudCrowd.config[:login], CloudCrowd.config[:password]] if CloudCrowd.config[:use_http_authentication]
|
101
|
+
@central_server = RestClient::Resource.new(*params)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Return the displayable status name of an internal CloudCrowd status number.
|
105
|
+
# (See the above constants).
|
106
|
+
def display_status(status)
|
107
|
+
DISPLAY_STATUS_MAP[status] || 'unknown'
|
108
|
+
end
|
109
|
+
|
110
|
+
# CloudCrowd::Actions are requested dynamically by name. Access them through
|
111
|
+
# this actions property, which behaves like a hash. At load time, we
|
112
|
+
# load all installed Actions and CloudCrowd's default Actions into it.
|
113
|
+
# If you wish to have certain workers be specialized to only handle certain
|
114
|
+
# Actions, then install only those into the actions directory.
|
115
|
+
def actions
|
116
|
+
return @actions if @actions
|
117
|
+
@actions = {}
|
118
|
+
default_actions = Dir["#{ROOT}/actions/*.rb"]
|
119
|
+
installed_actions = Dir["#{@config_path}/actions/*.rb"]
|
120
|
+
custom_actions = Dir["#{CloudCrowd.config[:actions_path]}/*.rb"]
|
121
|
+
(default_actions + installed_actions + custom_actions).each do |path|
|
122
|
+
name = File.basename(path, File.extname(path))
|
123
|
+
require path
|
124
|
+
@actions[name] = Module.const_get(Inflector.camelize(name))
|
125
|
+
end
|
126
|
+
@actions
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module CloudCrowd
|
2
|
+
|
3
|
+
# As you write your custom actions, have them inherit from CloudCrowd::Action.
|
4
|
+
# All actions must implement a +process+ method, which should return a
|
5
|
+
# JSON-serializable object that will be used as the output for the work unit.
|
6
|
+
# See the default actions for examples.
|
7
|
+
#
|
8
|
+
# Optionally, actions may define +split+ and +merge+ methods to do mapping
|
9
|
+
# and reducing around the +input+. +split+ should return an array of URLs --
|
10
|
+
# to be mapped into WorkUnits and processed in parallel. In the +merge+ step,
|
11
|
+
# +input+ will be an array of all the resulting outputs from calling process.
|
12
|
+
#
|
13
|
+
# All actions have use of an individual +work_directory+, for scratch files,
|
14
|
+
# and spend their duration inside of it, so relative paths work well.
|
15
|
+
class Action
|
16
|
+
|
17
|
+
FILE_URL = /\Afile:\/\//
|
18
|
+
|
19
|
+
attr_reader :input, :input_path, :file_name, :options, :work_directory
|
20
|
+
|
21
|
+
# Initializing an Action sets up all of the read-only variables that
|
22
|
+
# form the bulk of the API for action subclasses. (Paths to read from and
|
23
|
+
# write to). It creates the +work_directory+ and moves into it.
|
24
|
+
# If we're not merging multiple results, it downloads the input file into
|
25
|
+
# the +work_directory+ before starting.
|
26
|
+
def initialize(status, input, options, store)
|
27
|
+
@input, @options, @store = input, options, store
|
28
|
+
@job_id, @work_unit_id = options['job_id'], options['work_unit_id']
|
29
|
+
@work_directory = File.expand_path(File.join(@store.temp_storage_path, storage_prefix))
|
30
|
+
FileUtils.mkdir_p(@work_directory) unless File.exists?(@work_directory)
|
31
|
+
status == MERGING ? parse_input : download_input
|
32
|
+
end
|
33
|
+
|
34
|
+
# Each Action subclass must implement a +process+ method, overriding this.
|
35
|
+
def process
|
36
|
+
raise NotImplementedError.new("CloudCrowd::Actions must override 'process' with their own processing code.")
|
37
|
+
end
|
38
|
+
|
39
|
+
# Download a file to the specified path.
|
40
|
+
def download(url, path)
|
41
|
+
if url.match(FILE_URL)
|
42
|
+
FileUtils.cp(url.sub(FILE_URL, ''), path)
|
43
|
+
else
|
44
|
+
resp = RestClient::Request.execute(:url => url, :method => :get, :raw_response => true)
|
45
|
+
FileUtils.mv resp.file.path, path
|
46
|
+
end
|
47
|
+
path
|
48
|
+
end
|
49
|
+
|
50
|
+
# Takes a local filesystem path, saves the file to S3, and returns the
|
51
|
+
# public (or authenticated) url on S3 where the file can be accessed.
|
52
|
+
def save(file_path)
|
53
|
+
save_path = File.join(storage_prefix, File.basename(file_path))
|
54
|
+
@store.save(file_path, save_path)
|
55
|
+
end
|
56
|
+
|
57
|
+
# After the Action has finished, we remove the work directory and return
|
58
|
+
# to the root directory (where daemons run by default).
|
59
|
+
def cleanup_work_directory
|
60
|
+
FileUtils.rm_r(@work_directory) if File.exists?(@work_directory)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# Convert an unsafe URL into a filesystem-friendly filename.
|
67
|
+
def safe_filename(url)
|
68
|
+
ext = File.extname(url)
|
69
|
+
name = URI.unescape(File.basename(url)).gsub(/[^a-zA-Z0-9_\-.]/, '-').gsub(/-+/, '-')
|
70
|
+
File.basename(name, ext).gsub('.', '-') + ext
|
71
|
+
end
|
72
|
+
|
73
|
+
# The directory prefix to use for both local and S3 storage.
|
74
|
+
# [action_name]/job_[job_id]/unit_[work_unit_it]
|
75
|
+
def storage_prefix
|
76
|
+
path_parts = []
|
77
|
+
path_parts << Inflector.underscore(self.class)
|
78
|
+
path_parts << "job_#{@job_id}"
|
79
|
+
path_parts << "unit_#{@work_unit_id}" if @work_unit_id
|
80
|
+
@storage_prefix ||= File.join(path_parts)
|
81
|
+
end
|
82
|
+
|
83
|
+
# If we know that the input is JSON, replace it with the parsed form.
|
84
|
+
def parse_input
|
85
|
+
@input = JSON.parse(@input)
|
86
|
+
end
|
87
|
+
|
88
|
+
# If the input is a URL, download the file before beginning processing.
|
89
|
+
def download_input
|
90
|
+
Dir.chdir(@work_directory) do
|
91
|
+
input_is_url = !!URI.parse(@input) rescue false
|
92
|
+
return unless input_is_url
|
93
|
+
@input_path = File.join(@work_directory, safe_filename(@input))
|
94
|
+
@file_name = File.basename(@input_path, File.extname(@input_path))
|
95
|
+
download(@input, @input_path)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module CloudCrowd
|
2
|
+
|
3
|
+
# The main CloudCrowd (Sinatra) application. The actions are:
|
4
|
+
#
|
5
|
+
# == Admin
|
6
|
+
# [get /] Render the admin console, with a progress meter for running jobs.
|
7
|
+
# [get /status] Get the combined JSON of every active job and worker.
|
8
|
+
# [get /heartbeat] Returns 200 OK to let monitoring tools know the server's up.
|
9
|
+
#
|
10
|
+
# == Public API
|
11
|
+
# [post /jobs] Begin a new Job. Post with a JSON representation of the job-to-be. (see examples).
|
12
|
+
# [get /jobs/:job_id] Check the status of a Job. Response includes output, if the Job has finished.
|
13
|
+
# [delete /jobs/:job_id] Clean up a Job when you're done downloading the results. Removes all intermediate files.
|
14
|
+
#
|
15
|
+
# == Internal Workers API
|
16
|
+
# [post /work] Dequeue the next WorkUnit, and hand it off to the worker.
|
17
|
+
# [put /work/:unit_id] Mark a finished WorkUnit as completed or failed, with results.
|
18
|
+
# [put /worker] Keep a record of an actively running worker.
|
19
|
+
class App < Sinatra::Default
|
20
|
+
|
21
|
+
set :root, ROOT
|
22
|
+
set :authorization_realm, "CloudCrowd"
|
23
|
+
|
24
|
+
helpers Helpers
|
25
|
+
|
26
|
+
# static serves files from /public, methodoverride allows the _method param.
|
27
|
+
enable :static, :methodoverride
|
28
|
+
|
29
|
+
# Enabling HTTP Authentication turns it on for all requests.
|
30
|
+
before do
|
31
|
+
login_required if CloudCrowd.config[:use_http_authentication]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Render the admin console.
|
35
|
+
get '/' do
|
36
|
+
erb :index
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get the JSON for every active job in the queue and every active worker
|
40
|
+
# in the system. This action may get a little worrisome as the system grows
|
41
|
+
# larger -- keep it in mind.
|
42
|
+
get '/status' do
|
43
|
+
json(
|
44
|
+
'jobs' => Job.incomplete,
|
45
|
+
'workers' => WorkerRecord.alive(:order => 'name desc'),
|
46
|
+
'work_unit_count' => WorkUnit.incomplete.count
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Get the JSON for a worker record's work unit, if one exists.
|
51
|
+
get '/worker/:name' do
|
52
|
+
record = WorkerRecord.find_by_name params[:name]
|
53
|
+
json((record && record.work_unit) || {})
|
54
|
+
end
|
55
|
+
|
56
|
+
# To monitor the central server with Monit, God, Nagios, or another
|
57
|
+
# monitoring tool, you can hit /heartbeat to make sure.
|
58
|
+
get '/heartbeat' do
|
59
|
+
"buh-bump"
|
60
|
+
end
|
61
|
+
|
62
|
+
# PUBLIC API:
|
63
|
+
|
64
|
+
# Start a new job. Accepts a JSON representation of the job-to-be.
|
65
|
+
post '/jobs' do
|
66
|
+
json Job.create_from_request(JSON.parse(params[:job]))
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check the status of a job, returning the output if finished, and the
|
70
|
+
# number of work units remaining otherwise.
|
71
|
+
get '/jobs/:job_id' do
|
72
|
+
json current_job
|
73
|
+
end
|
74
|
+
|
75
|
+
# Cleans up a Job's saved S3 files. Delete a Job after you're done
|
76
|
+
# downloading the results.
|
77
|
+
delete '/jobs/:job_id' do
|
78
|
+
current_job.destroy
|
79
|
+
json nil
|
80
|
+
end
|
81
|
+
|
82
|
+
# INTERNAL WORKER DAEMON API:
|
83
|
+
|
84
|
+
# Internal method for worker daemons to fetch the work unit at the front
|
85
|
+
# of the queue. Work unit is marked as taken and handed off to the worker.
|
86
|
+
post '/work' do
|
87
|
+
json dequeue_work_unit
|
88
|
+
end
|
89
|
+
|
90
|
+
# When workers are done with their unit, either successfully on in failure,
|
91
|
+
# they mark it back on the central server and retrieve another. Failures
|
92
|
+
# pull from one down in the queue, so as to not repeat the same unit.
|
93
|
+
put '/work/:work_unit_id' do
|
94
|
+
handle_conflicts(409) do
|
95
|
+
case params[:status]
|
96
|
+
when 'succeeded'
|
97
|
+
current_work_unit.finish(params[:output], params[:time])
|
98
|
+
json dequeue_work_unit
|
99
|
+
when 'failed'
|
100
|
+
current_work_unit.fail(params[:output], params[:time])
|
101
|
+
json dequeue_work_unit(1)
|
102
|
+
else
|
103
|
+
error(500, "Completing a work unit must specify status.")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Every so often workers check in to let the central server know that
|
109
|
+
# they're still alive. Keep up-to-date records
|
110
|
+
put '/worker' do
|
111
|
+
params[:terminated] ? WorkerRecord.check_out(params) : WorkerRecord.check_in(params)
|
112
|
+
json nil
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|