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
@@ -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
|
@@ -81,16 +81,15 @@ module Mobilize
|
|
81
81
|
|
82
82
|
def Gsheet.write_temp(target_path,gdrive_slot,tsv)
|
83
83
|
#find and delete temp sheet, if any
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
temp_sheet =
|
84
|
+
temp_path = [target_path.gridsafe,"temp"].join("/")
|
85
|
+
temp_sheet = Gsheet.find_by_path(temp_path,gdrive_slot)
|
86
|
+
temp_sheet.delete if temp_sheet
|
87
|
+
#write data to temp sheet
|
88
|
+
temp_sheet = Gsheet.find_or_create_by_path(temp_path,gdrive_slot)
|
89
89
|
#this step has a tendency to fail; if it does,
|
90
90
|
#don't fail the stage, mark it as false
|
91
91
|
begin
|
92
|
-
|
93
|
-
temp_sheet.write(tsv,gdrive_user)
|
92
|
+
temp_sheet.write(tsv,Gdrive.owner_name)
|
94
93
|
rescue
|
95
94
|
return nil
|
96
95
|
end
|
@@ -109,7 +108,7 @@ module Mobilize
|
|
109
108
|
#only give the user edit permissions if they're the ones
|
110
109
|
#creating it
|
111
110
|
target_sheet = Gsheet.find_or_create_by_path(target_path,gdrive_slot)
|
112
|
-
target_sheet.spreadsheet.update_acl(u.email) unless target_sheet.spreadsheet.acl_entry(u.email).ie{|e| e and e.role=="owner"}
|
111
|
+
target_sheet.spreadsheet.update_acl(u.email,"writer") unless target_sheet.spreadsheet.acl_entry(u.email).ie{|e| e and e.role=="owner"}
|
113
112
|
target_sheet.delete_sheet1
|
114
113
|
end
|
115
114
|
#pass it crop param to determine whether to shrink target sheet to fit data
|
@@ -127,43 +126,28 @@ module Mobilize
|
|
127
126
|
s = Stage.where(:path=>stage_path).first
|
128
127
|
u = s.job.runner.user
|
129
128
|
crop = s.params['crop'] || true
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
end
|
153
|
-
Gdrive.unslot_worker_by_path(stage_path)
|
154
|
-
stderr = nil
|
155
|
-
s.update_status(stdout)
|
156
|
-
signal = 0
|
157
|
-
rescue => exc
|
158
|
-
if retries < Gdrive.max_file_write_retries
|
159
|
-
retries +=1
|
160
|
-
sleep Gdrive.file_write_retry_delay
|
161
|
-
else
|
162
|
-
stdout = nil
|
163
|
-
stderr = [exc.to_s,"\n",exc.backtrace.join("\n")].join
|
164
|
-
signal = 500
|
165
|
-
end
|
166
|
-
end
|
129
|
+
begin
|
130
|
+
#get tsv to write from stage
|
131
|
+
source = s.sources(gdrive_slot).first
|
132
|
+
raise "Need source for gsheet write" unless source
|
133
|
+
tsv = source.read(u.name,gdrive_slot)
|
134
|
+
raise "No data source found for #{source.url}" unless tsv
|
135
|
+
stdout = if tsv.to_s.length == 0
|
136
|
+
#soft error; no data to write. Stage will complete.
|
137
|
+
"Write skipped for #{s.target.url}"
|
138
|
+
else
|
139
|
+
Dataset.write_by_url(s.target.url,tsv,u.name,gdrive_slot,crop)
|
140
|
+
#update status
|
141
|
+
"Write successful for #{s.target.url}"
|
142
|
+
end
|
143
|
+
Gdrive.unslot_worker_by_path(stage_path)
|
144
|
+
stderr = nil
|
145
|
+
s.update_status(stdout)
|
146
|
+
signal = 0
|
147
|
+
rescue => exc
|
148
|
+
stdout = nil
|
149
|
+
stderr = [exc.to_s,"\n",exc.backtrace.join("\n")].join
|
150
|
+
signal = 500
|
167
151
|
end
|
168
152
|
return {'out_str'=>stdout, 'err_str'=>stderr, 'signal' => signal}
|
169
153
|
end
|
@@ -19,17 +19,13 @@ module Mobilize
|
|
19
19
|
def Resque.workers(state="all")
|
20
20
|
workers = ::Resque.workers.select{|w| w.queues.first == Resque.queue_name}
|
21
21
|
return workers if state == 'all'
|
22
|
-
working_workers = workers.select{|w| w.job['queue']
|
22
|
+
working_workers = workers.select{|w| w.job['queue']== Resque.queue_name}
|
23
23
|
return working_workers if state == 'working'
|
24
24
|
idle_workers = workers.select{|w| w.job['queue'].nil?}
|
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
|
29
|
-
w.job['payload'] and
|
30
|
-
w.job['payload']['class']!='Jobtracker' and
|
31
|
-
w.job['run_at'] < (Time.now.utc - Jobtracker.max_run_time)
|
32
|
-
end
|
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)}
|
33
29
|
return timeout_workers if state == 'timeout'
|
34
30
|
raise "invalid state #{state}"
|
35
31
|
end
|
@@ -117,16 +113,10 @@ module Mobilize
|
|
117
113
|
stage_path = f['payload']['args'].first
|
118
114
|
email = begin
|
119
115
|
s = Stage.where(:path=>stage_path).first
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
else
|
125
|
-
s.job.runner.user.email
|
126
|
-
end
|
127
|
-
rescue ScriptError, StandardError
|
128
|
-
#jobs without stages are sent to admins
|
129
|
-
[Gdrive.admin_group_name,Gdrive.domain].join("@")
|
116
|
+
s.job.runner.user.email
|
117
|
+
rescue
|
118
|
+
#jobs without stages are sent to first admin
|
119
|
+
Jobtracker.admin_emails.first
|
130
120
|
end
|
131
121
|
exc_to_s = f['error']
|
132
122
|
if fjobs[email].nil?
|
@@ -148,7 +138,10 @@ module Mobilize
|
|
148
138
|
|
149
139
|
def Resque.start_workers(count=1)
|
150
140
|
count.times do
|
151
|
-
|
141
|
+
dir_envs = "MOBILIZE_ENV=#{Base.env} " +
|
142
|
+
"MOBILIZE_CONFIG_DIR=#{Base.config_dir} " +
|
143
|
+
"MOBILIZE_LOG_DIR=#{Base.log_dir}"
|
144
|
+
"(cd #{Base.root};rake #{dir_envs} mobilize_base:work) >> #{Resque.log_path} 2>&1 &".bash
|
152
145
|
end
|
153
146
|
end
|
154
147
|
|
@@ -1,13 +1,152 @@
|
|
1
1
|
module Mobilize
|
2
2
|
module Jobtracker
|
3
|
-
|
4
|
-
|
3
|
+
def Jobtracker.config
|
4
|
+
Base.config('jobtracker')
|
5
|
+
end
|
6
|
+
|
7
|
+
#modify this to increase the frequency of request cycles
|
8
|
+
def Jobtracker.cycle_freq
|
9
|
+
Jobtracker.config['cycle_freq']
|
10
|
+
end
|
11
|
+
|
12
|
+
#frequency of notifications
|
13
|
+
def Jobtracker.notification_freq
|
14
|
+
Jobtracker.config['notification_freq']
|
15
|
+
end
|
16
|
+
|
17
|
+
def Jobtracker.runner_read_freq
|
18
|
+
Jobtracker.config['runner_read_freq']
|
19
|
+
end
|
20
|
+
|
21
|
+
#long running tolerance
|
22
|
+
def Jobtracker.max_run_time
|
23
|
+
Jobtracker.config['max_run_time']
|
24
|
+
end
|
25
|
+
|
26
|
+
def Jobtracker.admins
|
27
|
+
Jobtracker.config['admins']
|
28
|
+
end
|
29
|
+
|
30
|
+
def Jobtracker.admin_emails
|
31
|
+
Jobtracker.admins.map{|a| a['email'] }
|
32
|
+
end
|
33
|
+
|
34
|
+
def Jobtracker.worker
|
35
|
+
Resque.find_worker_by_path("jobtracker")
|
36
|
+
end
|
37
|
+
|
38
|
+
def Jobtracker.workers(state="all")
|
39
|
+
Resque.workers(state)
|
40
|
+
end
|
41
|
+
|
42
|
+
def Jobtracker.status
|
43
|
+
args = Jobtracker.get_args
|
44
|
+
return args['status'] if args
|
45
|
+
job = Resque.jobs.select{|j| j['args'].first=='jobtracker'}.first
|
46
|
+
return 'queued' if job
|
47
|
+
return 'stopped'
|
48
|
+
end
|
49
|
+
|
50
|
+
def Jobtracker.update_status(msg)
|
51
|
+
#Jobtracker has no persistent database state
|
52
|
+
Resque.set_worker_args_by_path("jobtracker",{'status'=>msg})
|
53
|
+
return true
|
54
|
+
end
|
55
|
+
|
56
|
+
def Jobtracker.restart
|
57
|
+
Jobtracker.stop!
|
58
|
+
Jobtracker.start
|
59
|
+
end
|
60
|
+
|
61
|
+
def Jobtracker.set_args(args)
|
62
|
+
Resque.set_worker_args(Jobtracker.worker,args)
|
63
|
+
return true
|
64
|
+
end
|
65
|
+
|
66
|
+
def Jobtracker.get_args
|
67
|
+
Resque.get_worker_args(Jobtracker.worker)
|
68
|
+
end
|
69
|
+
|
70
|
+
def Jobtracker.kill_workers
|
71
|
+
Resque.kill_workers
|
72
|
+
end
|
73
|
+
|
74
|
+
def Jobtracker.kill_idle_workers
|
75
|
+
Resque.kill_idle_workers
|
76
|
+
end
|
77
|
+
|
78
|
+
def Jobtracker.kill_idle_and_stale_workers
|
79
|
+
Resque.kill_idle_and_stale_workers
|
80
|
+
end
|
81
|
+
|
82
|
+
def Jobtracker.prep_workers
|
83
|
+
Resque.prep_workers
|
84
|
+
end
|
85
|
+
|
86
|
+
def Jobtracker.failures
|
87
|
+
Resque.failures
|
88
|
+
end
|
89
|
+
|
90
|
+
def Jobtracker.start
|
91
|
+
if Jobtracker.status!='stopped'
|
92
|
+
Jobtracker.update_status("Jobtracker still #{Jobtracker.status}")
|
93
|
+
else
|
94
|
+
#make sure that workers are running and at the right number
|
95
|
+
#Resque.prep_workers
|
96
|
+
#queue up the jobtracker (starts the perform method)
|
97
|
+
Jobtracker.enqueue!
|
98
|
+
end
|
99
|
+
return true
|
100
|
+
end
|
101
|
+
|
102
|
+
def Jobtracker.enqueue!
|
103
|
+
::Resque::Job.create(Resque.queue_name, Jobtracker, 'jobtracker',{})
|
104
|
+
end
|
105
|
+
|
106
|
+
def Jobtracker.restart!
|
107
|
+
Jobtracker.stop!
|
108
|
+
Jobtracker.start
|
109
|
+
return true
|
110
|
+
end
|
111
|
+
|
112
|
+
def Jobtracker.restart_workers!
|
113
|
+
Jobtracker.kill_workers
|
114
|
+
sleep 10
|
115
|
+
Jobtracker.prep_workers
|
116
|
+
Jobtracker.update_status("put workers back on the queue")
|
117
|
+
end
|
118
|
+
|
119
|
+
def Jobtracker.stop!
|
120
|
+
#send signal for Jobtracker to check for
|
121
|
+
Jobtracker.update_status('stopping')
|
122
|
+
sleep 5
|
123
|
+
i=0
|
124
|
+
while Jobtracker.status=='stopping'
|
125
|
+
Jobtracker.update_status("#{Jobtracker.to_s} still on queue, waiting")
|
126
|
+
sleep 5
|
127
|
+
i+=1
|
128
|
+
end
|
129
|
+
return true
|
130
|
+
end
|
131
|
+
|
132
|
+
def Jobtracker.last_notification
|
133
|
+
return Jobtracker.get_args["last_notification"] if Jobtracker.get_args
|
134
|
+
end
|
135
|
+
|
136
|
+
def Jobtracker.last_notification=(time)
|
137
|
+
Jobtracker.set_args({"last_notification"=>time})
|
138
|
+
end
|
139
|
+
|
140
|
+
def Jobtracker.notif_due?
|
141
|
+
last_duetime = Time.now.utc - Jobtracker.notification_freq
|
142
|
+
return (Jobtracker.last_notification.to_s.length==0 || Jobtracker.last_notification.to_datetime < last_duetime)
|
143
|
+
end
|
5
144
|
|
6
145
|
def Jobtracker.max_run_time_workers
|
7
146
|
#return workers who have been cranking away for 6+ hours
|
8
147
|
workers = Jobtracker.workers('working').select do |w|
|
9
|
-
w.job['
|
10
|
-
(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
|
11
150
|
end
|
12
151
|
return workers
|
13
152
|
end
|
@@ -35,36 +174,24 @@ module Mobilize
|
|
35
174
|
end
|
36
175
|
end.flatten.join("\n\n")
|
37
176
|
u = User.where(:name=>email.split("@").first).first
|
38
|
-
|
39
|
-
|
40
|
-
n['body'] += "\n\n#{runner_dst.http_url}" if runner_dst and runner_dst.http_url
|
41
|
-
end
|
177
|
+
runner_dst = Dataset.find_by_url("gsheet://#{u.runner.path}")
|
178
|
+
n['body'] += "\n\n#{runner_dst.http_url}" if runner_dst and runner_dst.http_url
|
42
179
|
n['to'] = email
|
43
|
-
n['bcc'] =
|
180
|
+
n['bcc'] = Jobtracker.admin_emails.join(",")
|
44
181
|
notifs << n
|
45
182
|
end
|
46
183
|
end
|
47
184
|
lws = Jobtracker.max_run_time_workers
|
48
185
|
if lws.length>0
|
49
|
-
bod = begin
|
50
|
-
lws.map{|w| w.job['payload']['args']}.first.join("\n")
|
51
|
-
rescue
|
52
|
-
"Failed to get job names"
|
53
|
-
end
|
54
186
|
n = {}
|
55
187
|
n['subject'] = "#{lws.length.to_s} max run time jobs"
|
56
|
-
n['body'] =
|
57
|
-
n['to'] =
|
188
|
+
n['body'] = lws.map{|w| %{spec:#{w['spec']} stg:#{w['stg']} runat:#{w['runat'].to_s}}}.join("\n\n")
|
189
|
+
n['to'] = Jobtracker.admin_emails.join(",")
|
58
190
|
notifs << n
|
59
191
|
end
|
60
192
|
#deliver each email generated
|
61
193
|
notifs.each do |notif|
|
62
|
-
|
63
|
-
Email.write(notif).deliver
|
64
|
-
rescue
|
65
|
-
#log email on failure
|
66
|
-
Jobtracker.update_status("Failed to deliver #{notif.to_s}")
|
67
|
-
end
|
194
|
+
Email.write(notif).deliver
|
68
195
|
end
|
69
196
|
#update notification time so JT knows to wait a while
|
70
197
|
Jobtracker.last_notification = Time.now.utc.to_s
|
@@ -116,5 +243,52 @@ module Mobilize
|
|
116
243
|
end.to_s.strip
|
117
244
|
Time.parse(deploy_time)
|
118
245
|
end
|
246
|
+
|
247
|
+
#test methods
|
248
|
+
def Jobtracker.restart_test_redis
|
249
|
+
Jobtracker.stop_test_redis
|
250
|
+
if !system("which redis-server")
|
251
|
+
raise "** can't find `redis-server` in your path, you need redis to run Resque and Mobilize"
|
252
|
+
end
|
253
|
+
"redis-server #{Base.root}/test/redis-test.conf".bash
|
254
|
+
end
|
255
|
+
|
256
|
+
def Jobtracker.stop_test_redis
|
257
|
+
processes = `ps -A -o pid,command | grep [r]edis-test`.split($/)
|
258
|
+
pids = processes.map { |process| process.split(" ")[0] }
|
259
|
+
puts "Killing test redis server..."
|
260
|
+
pids.each { |pid| Process.kill("TERM", pid.to_i) }
|
261
|
+
puts "removing redis db dump file"
|
262
|
+
sleep 5
|
263
|
+
`rm -f #{Base.root}/test/dump.rdb #{Base.root}/test/dump-cluster.rdb`
|
264
|
+
end
|
265
|
+
|
266
|
+
def Jobtracker.set_test_env
|
267
|
+
ENV['MOBILIZE_ENV']='test'
|
268
|
+
::Resque.redis="localhost:9736"
|
269
|
+
mongoid_config_path = "#{Base.root}/config/mobilize/mongoid.yml"
|
270
|
+
Mongoid.load!(mongoid_config_path, Base.env)
|
271
|
+
end
|
272
|
+
|
273
|
+
def Jobtracker.drop_test_db
|
274
|
+
Jobtracker.set_test_env
|
275
|
+
Mongoid.session(:default).collections.each do |collection|
|
276
|
+
unless collection.name =~ /^system\./
|
277
|
+
collection.drop
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def Jobtracker.build_test_runner(user_name)
|
283
|
+
Jobtracker.set_test_env
|
284
|
+
u = User.where(:name=>user_name).first
|
285
|
+
Jobtracker.update_status("delete old books and datasets")
|
286
|
+
# delete any old runner from previous test runs
|
287
|
+
gdrive_slot = Gdrive.owner_email
|
288
|
+
u.runner.gsheet(gdrive_slot).spreadsheet.delete
|
289
|
+
Jobtracker.update_status("enqueue jobtracker, wait 45s")
|
290
|
+
Mobilize::Jobtracker.start
|
291
|
+
sleep 45
|
292
|
+
end
|
119
293
|
end
|
120
294
|
end
|