mobilize-base 1.36 → 1.293

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/README.md +666 -1
  2. data/lib/mobilize-base.rb +1 -12
  3. data/lib/mobilize-base/extensions/array.rb +3 -8
  4. data/lib/mobilize-base/extensions/google_drive/acl.rb +1 -1
  5. data/lib/mobilize-base/extensions/google_drive/client_login_fetcher.rb +1 -2
  6. data/lib/mobilize-base/extensions/google_drive/file.rb +37 -11
  7. data/lib/mobilize-base/extensions/string.rb +6 -11
  8. data/lib/mobilize-base/extensions/yaml.rb +7 -10
  9. data/lib/mobilize-base/handlers/gbook.rb +38 -25
  10. data/lib/mobilize-base/handlers/gdrive.rb +4 -20
  11. data/lib/mobilize-base/handlers/gfile.rb +10 -64
  12. data/lib/mobilize-base/handlers/gridfs.rb +24 -19
  13. data/lib/mobilize-base/handlers/gsheet.rb +29 -45
  14. data/lib/mobilize-base/handlers/resque.rb +10 -17
  15. data/lib/mobilize-base/jobtracker.rb +196 -22
  16. data/lib/mobilize-base/models/job.rb +77 -107
  17. data/lib/mobilize-base/models/runner.rb +122 -36
  18. data/lib/mobilize-base/models/stage.rb +37 -18
  19. data/lib/mobilize-base/tasks.rb +13 -50
  20. data/lib/mobilize-base/version.rb +1 -1
  21. data/lib/samples/gdrive.yml +0 -15
  22. data/lib/samples/gridfs.yml +3 -0
  23. data/lib/samples/gsheet.yml +4 -4
  24. data/lib/samples/jobtracker.yml +6 -0
  25. data/mobilize-base.gemspec +3 -3
  26. data/test/base_job_rows.yml +11 -0
  27. data/test/mobilize-base_test.rb +106 -0
  28. data/test/test_base_1.yml +3 -0
  29. data/test/test_helper.rb +0 -155
  30. metadata +24 -36
  31. data/lib/mobilize-base/extensions/time.rb +0 -20
  32. data/lib/mobilize-base/helpers/job_helper.rb +0 -54
  33. data/lib/mobilize-base/helpers/jobtracker_helper.rb +0 -143
  34. data/lib/mobilize-base/helpers/runner_helper.rb +0 -83
  35. data/lib/mobilize-base/helpers/stage_helper.rb +0 -38
  36. data/lib/samples/gfile.yml +0 -9
  37. data/test/fixtures/base1_stage1.in.yml +0 -10
  38. data/test/fixtures/integration_expected.yml +0 -25
  39. data/test/fixtures/integration_jobs.yml +0 -12
  40. data/test/fixtures/is_due.yml +0 -97
  41. data/test/integration/mobilize-base_test.rb +0 -57
  42. 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 parent
28
+ def status
29
+ #last stage status
19
30
  j = self
20
- u = j.runner.user
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 children
34
+ def active_stage
31
35
  j = self
32
- u = j.runner.user
33
- u.jobs.select do |job|
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
- #takes a hash of job parameters (name, active, trigger, stages)
41
- #and creates/updates a job with it
42
- def update_from_hash(hash)
40
+ def completed_at
43
41
  j = self
44
- #update top line params
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 is_due?
45
+ def failed_at
76
46
  j = self
77
- #working or inactive jobs are not due
78
- if j.is_working? or j.active == false
79
- return false
80
- end
47
+ j.active_stage.failed_at if j.active_stage
48
+ end
81
49
 
82
- #if job contains handlers not loaded by jobtracker, not due
83
- loaded_handlers = Jobtracker.config['extensions'].map{|m| m.split("-").last}
84
- job_handlers = j.stages.map{|s| s.handler}.uniq
85
- #base handlers are the ones in mobilize-base/handlers
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
- #once
91
- if j.trigger.strip.downcase=='once'
92
- #active and once means due
93
- return true
94
- end
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
- #depedencies
97
- if j.parent
98
- #if parent completed more recently than self, is due
99
- if j.parent.completed_at and (j.completed_at.nil? or j.parent.completed_at > j.completed_at)
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
- end
105
-
106
- #time based
107
- last_comp_time = j.completed_at
108
- #check trigger; strip the "every" from the front if present, change dot to space
109
- trigger = j.trigger.strip.gsub("every","").gsub("."," ").strip
110
- number, unit, operator, mark = trigger.split(" ").map{|t_node| t_node.downcase}
111
- #operator is not used
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 false
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 curr_time.wday==number and (last_comp_time.nil? or last_comp_time.to_date != curr_time.to_date)
130
- if mark
131
- #check if it already ran today
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 curr_time.day==number and (last_comp_time.nil? or last_comp_time.to_date != curr_time.to_date)
144
- if mark
145
- #check if it already ran today
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
- begin
33
- #make sure any updates to activity are processed first
34
- #as in when someone runs a "once" job that has completed
35
- r.update_gsheet(gdrive_slot)
36
- #read the jobs in the gsheet and update models with news
37
- r.read_gsheet(gdrive_slot)
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
- gsheet_hashes = gsheet_tsv.tsv_to_hash_array
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
- gsheet_hashes.each do |gsheet_hash|
111
+ gsheet_jobs.each_with_index do |rj,rj_i|
74
112
  #skip non-jobs or jobs without required values
75
- next if (gsheet_hash['name'].to_s.first == "#" or ['name','active','trigger','stage1'].select{|c| gsheet_hash[c].to_s.strip==""}.length>0)
76
- #find job w this name, or make one
77
- j = r.jobs.select{|rj| rj.name == gsheet_hash['name']}.first || Job.find_or_create_by_path("#{r.path}/#{gsheet_hash['name']}")
78
- j.update_from_hash(gsheet_hash)
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 |gsheet_hash_path|
85
- j = Job.where(:path=>gsheet_hash_path).first
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:#{gsheet_hash_path}")
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.to_f > j.runner.completed_at.to_f}
99
- upd_rows = upd_rows = upd_jobs.map do |j|
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 worker
169
+ def jobs(jname=nil)
114
170
  r = self
115
- Mobilize::Resque.find_worker_by_path(r.path)
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!