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,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
|