mobilize-base 1.3 → 1.21
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +11 -10
- data/lib/mobilize-base/extensions/google_drive/file.rb +7 -7
- data/lib/mobilize-base/extensions/google_drive/worksheet.rb +2 -7
- data/lib/mobilize-base/extensions/string.rb +4 -12
- data/lib/mobilize-base/extensions/yaml.rb +7 -10
- data/lib/mobilize-base/handlers/gbook.rb +31 -24
- data/lib/mobilize-base/handlers/gfile.rb +3 -5
- data/lib/mobilize-base/handlers/gridfs.rb +24 -19
- data/lib/mobilize-base/handlers/gsheet.rb +20 -25
- data/lib/mobilize-base/handlers/resque.rb +4 -16
- data/lib/mobilize-base/jobtracker.rb +5 -13
- data/lib/mobilize-base/models/job.rb +48 -36
- data/lib/mobilize-base/models/runner.rb +123 -24
- data/lib/mobilize-base/models/stage.rb +43 -14
- data/lib/mobilize-base/tasks.rb +3 -16
- data/lib/mobilize-base/version.rb +1 -1
- data/lib/mobilize-base.rb +1 -5
- data/lib/samples/gridfs.yml +3 -0
- data/lib/samples/gsheet.yml +4 -4
- data/mobilize-base.gemspec +5 -4
- data/test/mobilize-base_test.rb +0 -1
- metadata +31 -20
- data/lib/mobilize-base/extensions/resque-server/views/queues.erb +0 -59
- data/lib/mobilize-base/extensions/resque-server/views/working.erb +0 -85
- data/lib/mobilize-base/helpers/job_helper.rb +0 -54
- data/lib/mobilize-base/helpers/runner_helper.rb +0 -83
- data/lib/mobilize-base/helpers/stage_helper.rb +0 -38
data/README.md
CHANGED
@@ -220,8 +220,9 @@ production:
|
|
220
220
|
|
221
221
|
gsheet.yml needs:
|
222
222
|
* max_cells, which is the number of cells a sheet is allowed to have
|
223
|
-
written to it at one time. Default is
|
224
|
-
|
223
|
+
written to it at one time. Default is 400k cells, which is the max per
|
224
|
+
book. Google Drive will throw its own exception if
|
225
|
+
you try to write more than that.
|
225
226
|
* Because Google Docs ties date formatting to the Locale for the
|
226
227
|
spreadsheet, there are 2 date format parameters:
|
227
228
|
* read_date_format, which is the format that should be read FROM google
|
@@ -355,16 +356,22 @@ mobilize_base:resque_web task, as detailed in [Start Resque-Web](#section_Start_
|
|
355
356
|
Mobilize stores cached data in MongoDB Gridfs.
|
356
357
|
It needs the below parameters, which can be found in the [lib/samples][git_samples] folder.
|
357
358
|
|
359
|
+
* max_versions - the number of __different__ versions of data to keep
|
360
|
+
for a given cache. Default is 10. This is meant mostly to allow you to
|
361
|
+
restore Runners from cache if necessary.
|
358
362
|
* max_compressed_write_size - the amount of compressed data Gridfs will
|
359
363
|
allow. If you try to write more than this, an exception will be thrown.
|
360
364
|
|
361
365
|
``` yml
|
362
366
|
---
|
363
367
|
development:
|
368
|
+
max_versions: 10 #number of versions of cache to keep in gridfs
|
364
369
|
max_compressed_write_size: 1000000000 #~1GB
|
365
370
|
test:
|
371
|
+
max_versions: 10 #number of versions of cache to keep in gridfs
|
366
372
|
max_compressed_write_size: 1000000000 #~1GB
|
367
373
|
production:
|
374
|
+
max_versions: 10 #number of versions of cache to keep in gridfs
|
368
375
|
max_compressed_write_size: 1000000000 #~1GB
|
369
376
|
```
|
370
377
|
|
@@ -557,14 +564,8 @@ the Runner itself.
|
|
557
564
|
and "base1.out" for the second test. The first
|
558
565
|
takes the output from the first stage and the second reads it straight
|
559
566
|
from the referenced sheet.
|
560
|
-
* All stages accept
|
561
|
-
|
562
|
-
* delay: an integer specifying the number of seconds between retries.
|
563
|
-
* always_on: if false, turns the job off on stage failures.
|
564
|
-
Otherwise the job will retry from the beginning with the same frequency as the Runner refresh rate.
|
565
|
-
* notify: by default, the stage owner will be notified on failure.
|
566
|
-
* if false, will not notify the stage owner in the event of a failure.
|
567
|
-
* If it's an email address, will email the specified person.
|
567
|
+
* All stages accept a "retries" parameter, which is an integer specifying the number of times that the system will try it again before
|
568
|
+
giving up.
|
568
569
|
* If a stage fails after all retries, it will output its standard error to a tab in the Runner with the name of the job, the name of the stage, and a ".err" extension
|
569
570
|
* The tab will be headed "response" and will contain the exception and backtrace for the error.
|
570
571
|
* The test uses "Requestor_mobilize(test)/base1.out" and
|
@@ -13,7 +13,7 @@ module GoogleDrive
|
|
13
13
|
f = self
|
14
14
|
#admin includes workers
|
15
15
|
return true if f.has_admin_acl?
|
16
|
-
accounts = (Mobilize::Gdrive.admin_emails + Mobilize::Gdrive.worker_emails)
|
16
|
+
accounts = (Mobilize::Gdrive.admin_emails + Mobilize::Gdrive.worker_emails)
|
17
17
|
accounts.each do |email|
|
18
18
|
f.update_acl(email)
|
19
19
|
end
|
@@ -21,9 +21,9 @@ module GoogleDrive
|
|
21
21
|
|
22
22
|
def has_admin_acl?
|
23
23
|
f = self
|
24
|
-
curr_emails = f.acls.map{|a| a.scope}.
|
25
|
-
admin_emails = (Mobilize::Gdrive.admin_emails + Mobilize::Gdrive.worker_emails)
|
26
|
-
if
|
24
|
+
curr_emails = f.acls.map{|a| a.scope}.sort
|
25
|
+
admin_emails = (Mobilize::Gdrive.admin_emails + Mobilize::Gdrive.worker_emails)
|
26
|
+
if (curr_emails & admin_emails) == admin_emails
|
27
27
|
return true
|
28
28
|
else
|
29
29
|
return false
|
@@ -32,9 +32,9 @@ module GoogleDrive
|
|
32
32
|
|
33
33
|
def has_worker_acl?
|
34
34
|
f = self
|
35
|
-
curr_emails = f.acls.map{|a| a.scope}.
|
35
|
+
curr_emails = f.acls.map{|a| a.scope}.sort
|
36
36
|
worker_emails = Mobilize::Gdrive.worker_emails.sort
|
37
|
-
if
|
37
|
+
if (curr_emails & worker_emails) == worker_emails
|
38
38
|
return true
|
39
39
|
else
|
40
40
|
return false
|
@@ -84,7 +84,7 @@ module GoogleDrive
|
|
84
84
|
end
|
85
85
|
def acl_entry(email)
|
86
86
|
f = self
|
87
|
-
f.acls.select{|a| ['group','user'].include?(a.scope_type) and a.scope
|
87
|
+
f.acls.select{|a| ['group','user'].include?(a.scope_type) and a.scope == email}.first
|
88
88
|
end
|
89
89
|
def entry_hash
|
90
90
|
f = self
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module GoogleDrive
|
2
2
|
class Worksheet
|
3
|
-
def to_tsv
|
3
|
+
def to_tsv
|
4
4
|
sheet = self
|
5
5
|
rows = sheet.rows
|
6
6
|
header = rows.first
|
@@ -8,12 +8,7 @@ module GoogleDrive
|
|
8
8
|
#look for blank cols to indicate end of row
|
9
9
|
col_last_i = (header.index("") || header.length)-1
|
10
10
|
#ignore user-entered line breaks for purposes of tsv reads
|
11
|
-
out_tsv = rows.map
|
12
|
-
row = r[0..col_last_i].join("\t")
|
13
|
-
row.gsub!("\n",gsub_line_breaks)
|
14
|
-
row = row + "\n"
|
15
|
-
row
|
16
|
-
end.join + "\n"
|
11
|
+
out_tsv = rows.map{|r| r[0..col_last_i].join("\t").gsub("\n","")+"\n"}.join + "\n"
|
17
12
|
out_tsv.tsv_convert_dates(Mobilize::Gsheet.config['sheet_date_format'],
|
18
13
|
Mobilize::Gsheet.config['read_date_format'])
|
19
14
|
end
|
@@ -11,19 +11,11 @@ class String
|
|
11
11
|
def opp
|
12
12
|
pp self
|
13
13
|
end
|
14
|
-
def to_md5
|
15
|
-
Digest::MD5.hexdigest(self)
|
16
|
-
end
|
17
14
|
def bash(except=true)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
err_str = stderr.read
|
23
|
-
end
|
24
|
-
exit_status = status.exitstatus
|
25
|
-
raise err_str if (exit_status !=0 and except==true)
|
26
|
-
return out_str
|
15
|
+
pid,stdin,stdout,stderr = Open4.popen4(self)
|
16
|
+
pid,stdin = [nil,nil]
|
17
|
+
raise stderr.read if (stderr.read.length>0 and except==true)
|
18
|
+
return stdout.read
|
27
19
|
end
|
28
20
|
def escape_regex
|
29
21
|
str = self
|
@@ -10,16 +10,13 @@ module YAML
|
|
10
10
|
#make sure urls have their colon spaces fixed
|
11
11
|
result_hash={}
|
12
12
|
easy_hash.each do |k,v|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
else
|
21
|
-
v
|
22
|
-
end
|
13
|
+
result_hash[k] = if v.class==String
|
14
|
+
v.gsub(": //","://")
|
15
|
+
elsif v.class==Array
|
16
|
+
v.map{|av| av.to_s.gsub(": //","://")}
|
17
|
+
else
|
18
|
+
v
|
19
|
+
end
|
23
20
|
end
|
24
21
|
return result_hash
|
25
22
|
end
|
@@ -14,44 +14,51 @@ module Mobilize
|
|
14
14
|
dst = Dataset.find_by_handler_and_path('gbook',path)
|
15
15
|
if dst and dst.http_url.to_s.length>0
|
16
16
|
book = Gbook.find_by_http_url(dst.http_url,gdrive_slot)
|
17
|
-
if
|
18
|
-
|
17
|
+
#doesn't count if it's deleted
|
18
|
+
if book.entry_hash[:deleted]
|
19
|
+
book = nil
|
19
20
|
else
|
20
|
-
|
21
|
+
return book
|
21
22
|
end
|
22
23
|
end
|
23
|
-
#try to find books by title
|
24
24
|
books = Gbook.find_all_by_path(path,gdrive_slot)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
25
|
+
dst = Dataset.find_or_create_by_handler_and_path('gbook',path)
|
26
|
+
book = nil
|
27
|
+
if books.length>1 and dst.http_url.to_s.length>0
|
28
|
+
#some idiot process or malicious user created a duplicate book.
|
29
|
+
#Fix by deleting all but the one with dst entry's key
|
30
|
+
dkey = dst.http_url.split("key=").last
|
31
|
+
books.each do |b|
|
32
|
+
bkey = b.resource_id.split(":").last
|
33
|
+
if bkey == dkey
|
34
|
+
book = b
|
35
|
+
dst.update_attributes(:http_url=>book.human_url)
|
36
|
+
else
|
37
|
+
#delete the invalid book
|
38
|
+
b.delete
|
39
|
+
("Deleted duplicate book #{path}").oputs
|
40
|
+
end
|
41
|
+
end
|
42
|
+
else
|
43
|
+
#If it's a new dst or if there are multiple books
|
44
|
+
#take the first
|
45
|
+
book = books.first
|
46
|
+
dst.update_attributes(:http_url=>book.human_url) if book
|
33
47
|
end
|
34
48
|
return book
|
35
49
|
end
|
36
|
-
|
37
50
|
def Gbook.find_or_create_by_path(path,gdrive_slot)
|
38
51
|
book = Gbook.find_by_path(path,gdrive_slot)
|
52
|
+
dst = Dataset.find_or_create_by_handler_and_path('gbook',path)
|
39
53
|
if book.nil?
|
40
54
|
#always use owner email to make sure all books are owned by owner account
|
41
55
|
book = Gdrive.root(Gdrive.owner_email).create_spreadsheet(path)
|
42
56
|
("Created book #{path} at #{Time.now.utc.to_s}; Access at #{book.human_url}").oputs
|
43
|
-
#check to make sure the dataset has a blank url; if not, error out
|
44
|
-
dst = Dataset.find_or_create_by_handler_and_path('gbook',path)
|
45
|
-
if dst.http_url.to_s.length>0
|
46
|
-
#add acls to book regardless
|
47
|
-
book.add_admin_acl
|
48
|
-
raise "Book #{path} is already assigned to #{dst.http_url}; please update the record with #{book.human_url}"
|
49
|
-
else
|
50
|
-
api_url = book.human_url.split("&").first
|
51
|
-
dst.update_attributes(:http_url=>api_url)
|
52
|
-
book.add_admin_acl
|
53
|
-
end
|
54
57
|
end
|
58
|
+
#always make sure book dataset http URL is up to date
|
59
|
+
#and that book has admin acl
|
60
|
+
dst.update_attributes(:http_url=>book.human_url)
|
61
|
+
book.add_admin_acl
|
55
62
|
return book
|
56
63
|
end
|
57
64
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Mobilize
|
2
2
|
module Gfile
|
3
|
-
def Gfile.path_to_dst(path,stage_path
|
3
|
+
def Gfile.path_to_dst(path,stage_path)
|
4
4
|
#don't need the ://
|
5
5
|
path = path.split("://").last if path.index("://")
|
6
6
|
if Gfile.find_by_path(path)
|
@@ -38,8 +38,7 @@ module Mobilize
|
|
38
38
|
end
|
39
39
|
#update http url for file
|
40
40
|
dst = Dataset.find_by_handler_and_path("gfile",dst_path)
|
41
|
-
|
42
|
-
dst.update_attributes(:http_url=>api_url)
|
41
|
+
dst.update_attributes(:http_url=>file.human_url)
|
43
42
|
true
|
44
43
|
end
|
45
44
|
|
@@ -87,8 +86,7 @@ module Mobilize
|
|
87
86
|
#always make sure dataset http URL is up to date
|
88
87
|
#and that it has admin acl
|
89
88
|
if file
|
90
|
-
|
91
|
-
dst.update_attributes(:http_url=>api_url)
|
89
|
+
dst.update_attributes(:http_url=>file.human_url)
|
92
90
|
file.add_admin_acl
|
93
91
|
end
|
94
92
|
return file
|
@@ -1,38 +1,43 @@
|
|
1
|
-
require 'tempfile'
|
2
1
|
module Mobilize
|
3
2
|
module Gridfs
|
4
3
|
def Gridfs.config
|
5
4
|
Base.config('gridfs')
|
6
5
|
end
|
7
6
|
|
8
|
-
def Gridfs.
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
def Gridfs.grid
|
8
|
+
session = ::Mongoid.configure.sessions['default']
|
9
|
+
database_name = session['database']
|
10
|
+
host,port = session['hosts'].first.split(":")
|
11
|
+
return ::Mongo::GridFileSystem.new(::Mongo::Connection.new(host,port).db(database_name))
|
12
12
|
end
|
13
13
|
|
14
|
-
def Gridfs.
|
14
|
+
def Gridfs.read_by_dataset_path(dst_path,user_name,*args)
|
15
|
+
begin
|
16
|
+
zs=Gridfs.grid.open(dst_path,'r').read
|
17
|
+
return ::Zlib::Inflate.inflate(zs)
|
18
|
+
rescue
|
19
|
+
return nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def Gridfs.write_by_dataset_path(dst_path,string,user_name,*args)
|
15
24
|
zs = ::Zlib::Deflate.deflate(string)
|
16
25
|
raise "compressed string too large for Gridfs write" if zs.length > Gridfs.config['max_compressed_write_size']
|
17
|
-
|
18
|
-
|
19
|
-
curr_zs = curr_file.data if curr_file
|
20
|
-
#overwrite when there is a change
|
26
|
+
curr_zs = Gridfs.read_by_dataset_path(dst_path,user_name).to_s
|
27
|
+
#write a new version when there is a change
|
21
28
|
if curr_zs != zs
|
22
|
-
|
23
|
-
#create temp file w zstring
|
24
|
-
temp_file = ::Tempfile.new("#{string}#{Time.now.to_f}".to_md5)
|
25
|
-
temp_file.print(zs)
|
26
|
-
temp_file.close
|
27
|
-
#put data in file
|
28
|
-
Mongoid::GridFs.put(temp_file.path,:filename=>dst_path)
|
29
|
+
Gridfs.grid.open(dst_path,'w',:versions => Gridfs.config['max_versions']){|f| f.write(zs)}
|
29
30
|
end
|
30
31
|
return true
|
31
32
|
end
|
32
33
|
|
33
34
|
def Gridfs.delete(dst_path)
|
34
|
-
|
35
|
-
|
35
|
+
begin
|
36
|
+
Gridfs.grid.delete(dst_path)
|
37
|
+
return true
|
38
|
+
rescue
|
39
|
+
return nil
|
40
|
+
end
|
36
41
|
end
|
37
42
|
end
|
38
43
|
end
|
@@ -10,10 +10,12 @@ module Mobilize
|
|
10
10
|
end
|
11
11
|
|
12
12
|
# converts a source path or target path to a dst in the context of handler and stage
|
13
|
-
def Gsheet.path_to_dst(path,stage_path
|
13
|
+
def Gsheet.path_to_dst(path,stage_path)
|
14
14
|
s = Stage.where(:path=>stage_path).first
|
15
15
|
params = s.params
|
16
16
|
target_path = params['target']
|
17
|
+
#take random slot if one is not available
|
18
|
+
gdrive_slot = Gdrive.slot_worker_by_path(stage_path) || Gdrive.worker_emails.sort_by{rand}.first
|
17
19
|
#if this is the target, it doesn't have to exist already
|
18
20
|
is_target = true if path == target_path
|
19
21
|
#don't need the ://
|
@@ -44,7 +46,9 @@ module Mobilize
|
|
44
46
|
|
45
47
|
def Gsheet.read_by_dataset_path(dst_path,user_name,*args)
|
46
48
|
#expects gdrive slot as first arg, otherwise chooses random
|
47
|
-
gdrive_slot = args
|
49
|
+
gdrive_slot = args
|
50
|
+
worker_emails = Gdrive.worker_emails.sort_by{rand}
|
51
|
+
gdrive_slot = worker_emails.first unless worker_emails.include?(gdrive_slot)
|
48
52
|
sheet = Gsheet.find_by_path(dst_path,gdrive_slot)
|
49
53
|
sheet.read(user_name) if sheet
|
50
54
|
end
|
@@ -52,6 +56,8 @@ module Mobilize
|
|
52
56
|
def Gsheet.write_by_dataset_path(dst_path,tsv,user_name,*args)
|
53
57
|
#expects gdrive slot as first arg, otherwise chooses random
|
54
58
|
gdrive_slot,crop = args
|
59
|
+
worker_emails = Gdrive.worker_emails.sort_by{rand}
|
60
|
+
gdrive_slot = worker_emails.first unless worker_emails.include?(gdrive_slot)
|
55
61
|
crop ||= true
|
56
62
|
Gsheet.write_target(dst_path,tsv,user_name,gdrive_slot,crop)
|
57
63
|
end
|
@@ -81,16 +87,15 @@ module Mobilize
|
|
81
87
|
|
82
88
|
def Gsheet.write_temp(target_path,gdrive_slot,tsv)
|
83
89
|
#find and delete temp sheet, if any
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
temp_sheet =
|
90
|
+
temp_path = [target_path.gridsafe,"temp"].join("/")
|
91
|
+
temp_sheet = Gsheet.find_by_path(temp_path,gdrive_slot)
|
92
|
+
temp_sheet.delete if temp_sheet
|
93
|
+
#write data to temp sheet
|
94
|
+
temp_sheet = Gsheet.find_or_create_by_path(temp_path,gdrive_slot)
|
89
95
|
#this step has a tendency to fail; if it does,
|
90
96
|
#don't fail the stage, mark it as false
|
91
97
|
begin
|
92
|
-
|
93
|
-
temp_sheet.write(tsv,gdrive_user)
|
98
|
+
temp_sheet.write(tsv,Gdrive.owner_name)
|
94
99
|
rescue
|
95
100
|
return nil
|
96
101
|
end
|
@@ -109,7 +114,7 @@ module Mobilize
|
|
109
114
|
#only give the user edit permissions if they're the ones
|
110
115
|
#creating it
|
111
116
|
target_sheet = Gsheet.find_or_create_by_path(target_path,gdrive_slot)
|
112
|
-
target_sheet.spreadsheet.update_acl(
|
117
|
+
target_sheet.spreadsheet.update_acl(user_email,"writer") unless target_sheet.spreadsheet.acl_entry(u.email).ie{|e| e and e.role=="owner"}
|
113
118
|
target_sheet.delete_sheet1
|
114
119
|
end
|
115
120
|
#pass it crop param to determine whether to shrink target sheet to fit data
|
@@ -129,24 +134,14 @@ module Mobilize
|
|
129
134
|
crop = s.params['crop'] || true
|
130
135
|
begin
|
131
136
|
#get tsv to write from stage
|
132
|
-
source = s.sources
|
137
|
+
source = s.sources.first
|
133
138
|
raise "Need source for gsheet write" unless source
|
134
139
|
tsv = source.read(u.name,gdrive_slot)
|
135
|
-
raise "No data
|
136
|
-
|
137
|
-
tsv_col_count = tsv.to_s.split("\n").first.to_s.split("\t").length
|
138
|
-
tsv_cell_count = tsv_row_count * tsv_col_count
|
139
|
-
stdout = if tsv_row_count == 0
|
140
|
-
#soft error; no data to write. Stage will complete.
|
141
|
-
"Write skipped for #{s.target.url}"
|
142
|
-
elsif tsv_cell_count > Gsheet.max_cells
|
143
|
-
raise "Too many datapoints; you have #{tsv_cell_count.to_s}, max is #{Gsheet.max_cells.to_s}"
|
144
|
-
else
|
145
|
-
Dataset.write_by_url(s.target.url,tsv,u.name,gdrive_slot,crop)
|
146
|
-
#update status
|
147
|
-
"Write successful for #{s.target.url}"
|
148
|
-
end
|
140
|
+
raise "No data found in #{source.url}" unless tsv
|
141
|
+
Dataset.write_by_url(s.target.url,tsv,u.name,gdrive_slot,crop)
|
149
142
|
Gdrive.unslot_worker_by_path(stage_path)
|
143
|
+
#update status
|
144
|
+
stdout = "Write successful for #{s.target.url}"
|
150
145
|
stderr = nil
|
151
146
|
s.update_status(stdout)
|
152
147
|
signal = 0
|
@@ -25,7 +25,7 @@ module Mobilize
|
|
25
25
|
return idle_workers if state == 'idle'
|
26
26
|
stale_workers = workers.select{|w| Time.parse(w.started) < Jobtracker.deployed_at}
|
27
27
|
return stale_workers if state == 'stale'
|
28
|
-
timeout_workers = workers.select{|w| w.job['payload'] and w.job['payload']['class']!='Jobtracker' and w.job['
|
28
|
+
timeout_workers = workers.select{|w| w.job['payload'] and w.job['payload']['class']!='Jobtracker' and w.job['runat'] < (Time.now.utc - Jobtracker.max_run_time)}
|
29
29
|
return timeout_workers if state == 'timeout'
|
30
30
|
raise "invalid state #{state}"
|
31
31
|
end
|
@@ -109,28 +109,16 @@ module Mobilize
|
|
109
109
|
Resque.failures.each_with_index do |f,f_i|
|
110
110
|
#skip if already notified
|
111
111
|
next if f['notified']
|
112
|
-
#try to send message to stage owner, where appropriate
|
113
112
|
stage_path = f['payload']['args'].first
|
114
|
-
|
115
|
-
|
116
|
-
if s.params['notify'].to_s=="false"
|
117
|
-
next
|
118
|
-
elsif s.params['notify'].index("@")
|
119
|
-
s.params['notify']
|
120
|
-
else
|
121
|
-
s.job.runner.user.email
|
122
|
-
end
|
123
|
-
rescue
|
124
|
-
#jobs without stages are sent to first admin
|
125
|
-
Jobtracker.admin_emails.first
|
126
|
-
end
|
113
|
+
s = Stage.where(:path=>stage_path).first
|
114
|
+
email = s.job.runner.user.email
|
127
115
|
exc_to_s = f['error']
|
128
116
|
if fjobs[email].nil?
|
129
117
|
fjobs[email] = {stage_path => {exc_to_s => 1}}
|
130
118
|
elsif fjobs[email][stage_path].nil?
|
131
119
|
fjobs[email][stage_path] = {exc_to_s => 1}
|
132
120
|
elsif fjobs[email][stage_path][exc_to_s].nil?
|
133
|
-
fjobs[email][stage_path][exc_to_s] = 1
|
121
|
+
fjobs[email][stage_path][exc_to_s] = 1
|
134
122
|
else
|
135
123
|
fjobs[email][stage_path][exc_to_s] += 1
|
136
124
|
end
|
@@ -48,8 +48,6 @@ module Mobilize
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def Jobtracker.update_status(msg)
|
51
|
-
#this is to keep jobtracker from resisting stop commands
|
52
|
-
return false if Jobtracker.status=="stopping"
|
53
51
|
#Jobtracker has no persistent database state
|
54
52
|
Resque.set_worker_args_by_path("jobtracker",{'status'=>msg})
|
55
53
|
return true
|
@@ -124,7 +122,7 @@ module Mobilize
|
|
124
122
|
sleep 5
|
125
123
|
i=0
|
126
124
|
while Jobtracker.status=='stopping'
|
127
|
-
|
125
|
+
Jobtracker.update_status("#{Jobtracker.to_s} still on queue, waiting")
|
128
126
|
sleep 5
|
129
127
|
i+=1
|
130
128
|
end
|
@@ -147,8 +145,8 @@ module Mobilize
|
|
147
145
|
def Jobtracker.max_run_time_workers
|
148
146
|
#return workers who have been cranking away for 6+ hours
|
149
147
|
workers = Jobtracker.workers('working').select do |w|
|
150
|
-
w.job['
|
151
|
-
(Time.now.utc - Time.parse(w.job['
|
148
|
+
w.job['runat'].to_s.length>0 and
|
149
|
+
(Time.now.utc - Time.parse(w.job['runat'])) > Jobtracker.max_run_time
|
152
150
|
end
|
153
151
|
return workers
|
154
152
|
end
|
@@ -187,18 +185,13 @@ module Mobilize
|
|
187
185
|
if lws.length>0
|
188
186
|
n = {}
|
189
187
|
n['subject'] = "#{lws.length.to_s} max run time jobs"
|
190
|
-
n['body'] = lws.map{|w| %{spec:#{w['spec']} stg:#{w['stg']}
|
188
|
+
n['body'] = lws.map{|w| %{spec:#{w['spec']} stg:#{w['stg']} runat:#{w['runat'].to_s}}}.join("\n\n")
|
191
189
|
n['to'] = Jobtracker.admin_emails.join(",")
|
192
190
|
notifs << n
|
193
191
|
end
|
194
192
|
#deliver each email generated
|
195
193
|
notifs.each do |notif|
|
196
|
-
|
197
|
-
Email.write(notif).deliver
|
198
|
-
rescue
|
199
|
-
#log email on failure
|
200
|
-
Jobtracker.update_status("Failed to deliver #{notif.to_s}")
|
201
|
-
end
|
194
|
+
Email.write(notif).deliver
|
202
195
|
end
|
203
196
|
#update notification time so JT knows to wait a while
|
204
197
|
Jobtracker.last_notification = Time.now.utc.to_s
|
@@ -293,7 +286,6 @@ module Mobilize
|
|
293
286
|
# delete any old runner from previous test runs
|
294
287
|
gdrive_slot = Gdrive.owner_email
|
295
288
|
u.runner.gsheet(gdrive_slot).spreadsheet.delete
|
296
|
-
Dataset.find_by_handler_and_path('gbook',u.runner.title).delete
|
297
289
|
Jobtracker.update_status("enqueue jobtracker, wait 45s")
|
298
290
|
Mobilize::Jobtracker.start
|
299
291
|
sleep 45
|
@@ -2,54 +2,66 @@ module Mobilize
|
|
2
2
|
class Job
|
3
3
|
include Mongoid::Document
|
4
4
|
include Mongoid::Timestamps
|
5
|
-
include Mobilize::JobHelper
|
6
5
|
field :path, type: String
|
7
6
|
field :active, type: Boolean
|
8
7
|
field :trigger, type: String
|
9
8
|
|
10
9
|
index({ path: 1})
|
11
10
|
|
11
|
+
def name
|
12
|
+
j = self
|
13
|
+
j.path.split("/").last
|
14
|
+
end
|
15
|
+
|
16
|
+
def stages
|
17
|
+
j = self
|
18
|
+
#starts with the job path, followed by a slash
|
19
|
+
Stage.where(:path=>/^#{j.path.escape_regex}\//).to_a.sort_by{|s| s.path}
|
20
|
+
end
|
21
|
+
|
12
22
|
def Job.find_or_create_by_path(path)
|
13
23
|
j = Job.where(:path=>path).first
|
14
24
|
j = Job.create(:path=>path) unless j
|
15
25
|
return j
|
16
26
|
end
|
17
27
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
j
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
28
|
+
def status
|
29
|
+
#last stage status
|
30
|
+
j = self
|
31
|
+
j.active_stage.status if j.active_stage
|
32
|
+
end
|
33
|
+
|
34
|
+
def active_stage
|
35
|
+
j = self
|
36
|
+
#latest started at or first
|
37
|
+
j.stages.select{|s| s.started_at}.sort_by{|s| s.started_at}.last || j.stages.first
|
38
|
+
end
|
39
|
+
|
40
|
+
def completed_at
|
41
|
+
j = self
|
42
|
+
j.stages.last.completed_at if j.stages.last
|
43
|
+
end
|
44
|
+
|
45
|
+
def failed_at
|
46
|
+
j = self
|
47
|
+
j.active_stage.failed_at if j.active_stage
|
48
|
+
end
|
49
|
+
|
50
|
+
def status_at
|
51
|
+
j = self
|
52
|
+
j.active_stage.status_at if j.active_stage
|
53
|
+
end
|
54
|
+
|
55
|
+
#convenience methods
|
56
|
+
def runner
|
57
|
+
j = self
|
58
|
+
runner_path = j.path.split("/")[0..-2].join("/")
|
59
|
+
return Runner.where(:path=>runner_path).first
|
60
|
+
end
|
61
|
+
|
62
|
+
def is_working?
|
63
|
+
j = self
|
64
|
+
j.stages.select{|s| s.is_working?}.compact.length>0
|
53
65
|
end
|
54
66
|
|
55
67
|
def is_due?
|