cloud-crowd 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|