cloud-crowd 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,9 @@
1
+ # This is a standard ActiveRecord database.yml file. You can configure it
2
+ # to use any database that ActiveRecord supports.
3
+
4
+ :adapter: mysql
5
+ :encoding: utf8
6
+ :username: root
7
+ :password:
8
+ :socket: /tmp/mysql.sock
9
+ :database: cloud_crowd
@@ -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.
@@ -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