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,50 @@
|
|
1
|
+
module CloudCrowd
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
# Authorization takes after sinatra-authorization... See
|
5
|
+
# http://github.com/integrity/sinatra-authorization
|
6
|
+
# for the original.
|
7
|
+
module Authorization
|
8
|
+
|
9
|
+
# Ensure that the request includes the correct credentials.
|
10
|
+
def login_required
|
11
|
+
return if authorized?
|
12
|
+
unauthorized! unless auth.provided?
|
13
|
+
bad_request! unless auth.basic?
|
14
|
+
unauthorized! unless authorize(*auth.credentials)
|
15
|
+
request.env['REMOTE_USER'] = auth.username
|
16
|
+
end
|
17
|
+
|
18
|
+
# Has the request been authenticated?
|
19
|
+
def authorized?
|
20
|
+
!!request.env['REMOTE_USER']
|
21
|
+
end
|
22
|
+
|
23
|
+
# A request is authorized if its login and password match those stored
|
24
|
+
# in config.yml, or if authentication is disabled. If authentication is
|
25
|
+
# turned on, then every request is authenticated, including between
|
26
|
+
# the worker daemons and the central server.
|
27
|
+
def authorize(login, password)
|
28
|
+
return true unless CloudCrowd.config[:use_http_authentication]
|
29
|
+
return CloudCrowd.config[:login] == login &&
|
30
|
+
CloudCrowd.config[:password] == password
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def auth
|
37
|
+
@auth ||= Rack::Auth::Basic::Request.new(request.env)
|
38
|
+
end
|
39
|
+
|
40
|
+
def unauthorized!(realm = App.authorization_realm)
|
41
|
+
response['WWW-Authenticate'] = "Basic realm=\"#{realm}\""
|
42
|
+
halt 401, 'Authorization Required'
|
43
|
+
end
|
44
|
+
|
45
|
+
def bad_request!
|
46
|
+
halt 400, 'Bad Request'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module CloudCrowd
|
2
|
+
module Helpers
|
3
|
+
module Resources
|
4
|
+
|
5
|
+
# Convenience method for responding with JSON. Sets the content-type,
|
6
|
+
# serializes, and allows empty responses.
|
7
|
+
def json(obj)
|
8
|
+
content_type :json
|
9
|
+
return status(204) && '' if obj.nil?
|
10
|
+
obj.to_json
|
11
|
+
end
|
12
|
+
|
13
|
+
# Lazy-fetch the job specified by <tt>job_id</tt>.
|
14
|
+
def current_job
|
15
|
+
@job ||= Job.find_by_id(params[:job_id]) or raise Sinatra::NotFound
|
16
|
+
end
|
17
|
+
|
18
|
+
# Lazy-fetch the WorkUnit specified by <tt>work_unit_id</tt>.
|
19
|
+
def current_work_unit
|
20
|
+
@work_unit ||= WorkUnit.find_by_id(params[:work_unit_id]) or raise Sinatra::NotFound
|
21
|
+
end
|
22
|
+
|
23
|
+
# Try to fetch a work unit from the queue. If none are pending, respond
|
24
|
+
# with no content.
|
25
|
+
def dequeue_work_unit(offset=0)
|
26
|
+
handle_conflicts do
|
27
|
+
worker, actions = params[:worker_name], params[:worker_actions].split(',')
|
28
|
+
WorkUnit.dequeue(worker, actions, offset)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# We're using ActiveRecords optimistic locking, so stale work units
|
33
|
+
# may sometimes arise. handle_conflicts responds with a the HTTP status
|
34
|
+
# code of your choosing if the update failed to be applied.
|
35
|
+
def handle_conflicts(code=204)
|
36
|
+
begin
|
37
|
+
yield
|
38
|
+
rescue ActiveRecord::StaleObjectError => e
|
39
|
+
return status(code) && ''
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module CloudCrowd
|
2
|
+
|
3
|
+
# Pilfered in parts from the ActiveSupport::Inflector.
|
4
|
+
module Inflector
|
5
|
+
|
6
|
+
def self.camelize(word)
|
7
|
+
word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.underscore(word)
|
11
|
+
word.to_s.gsub(/::/, '/').
|
12
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
13
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
14
|
+
tr("-", "_").
|
15
|
+
downcase
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module CloudCrowd
|
2
|
+
|
3
|
+
# Adds named scopes and query methods for every CloudCrowd status to
|
4
|
+
# both Jobs and WorkUnits.
|
5
|
+
module ModelStatus
|
6
|
+
|
7
|
+
def self.included(klass)
|
8
|
+
|
9
|
+
klass.class_eval do
|
10
|
+
# Note that COMPLETE and INCOMPLETE are unions of other states.
|
11
|
+
named_scope 'processing', :conditions => {:status => PROCESSING}
|
12
|
+
named_scope 'succeeded', :conditions => {:status => SUCCEEDED}
|
13
|
+
named_scope 'failed', :conditions => {:status => FAILED}
|
14
|
+
named_scope 'splitting', :conditions => {:status => SPLITTING}
|
15
|
+
named_scope 'merging', :conditions => {:status => MERGING}
|
16
|
+
named_scope 'complete', :conditions => {:status => COMPLETE}
|
17
|
+
named_scope 'incomplete', :conditions => {:status => INCOMPLETE}
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
def processing?; self.status == PROCESSING; end
|
23
|
+
def succeeded?; self.status == SUCCEEDED; end
|
24
|
+
def failed?; self.status == FAILED; end
|
25
|
+
def splitting?; self.status == SPLITTING; end
|
26
|
+
def merging?; self.status == MERGING; end
|
27
|
+
def complete?; COMPLETE.include?(self.status); end
|
28
|
+
def incomplete?; INCOMPLETE.include?(self.status); end
|
29
|
+
|
30
|
+
# Get the displayable status name of the model's status code.
|
31
|
+
def display_status
|
32
|
+
CloudCrowd.display_status(self.status)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
require 'cloud_crowd/models/job'
|
39
|
+
require 'cloud_crowd/models/work_unit'
|
40
|
+
require 'cloud_crowd/models/worker_record'
|
@@ -0,0 +1,176 @@
|
|
1
|
+
module CloudCrowd
|
2
|
+
|
3
|
+
# A chunk of work that will be farmed out into many WorkUnits to be processed
|
4
|
+
# in parallel by each active CloudCrowd::Worker. Jobs are defined by a list
|
5
|
+
# of inputs (usually public urls to files), an action (the name of a script that
|
6
|
+
# CloudCrowd knows how to run), and, eventually a corresponding list of output.
|
7
|
+
class Job < ActiveRecord::Base
|
8
|
+
include ModelStatus
|
9
|
+
|
10
|
+
has_many :work_units, :dependent => :destroy
|
11
|
+
|
12
|
+
validates_presence_of :status, :inputs, :action, :options
|
13
|
+
|
14
|
+
before_validation_on_create :set_initial_status
|
15
|
+
after_create :queue_for_workers
|
16
|
+
before_destroy :cleanup_assets
|
17
|
+
|
18
|
+
# Create a Job from an incoming JSON or XML request, and add it to the queue.
|
19
|
+
# TODO: Think about XML support.
|
20
|
+
def self.create_from_request(h)
|
21
|
+
self.create(
|
22
|
+
:inputs => h['inputs'].to_json,
|
23
|
+
:action => h['action'],
|
24
|
+
:options => (h['options'] || {}).to_json,
|
25
|
+
:email => h['email'],
|
26
|
+
:callback_url => h['callback_url']
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
# After work units are marked successful, we check to see if all of them have
|
31
|
+
# finished, if so, continue on to the next phase of the job.
|
32
|
+
def check_for_completion
|
33
|
+
return unless all_work_units_complete?
|
34
|
+
transition_to_next_phase
|
35
|
+
output_list = gather_outputs_from_work_units
|
36
|
+
|
37
|
+
if complete?
|
38
|
+
self.outputs = output_list.to_json
|
39
|
+
self.time = Time.now - self.created_at
|
40
|
+
end
|
41
|
+
self.save
|
42
|
+
|
43
|
+
case self.status
|
44
|
+
when PROCESSING then queue_for_workers(output_list.map {|o| JSON.parse(o) }.flatten)
|
45
|
+
when MERGING then queue_for_workers(output_list.to_json)
|
46
|
+
else fire_callback
|
47
|
+
end
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
# If a <tt>callback_url</tt> is defined, post the Job's JSON to it upon
|
52
|
+
# completion. The <tt>callback_url</tt> may include HTTP basic authentication,
|
53
|
+
# if you like:
|
54
|
+
# http://user:password@example.com/job_complete
|
55
|
+
def fire_callback
|
56
|
+
begin
|
57
|
+
RestClient.post(callback_url, {:job => self.to_json}) if callback_url
|
58
|
+
rescue RestClient::Exception => e
|
59
|
+
puts "Failed to fire job callback. Hmmm, what should happen here?"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Cleaning up after a job will remove all of its files from S3. Destroying
|
64
|
+
# a Job calls cleanup_assets first.
|
65
|
+
def cleanup_assets
|
66
|
+
AssetStore.new.cleanup(self)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Have all of the WorkUnits finished?
|
70
|
+
#--
|
71
|
+
# We could trade reads for writes here
|
72
|
+
# by keeping a completed_count on the Job itself.
|
73
|
+
#++
|
74
|
+
def all_work_units_complete?
|
75
|
+
self.work_units.incomplete.count <= 0
|
76
|
+
end
|
77
|
+
|
78
|
+
# Have any of the WorkUnits failed?
|
79
|
+
def any_work_units_failed?
|
80
|
+
self.work_units.failed.count > 0
|
81
|
+
end
|
82
|
+
|
83
|
+
# This job is splittable if its Action has a +split+ method.
|
84
|
+
def splittable?
|
85
|
+
self.action_class.public_instance_methods.include? 'split'
|
86
|
+
end
|
87
|
+
|
88
|
+
# This job is mergeable if its Action has a +merge+ method.
|
89
|
+
def mergeable?
|
90
|
+
self.processing? && self.action_class.public_instance_methods.include?('merge')
|
91
|
+
end
|
92
|
+
|
93
|
+
# Retrieve the class for this Job's Action.
|
94
|
+
def action_class
|
95
|
+
klass = CloudCrowd.actions[self.action]
|
96
|
+
return klass if klass
|
97
|
+
raise Error::ActionNotFound, "no action named: '#{self.action}' could be found"
|
98
|
+
end
|
99
|
+
|
100
|
+
# How complete is this Job?
|
101
|
+
def percent_complete
|
102
|
+
return 0 if splitting?
|
103
|
+
return 100 if complete?
|
104
|
+
return 99 if merging?
|
105
|
+
(work_units.complete.count / work_units.count.to_f * 100).round
|
106
|
+
end
|
107
|
+
|
108
|
+
# How long has this Job taken?
|
109
|
+
def time_taken
|
110
|
+
return self.time if self.time
|
111
|
+
Time.now - self.created_at
|
112
|
+
end
|
113
|
+
|
114
|
+
# Generate a stable 8-bit Hex color code, based on the Job's id.
|
115
|
+
def color
|
116
|
+
@color ||= Digest::MD5.hexdigest(self.id.to_s)[-7...-1]
|
117
|
+
end
|
118
|
+
|
119
|
+
# A JSON representation of this job includes the statuses of its component
|
120
|
+
# WorkUnits, as well as any completed outputs.
|
121
|
+
def to_json(opts={})
|
122
|
+
atts = {
|
123
|
+
'id' => id,
|
124
|
+
'color' => color,
|
125
|
+
'status' => display_status,
|
126
|
+
'percent_complete' => percent_complete,
|
127
|
+
'work_units' => work_units.count,
|
128
|
+
'time_taken' => time_taken
|
129
|
+
}
|
130
|
+
atts['outputs'] = JSON.parse(outputs) if outputs
|
131
|
+
atts['email'] = email if email
|
132
|
+
atts.to_json
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
# When the WorkUnits are all finished, gather all their outputs together
|
139
|
+
# before removing them from the database entirely.
|
140
|
+
def gather_outputs_from_work_units
|
141
|
+
units = self.work_units.complete
|
142
|
+
outs = self.work_units.complete.map {|u| JSON.parse(u.output)['output'] }
|
143
|
+
self.work_units.complete.destroy_all
|
144
|
+
outs
|
145
|
+
end
|
146
|
+
|
147
|
+
# Transition this Job's status to the appropriate next status.
|
148
|
+
def transition_to_next_phase
|
149
|
+
self.status = any_work_units_failed? ? FAILED :
|
150
|
+
self.splitting? ? PROCESSING :
|
151
|
+
self.mergeable? ? MERGING :
|
152
|
+
SUCCEEDED
|
153
|
+
end
|
154
|
+
|
155
|
+
# When starting a new job, or moving to a new stage, split up the inputs
|
156
|
+
# into WorkUnits, and queue them. Workers will start picking them up right
|
157
|
+
# away.
|
158
|
+
def queue_for_workers(input=nil)
|
159
|
+
input ||= JSON.parse(self.inputs)
|
160
|
+
[input].flatten.each do |wu_input|
|
161
|
+
WorkUnit.create(
|
162
|
+
:job => self,
|
163
|
+
:action => self.action,
|
164
|
+
:input => wu_input,
|
165
|
+
:status => self.status
|
166
|
+
)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# A Job starts out either splitting or processing, depending on its action.
|
171
|
+
def set_initial_status
|
172
|
+
self.status = self.splittable? ? SPLITTING : PROCESSING
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module CloudCrowd
|
2
|
+
|
3
|
+
# A WorkUnit is an atomic chunk of work from a job, processing a single input
|
4
|
+
# through a single action. The WorkUnits are run in parallel, with each worker
|
5
|
+
# daemon processing one at a time. The splitting and merging stages of a job
|
6
|
+
# are each run as a single WorkUnit.
|
7
|
+
class WorkUnit < ActiveRecord::Base
|
8
|
+
include ModelStatus
|
9
|
+
|
10
|
+
belongs_to :job
|
11
|
+
belongs_to :worker_record
|
12
|
+
|
13
|
+
validates_presence_of :job_id, :status, :input, :action
|
14
|
+
|
15
|
+
after_save :check_for_job_completion
|
16
|
+
|
17
|
+
# Find the first available WorkUnit in the queue, and take it out.
|
18
|
+
# +enabled_actions+ must be passed to whitelist the types of WorkUnits than
|
19
|
+
# can be retrieved for processing. Optionally, specify the +offset+ to peek
|
20
|
+
# further on in line.
|
21
|
+
def self.dequeue(worker_name, enabled_actions=[], offset=0)
|
22
|
+
unit = self.first(
|
23
|
+
:conditions => {:status => INCOMPLETE, :worker_record_id => nil, :action => enabled_actions},
|
24
|
+
:order => "created_at asc",
|
25
|
+
:offset => offset
|
26
|
+
)
|
27
|
+
unit ? unit.assign_to(worker_name) : nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# After saving a WorkUnit, its Job should check if it just became complete.
|
31
|
+
def check_for_job_completion
|
32
|
+
self.job.check_for_completion if complete?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Mark this unit as having finished successfully.
|
36
|
+
def finish(output, time_taken)
|
37
|
+
update_attributes({
|
38
|
+
:status => SUCCEEDED,
|
39
|
+
:worker_record => nil,
|
40
|
+
:attempts => self.attempts + 1,
|
41
|
+
:output => output,
|
42
|
+
:time => time_taken
|
43
|
+
})
|
44
|
+
end
|
45
|
+
|
46
|
+
# Mark this unit as having failed. May attempt a retry.
|
47
|
+
def fail(output, time_taken)
|
48
|
+
tries = self.attempts + 1
|
49
|
+
return try_again if tries < CloudCrowd.config[:work_unit_retries]
|
50
|
+
update_attributes({
|
51
|
+
:status => FAILED,
|
52
|
+
:worker_record => nil,
|
53
|
+
:attempts => tries,
|
54
|
+
:output => output,
|
55
|
+
:time => time_taken
|
56
|
+
})
|
57
|
+
end
|
58
|
+
|
59
|
+
# Ever tried. Ever failed. No matter. Try again. Fail again. Fail better.
|
60
|
+
def try_again
|
61
|
+
update_attributes({
|
62
|
+
:worker_record => nil,
|
63
|
+
:attempts => self.attempts + 1
|
64
|
+
})
|
65
|
+
end
|
66
|
+
|
67
|
+
# When a Worker checks out a WorkUnit, establish the connection between
|
68
|
+
# WorkUnit and WorkerRecord.
|
69
|
+
def assign_to(worker_name)
|
70
|
+
self.worker_record = WorkerRecord.find_by_name!(worker_name)
|
71
|
+
self.save ? self : nil
|
72
|
+
end
|
73
|
+
|
74
|
+
# The JSON representation of a WorkUnit shares the Job's options with all
|
75
|
+
# its sister WorkUnits.
|
76
|
+
def to_json
|
77
|
+
{
|
78
|
+
'id' => self.id,
|
79
|
+
'job_id' => self.job_id,
|
80
|
+
'input' => self.input,
|
81
|
+
'attempts' => self.attempts,
|
82
|
+
'action' => self.action,
|
83
|
+
'options' => JSON.parse(self.job.options),
|
84
|
+
'status' => self.status
|
85
|
+
}.to_json
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module CloudCrowd
|
2
|
+
|
3
|
+
# A WorkerRecord is a recording of an active worker daemon running remotely.
|
4
|
+
# Every time it checks in, we keep track of its status. The attributes shown
|
5
|
+
# here may lag their actual values by up to Worker::CHECK_IN_INTERVAL seconds.
|
6
|
+
class WorkerRecord < ActiveRecord::Base
|
7
|
+
|
8
|
+
EXPIRES_AFTER = 2 * Worker::CHECK_IN_INTERVAL
|
9
|
+
|
10
|
+
has_one :work_unit
|
11
|
+
|
12
|
+
validates_presence_of :name, :thread_status
|
13
|
+
|
14
|
+
before_destroy :clear_work_units
|
15
|
+
|
16
|
+
named_scope :alive, lambda { {:conditions => ['updated_at > ?', Time.now - EXPIRES_AFTER]} }
|
17
|
+
named_scope :dead, lambda { {:conditions => ['updated_at <= ?', Time.now - EXPIRES_AFTER]} }
|
18
|
+
|
19
|
+
# Save a Worker's current status to the database.
|
20
|
+
def self.check_in(params)
|
21
|
+
attrs = {:thread_status => params[:thread_status], :updated_at => Time.now}
|
22
|
+
self.find_or_create_by_name(params[:name]).update_attributes!(attrs)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Remove a terminated Worker's record from the database.
|
26
|
+
def self.check_out(params)
|
27
|
+
self.find_by_name(params[:name]).destroy
|
28
|
+
end
|
29
|
+
|
30
|
+
# We consider the worker to be alive if it's checked in more recently
|
31
|
+
# than twice the expected interval ago.
|
32
|
+
def alive?
|
33
|
+
updated_at > Time.now - EXPIRES_AFTER
|
34
|
+
end
|
35
|
+
|
36
|
+
# Derive the Worker's PID on the remote machine from the name.
|
37
|
+
def pid
|
38
|
+
@pid ||= self.name.split('@').first
|
39
|
+
end
|
40
|
+
|
41
|
+
# Derive the hostname from the Worker's name.
|
42
|
+
def hostname
|
43
|
+
@hostname ||= self.name.split('@').last
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_json(opts={})
|
47
|
+
{
|
48
|
+
'name' => name,
|
49
|
+
'status' => work_unit && work_unit.display_status,
|
50
|
+
}.to_json
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def clear_work_units
|
57
|
+
WorkUnit.update_all('worker_record_id = null', "worker_record_id = #{id}")
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|