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.
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!