mobilize-base 1.36 → 1.293
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/README.md +666 -1
- data/lib/mobilize-base.rb +1 -12
- data/lib/mobilize-base/extensions/array.rb +3 -8
- data/lib/mobilize-base/extensions/google_drive/acl.rb +1 -1
- data/lib/mobilize-base/extensions/google_drive/client_login_fetcher.rb +1 -2
- data/lib/mobilize-base/extensions/google_drive/file.rb +37 -11
- data/lib/mobilize-base/extensions/string.rb +6 -11
- data/lib/mobilize-base/extensions/yaml.rb +7 -10
- data/lib/mobilize-base/handlers/gbook.rb +38 -25
- data/lib/mobilize-base/handlers/gdrive.rb +4 -20
- data/lib/mobilize-base/handlers/gfile.rb +10 -64
- data/lib/mobilize-base/handlers/gridfs.rb +24 -19
- data/lib/mobilize-base/handlers/gsheet.rb +29 -45
- data/lib/mobilize-base/handlers/resque.rb +10 -17
- data/lib/mobilize-base/jobtracker.rb +196 -22
- data/lib/mobilize-base/models/job.rb +77 -107
- data/lib/mobilize-base/models/runner.rb +122 -36
- data/lib/mobilize-base/models/stage.rb +37 -18
- data/lib/mobilize-base/tasks.rb +13 -50
- data/lib/mobilize-base/version.rb +1 -1
- data/lib/samples/gdrive.yml +0 -15
- data/lib/samples/gridfs.yml +3 -0
- data/lib/samples/gsheet.yml +4 -4
- data/lib/samples/jobtracker.yml +6 -0
- data/mobilize-base.gemspec +3 -3
- data/test/base_job_rows.yml +11 -0
- data/test/mobilize-base_test.rb +106 -0
- data/test/test_base_1.yml +3 -0
- data/test/test_helper.rb +0 -155
- metadata +24 -36
- data/lib/mobilize-base/extensions/time.rb +0 -20
- data/lib/mobilize-base/helpers/job_helper.rb +0 -54
- data/lib/mobilize-base/helpers/jobtracker_helper.rb +0 -143
- data/lib/mobilize-base/helpers/runner_helper.rb +0 -83
- data/lib/mobilize-base/helpers/stage_helper.rb +0 -38
- data/lib/samples/gfile.yml +0 -9
- data/test/fixtures/base1_stage1.in.yml +0 -10
- data/test/fixtures/integration_expected.yml +0 -25
- data/test/fixtures/integration_jobs.yml +0 -12
- data/test/fixtures/is_due.yml +0 -97
- data/test/integration/mobilize-base_test.rb +0 -57
- data/test/unit/mobilize-base_test.rb +0 -33
@@ -2,153 +2,123 @@ 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
|
-
def
|
28
|
+
def status
|
29
|
+
#last stage status
|
19
30
|
j = self
|
20
|
-
|
21
|
-
if j.trigger.strip[0..4].downcase == "after"
|
22
|
-
parent_name = j.trigger[5..-1].to_s.strip
|
23
|
-
parent_j = u.jobs.select{|job| job.name == parent_name}.first
|
24
|
-
return parent_j
|
25
|
-
else
|
26
|
-
return nil
|
27
|
-
end
|
31
|
+
j.active_stage.status if j.active_stage
|
28
32
|
end
|
29
33
|
|
30
|
-
def
|
34
|
+
def active_stage
|
31
35
|
j = self
|
32
|
-
|
33
|
-
|
34
|
-
parent_name = job.trigger[5..-1].to_s.strip
|
35
|
-
job.trigger.strip[0..4].downcase == "after" and
|
36
|
-
parent_name == j.name
|
37
|
-
end
|
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
38
|
end
|
39
39
|
|
40
|
-
|
41
|
-
#and creates/updates a job with it
|
42
|
-
def update_from_hash(hash)
|
40
|
+
def completed_at
|
43
41
|
j = self
|
44
|
-
|
45
|
-
j.update_attributes(:active => hash['active'],
|
46
|
-
:trigger => hash['trigger'])
|
47
|
-
(1..5).to_a.each do |s_idx|
|
48
|
-
stage_string = hash["stage#{s_idx.to_s}"]
|
49
|
-
s = Stage.find_by_path("#{j.path}/stage#{s_idx.to_s}")
|
50
|
-
if stage_string.to_s.length==0
|
51
|
-
#delete this stage and all stages after
|
52
|
-
if s
|
53
|
-
j = s.job
|
54
|
-
j.stages[(s.idx-1)..-1].each{|ps| ps.delete}
|
55
|
-
#just in case
|
56
|
-
s.delete
|
57
|
-
end
|
58
|
-
break
|
59
|
-
elsif s.nil?
|
60
|
-
#create this stage
|
61
|
-
s = Stage.find_or_create_by_path("#{j.path}/stage#{s_idx.to_s}")
|
62
|
-
end
|
63
|
-
#parse command string, update stage with it
|
64
|
-
s_handler, call, param_string = [""*3]
|
65
|
-
stage_string.split(" ").ie do |spls|
|
66
|
-
s_handler = spls.first.split(".").first
|
67
|
-
call = spls.first.split(".").last
|
68
|
-
param_string = spls[1..-1].join(" ").strip
|
69
|
-
end
|
70
|
-
s.update_attributes(:call=>call, :handler=>s_handler, :param_string=>param_string)
|
71
|
-
end
|
72
|
-
return j.reload
|
42
|
+
j.stages.last.completed_at if j.stages.last
|
73
43
|
end
|
74
44
|
|
75
|
-
def
|
45
|
+
def failed_at
|
76
46
|
j = self
|
77
|
-
|
78
|
-
|
79
|
-
return false
|
80
|
-
end
|
47
|
+
j.active_stage.failed_at if j.active_stage
|
48
|
+
end
|
81
49
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
if (job_handlers - loaded_handlers - Base.handlers).length>0
|
87
|
-
return false
|
88
|
-
end
|
50
|
+
def status_at
|
51
|
+
j = self
|
52
|
+
j.active_stage.status_at if j.active_stage
|
53
|
+
end
|
89
54
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
95
61
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
62
|
+
def is_working?
|
63
|
+
j = self
|
64
|
+
j.stages.select{|s| s.is_working?}.compact.length>0
|
65
|
+
end
|
66
|
+
|
67
|
+
def is_due?
|
68
|
+
j = self
|
69
|
+
return false if j.is_working? or j.active == false or j.trigger.to_s.starts_with?("after")
|
70
|
+
last_run = j.completed_at
|
71
|
+
#check trigger
|
72
|
+
trigger = j.trigger
|
73
|
+
return true if trigger == 'once'
|
74
|
+
#strip the "every" from the front if present
|
75
|
+
trigger = trigger.gsub("every","").gsub("."," ").strip
|
76
|
+
value,unit,operator,job_utctime = trigger.split(" ")
|
77
|
+
curr_utctime = Time.now.utc
|
78
|
+
curr_utcdate = curr_utctime.to_date.strftime("%Y-%m-%d")
|
79
|
+
if job_utctime
|
80
|
+
job_utctime = job_utctime.split(" ").first
|
81
|
+
job_utctime = Time.parse([curr_utcdate,job_utctime,"UTC"].join(" "))
|
82
|
+
end
|
83
|
+
#after is the only operator
|
84
|
+
raise "Unknown #{operator.to_s} operator" if operator and operator != "after"
|
85
|
+
if ["hour","hours"].include?(unit)
|
86
|
+
#if it's later than the last run + hour tolerance, is due
|
87
|
+
if last_run.nil? or curr_utctime > (last_run + value.to_i.hour)
|
100
88
|
return true
|
101
|
-
else
|
102
|
-
return false
|
103
89
|
end
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
operator = nil
|
113
|
-
#get time for time-based evaluations
|
114
|
-
curr_time = Time.now.utc
|
115
|
-
if ["hour","hours","day","days"].include?(unit)
|
116
|
-
if mark
|
117
|
-
last_mark_time = Time.at_marks_ago(number,unit,mark)
|
118
|
-
if last_comp_time.nil? or last_comp_time < last_mark_time
|
119
|
-
return true
|
90
|
+
elsif ["day","days"].include?(unit)
|
91
|
+
if last_run.nil? or curr_utctime.to_date >= (last_run.to_date + value.to_i.day)
|
92
|
+
if operator and job_utctime
|
93
|
+
if curr_utctime>job_utctime and (job_utctime - curr_utctime).abs < 1.hour
|
94
|
+
return true
|
95
|
+
end
|
96
|
+
elsif operator || job_utctime
|
97
|
+
raise "Please specify both an operator and a time in UTC, or neither"
|
120
98
|
else
|
121
|
-
return
|
99
|
+
return true
|
122
100
|
end
|
123
|
-
elsif last_comp_time.nil? or last_comp_time < (curr_time - number.to_i.send(unit))
|
124
|
-
return true
|
125
|
-
else
|
126
|
-
return false
|
127
101
|
end
|
128
102
|
elsif unit == "day_of_week"
|
129
|
-
if
|
130
|
-
if
|
131
|
-
|
132
|
-
last_mark_time = Time.at_marks_ago(1,"day",mark)
|
133
|
-
if last_comp_time < last_mark_time
|
103
|
+
if curr_utctime.wday==value and (last_run.nil? or last_run.to_date != curr_utctime.to_date)
|
104
|
+
if operator and job_utctime
|
105
|
+
if curr_utctime>job_utctime and (job_utctime - curr_utctime).abs < 1.hour
|
134
106
|
return true
|
135
|
-
else
|
136
|
-
return false
|
137
107
|
end
|
108
|
+
elsif operator || job_utctime
|
109
|
+
raise "Please specify both an operator and a time in UTC, or neither"
|
138
110
|
else
|
139
111
|
return true
|
140
112
|
end
|
141
113
|
end
|
142
114
|
elsif unit == "day_of_month"
|
143
|
-
if
|
144
|
-
if
|
145
|
-
|
146
|
-
last_mark_time = Time.at_marks_ago(1,"day",mark)
|
147
|
-
if last_comp_time < last_mark_time
|
115
|
+
if curr_utctime.day==value and (last_run.nil? or last_run.to_date != curr_utctime.to_date)
|
116
|
+
if operator and job_utctime
|
117
|
+
if curr_utctime>job_utctime and (job_utctime - curr_utctime).abs < 1.hour
|
148
118
|
return true
|
149
|
-
else
|
150
|
-
return false
|
151
119
|
end
|
120
|
+
elsif operator || job_utctime
|
121
|
+
raise "Please specify both an operator and a time in UTC, or neither"
|
152
122
|
else
|
153
123
|
return true
|
154
124
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module Mobilize
|
2
2
|
class Runner
|
3
|
-
include Mobilize::RunnerHelper
|
4
3
|
include Mongoid::Document
|
5
4
|
include Mongoid::Timestamps
|
6
5
|
field :path, type: String
|
@@ -12,6 +11,20 @@ module Mobilize
|
|
12
11
|
|
13
12
|
index({ path: 1})
|
14
13
|
|
14
|
+
def headers
|
15
|
+
%w{name active trigger status stage1 stage2 stage3 stage4 stage5}
|
16
|
+
end
|
17
|
+
|
18
|
+
def title
|
19
|
+
r = self
|
20
|
+
r.path.split("/").first
|
21
|
+
end
|
22
|
+
|
23
|
+
def worker
|
24
|
+
r = self
|
25
|
+
Mobilize::Resque.find_worker_by_path(r.path)
|
26
|
+
end
|
27
|
+
|
15
28
|
def Runner.find_by_path(path)
|
16
29
|
Runner.where(:path=>path).first
|
17
30
|
end
|
@@ -19,7 +32,6 @@ module Mobilize
|
|
19
32
|
def Runner.find_by_title(title)
|
20
33
|
Runner.where(:path=>"#{title}/jobs").first
|
21
34
|
end
|
22
|
-
|
23
35
|
def Runner.perform(id,*args)
|
24
36
|
r = Runner.find_by_path(id)
|
25
37
|
#get gdrive slot for read
|
@@ -29,18 +41,12 @@ module Mobilize
|
|
29
41
|
return false
|
30
42
|
end
|
31
43
|
r.update_attributes(:started_at=>Time.now.utc)
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
#queue up the jobs that are due and active
|
39
|
-
rescue => exc
|
40
|
-
#log the exception, but continue w job processing
|
41
|
-
#This ensures jobs are still processed if google drive goes down
|
42
|
-
r.update_status("Failed to read or update gsheet with #{exc.to_s} #{exc.backtrace.join(";")}")
|
43
|
-
end
|
44
|
+
#make sure any updates to activity are processed first
|
45
|
+
#as in when someone runs a "once" job that has completed
|
46
|
+
r.update_gsheet(gdrive_slot)
|
47
|
+
#read the jobs in the gsheet and update models with news
|
48
|
+
r.read_gsheet(gdrive_slot)
|
49
|
+
#queue up the jobs that are due and active
|
44
50
|
r.jobs.each do |j|
|
45
51
|
begin
|
46
52
|
if j.is_due?
|
@@ -50,41 +56,99 @@ module Mobilize
|
|
50
56
|
s.enqueue!
|
51
57
|
end
|
52
58
|
rescue ScriptError, StandardError => exc
|
53
|
-
r.update_status("Failed to enqueue #{j.path}")
|
59
|
+
r.update_status("Failed to enqueue #{j.path} with #{exc.to_s}")
|
60
|
+
j.update_attributes(:active=>false)
|
54
61
|
end
|
55
62
|
end
|
56
63
|
r.update_gsheet(gdrive_slot)
|
57
64
|
r.update_attributes(:completed_at=>Time.now.utc)
|
58
65
|
end
|
59
66
|
|
67
|
+
def dataset
|
68
|
+
r = self
|
69
|
+
Dataset.find_or_create_by_handler_and_path("gsheet",r.path)
|
70
|
+
end
|
71
|
+
|
60
72
|
def Runner.find_or_create_by_path(path)
|
61
73
|
Runner.where(:path=>path).first || Runner.create(:path=>path,:active=>true)
|
62
74
|
end
|
63
75
|
|
76
|
+
def gbook(gdrive_slot)
|
77
|
+
r = self
|
78
|
+
title = r.path.split("/").first
|
79
|
+
Gbook.find_by_path(title,gdrive_slot)
|
80
|
+
end
|
81
|
+
|
82
|
+
def gsheet(gdrive_slot)
|
83
|
+
r = self
|
84
|
+
u = r.user
|
85
|
+
jobs_sheet = Gsheet.find_by_path(r.path,gdrive_slot)
|
86
|
+
#make sure the user has a runner with a jobs sheet and has write privileges on the spreadsheet
|
87
|
+
unless (jobs_sheet and jobs_sheet.spreadsheet.acl_entry(u.email).ie{|e| e and e.role=="writer"})
|
88
|
+
#only give the user edit permissions if they're the ones
|
89
|
+
#creating it
|
90
|
+
jobs_sheet = Gsheet.find_or_create_by_path(r.path,gdrive_slot)
|
91
|
+
unless jobs_sheet.spreadsheet.acl_entry(u.email).ie{|e| e and e.role=="owner"}
|
92
|
+
jobs_sheet.spreadsheet.update_acl(u.email,"writer")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
jobs_sheet.add_headers(r.headers)
|
96
|
+
#add url to dataset
|
97
|
+
Dataset.find_or_create_by_url("gsheet://#{r.path}").update_attributes(:http_url=>jobs_sheet.spreadsheet.human_url)
|
98
|
+
begin;jobs_sheet.delete_sheet1;rescue;end #don't care if sheet1 deletion fails
|
99
|
+
return jobs_sheet
|
100
|
+
end
|
101
|
+
|
64
102
|
def read_gsheet(gdrive_slot)
|
65
103
|
r = self
|
66
104
|
#argument converts line breaks in cells to spaces
|
67
105
|
gsheet_tsv = r.gsheet(gdrive_slot).to_tsv(" ")
|
68
106
|
#turn it into a hash array
|
69
|
-
|
107
|
+
gsheet_jobs = gsheet_tsv.tsv_to_hash_array
|
70
108
|
#go through each job, update relevant job with its params
|
71
109
|
done_jobs = []
|
72
110
|
#parse out the jobs and update the Job collection
|
73
|
-
|
111
|
+
gsheet_jobs.each_with_index do |rj,rj_i|
|
74
112
|
#skip non-jobs or jobs without required values
|
75
|
-
next if (
|
76
|
-
|
77
|
-
|
78
|
-
j.
|
113
|
+
next if (rj['name'].to_s.first == "#" or ['name','active','trigger','stage1'].select{|c| rj[c].to_s.strip==""}.length>0)
|
114
|
+
j = Job.find_or_create_by_path("#{r.path}/#{rj['name']}")
|
115
|
+
#update top line params
|
116
|
+
j.update_attributes(:active => rj['active'],
|
117
|
+
:trigger => rj['trigger'])
|
118
|
+
(1..5).to_a.each do |s_idx|
|
119
|
+
stage_string = rj["stage#{s_idx.to_s}"]
|
120
|
+
s = Stage.find_by_path("#{j.path}/stage#{s_idx.to_s}")
|
121
|
+
if stage_string.to_s.length==0
|
122
|
+
#delete this stage and all stages after
|
123
|
+
if s
|
124
|
+
j = s.job
|
125
|
+
j.stages[(s.idx-1)..-1].each{|ps| ps.delete}
|
126
|
+
#just in case
|
127
|
+
s.delete
|
128
|
+
end
|
129
|
+
break
|
130
|
+
elsif s.nil?
|
131
|
+
#create this stage
|
132
|
+
s = Stage.find_or_create_by_path("#{j.path}/stage#{s_idx.to_s}")
|
133
|
+
end
|
134
|
+
#parse command string, update stage with it
|
135
|
+
s_handler, call, param_string = [""*3]
|
136
|
+
stage_string.split(" ").ie do |spls|
|
137
|
+
s_handler = spls.first.split(".").first
|
138
|
+
call = spls.first.split(".").last
|
139
|
+
param_string = spls[1..-1].join(" ").strip
|
140
|
+
end
|
141
|
+
s.update_attributes(:call=>call, :handler=>s_handler, :param_string=>param_string)
|
142
|
+
end
|
79
143
|
r.update_status("Updated #{j.path} stages at #{Time.now.utc}")
|
80
144
|
#add this job to list of read ones
|
81
145
|
done_jobs << j
|
82
146
|
end
|
83
147
|
#delete user jobs that are not included in Runner
|
84
|
-
(r.jobs.map{|j| j.path} - done_jobs.map{|j| j.path}).each do |
|
85
|
-
j = Job.where(:path=>
|
148
|
+
(r.jobs.map{|j| j.path} - done_jobs.map{|j| j.path}).each do |rj_path|
|
149
|
+
j = Job.where(:path=>rj_path).first
|
86
150
|
j.delete if j
|
87
|
-
r.update_status("Deleted job:#{
|
151
|
+
r.update_status("Deleted job:#{rj_path}")
|
88
152
|
end
|
89
153
|
r.update_status("jobs read at #{Time.now.utc}")
|
90
154
|
return true
|
@@ -95,24 +159,46 @@ module Mobilize
|
|
95
159
|
#there's nothing to update if runner has never had a completed at
|
96
160
|
return false unless r.completed_at
|
97
161
|
jobs_gsheet = r.gsheet(gdrive_slot)
|
98
|
-
upd_jobs = r.jobs.select{|j| j.status_at and j.status_at
|
99
|
-
upd_rows =
|
100
|
-
uj = {'name'=>j.name, 'status'=>j.status}
|
101
|
-
#jobs can only be turned off
|
102
|
-
#automatically, not back on
|
103
|
-
if j.active==false
|
104
|
-
uj['active'] = false
|
105
|
-
end
|
106
|
-
uj
|
107
|
-
end
|
162
|
+
upd_jobs = r.jobs.select{|j| j.status_at and j.status_at > j.runner.completed_at}
|
163
|
+
upd_rows = upd_jobs.map{|j| {'name'=>j.name, 'active'=>j.active, 'status'=>j.status}}
|
108
164
|
jobs_gsheet.add_or_update_rows(upd_rows)
|
109
165
|
r.update_status("gsheet updated")
|
110
166
|
return true
|
111
167
|
end
|
112
168
|
|
113
|
-
def
|
169
|
+
def jobs(jname=nil)
|
114
170
|
r = self
|
115
|
-
|
171
|
+
js = Job.where(:path=>/^#{r.path.escape_regex}/).to_a
|
172
|
+
if jname
|
173
|
+
return js.sel{|j| j.name == jname}.first
|
174
|
+
else
|
175
|
+
return js
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def user
|
180
|
+
r = self
|
181
|
+
user_name = r.path.split("_").second.split("(").first.split("/").first
|
182
|
+
User.where(:name=>user_name).first
|
183
|
+
end
|
184
|
+
|
185
|
+
def update_status(msg)
|
186
|
+
r = self
|
187
|
+
r.update_attributes(:status=>msg, :status_at=>Time.now.utc)
|
188
|
+
Mobilize::Resque.set_worker_args_by_path(r.path,{'status'=>msg})
|
189
|
+
return true
|
190
|
+
end
|
191
|
+
|
192
|
+
def is_working?
|
193
|
+
r = self
|
194
|
+
Mobilize::Resque.active_paths.include?(r.path)
|
195
|
+
end
|
196
|
+
|
197
|
+
def is_due?
|
198
|
+
r = self.reload
|
199
|
+
return false if r.is_working?
|
200
|
+
prev_due_time = Time.now.utc - Jobtracker.runner_read_freq
|
201
|
+
return true if r.started_at.nil? or r.started_at < prev_due_time
|
116
202
|
end
|
117
203
|
|
118
204
|
def enqueue!
|