webapp_worker 0.0.1 → 0.0.2

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.markdown CHANGED
@@ -12,11 +12,73 @@ or use it in your Gemfile
12
12
 
13
13
  ## Quick Test
14
14
 
15
- ``
15
+ waw -e development -f jobs.yml (parses data)
16
+ waw -e development -f jobs.yml -j (parses and shows jobs)
17
+ waw -e development -f jobs.yml -n 1 (parses and shows next X number of jobs that will run)
18
+ waw -e development -f jobs.yml -r (parses and starts to run)
16
19
 
17
20
  ## Using in your webapp
18
21
 
19
-
22
+ Create a jobs yaml file.
23
+
24
+ ---
25
+ development:
26
+ mailto:
27
+ server_host_name:
28
+ - :command: "rake job:run"
29
+ :minute: 0-59/5
30
+
31
+ Load this file somewhere in your web app, like an initializer file.
32
+
33
+ jobs = "config/jobs.yml"
34
+
35
+ a = WebappWorker::Application.new(environment:"development",mailto:"")
36
+ a.parse_yaml(jobs)
37
+ a.run
38
+
39
+ You don't have to use a jobs file, you can specify the yaml or hash in the code above like this:
40
+
41
+ job = {:command=>"rake job:run", :minute=>"0-59/5", :hour=>"0-4/2", :day=>1, :month=>"0-12/1"}
42
+
43
+ a = WebappWorker::Application.new(environment:"development",mailto:"",jobs:[job])
44
+ a.run
45
+
46
+ ## Example Output of waw
47
+
48
+ $ waw -e local -f config/jobs.yml -j
49
+ Job File: config/jobs.yml
50
+
51
+ Host: localhost
52
+ Mailto:
53
+ Environment: development
54
+ Amount of Jobs: 9
55
+
56
+ Command to Run: rake job:run
57
+ Next Run: [2012-01-03 22:00:00 -0700]
58
+ Command to Run: rake job:run
59
+ Next Run: [2012-01-03 22:02:00 -0700]
60
+ Command to Run: rake job:run
61
+ Next Run: [2012-01-03 21:12:00 -0700]
62
+ Command to Run: rake job:run
63
+ Next Run: [2012-01-03 21:14:00 -0700]
64
+ Command to Run: rake job:run
65
+ Next Run: [2012-01-03 21:16:00 -0700]
66
+ Command to Run: rake job:run
67
+ Next Run: [2012-01-03 21:18:00 -0700]
68
+ Command to Run: rake job:run
69
+ Next Run: [2012-01-03 21:22:00 -0700]
70
+ Command to Run: rake job:run
71
+ Next Run: [2012-01-03 21:24:00 -0700]
72
+ Command to Run: rake job:run
73
+ Next Run: [2012-01-03 21:30:00 -0700]
74
+
75
+ ## Roadmap
76
+
77
+ - Process also needs to understand when to die and to start back up. (new version being used in the web app server)
78
+ - Start having the webapp worker registering to a central point or do UDP mutlicasting to find each other.
79
+ - Once self registering is enabled, webapp_workers need to communicate effectively.
80
+ - Once communication is esatablished webapp_workers need to do the scheduling for themselves.
81
+ - Spit out reports of the different jobs and how fast they run.
20
82
 
21
83
  ## Contributing
22
84
 
data/bin/waw CHANGED
@@ -54,5 +54,5 @@ elsif opts[:run] == false && opts[:jobs] == true
54
54
  end
55
55
  else
56
56
  puts "Running Jobs"
57
- a.run
57
+ a.run(opts[:debug],opts[:verbose])
58
58
  end
@@ -1,11 +1,27 @@
1
1
  require 'socket'
2
+ require 'timeout'
3
+ require 'open4'
4
+ require 'logger'
5
+
6
+ module Process
7
+ class << self
8
+ def alive?(pid)
9
+ begin
10
+ Process.kill(0, pid.to_i)
11
+ true
12
+ rescue Errno::ESRCH
13
+ false
14
+ end
15
+ end
16
+ end
17
+ end
2
18
 
3
19
  module WebappWorker
4
20
  class Application
5
- attr_accessor :hostname, :mailto, :environment, :jobs
21
+ attr_accessor :hostname, :mailto, :environment, :jobs, :file, :file_mtime
6
22
 
7
23
  def initialize(user_supplied_hash={})
8
- standard_hash = { hostname:"#{self.hostname}", mailto:"", environment:"local", jobs:"" }
24
+ standard_hash = { hostname:"#{self.hostname}", mailto:"", environment:"local", jobs:"", file:"" }
9
25
 
10
26
  user_supplied_hash = {} unless user_supplied_hash
11
27
  user_supplied_hash = standard_hash.merge(user_supplied_hash)
@@ -18,15 +34,34 @@ module WebappWorker
18
34
  end
19
35
 
20
36
  def parse_yaml(yaml)
21
- @mailto = (YAML.load_file(yaml))[@environment]["mailto"] unless @mailto
22
- @jobs = (YAML.load_file(yaml))[@environment][@hostname] unless @hostname.nil?
37
+ @file = yaml
38
+ @file_mtime = File.mtime(@file)
39
+ @mailto = (YAML.load_file(@file))[@environment]["mailto"] unless @mailto
40
+ @jobs = (YAML.load_file(@file))[@environment][@hostname] unless @hostname.nil?
23
41
  end
24
42
 
25
43
  def hostname
26
44
  return Socket.gethostname.downcase
27
45
  end
28
46
 
29
- def next_command_run?(til)
47
+ def next_command_run_time?
48
+ commands = {}
49
+ c = {}
50
+
51
+ @jobs.each do |job|
52
+ j = WebappWorker::Job.new(job)
53
+ commands.store(j.command,j.next_run?)
54
+ end
55
+ (commands.sort_by { |key,value| value }).collect { |key,value| c.store(key,value) }
56
+
57
+ c.each do |key,value|
58
+ return value[0]
59
+ end
60
+ end
61
+
62
+ def commands_to_run
63
+ self.check_file_modification_time
64
+
30
65
  commands = {}
31
66
  c = {}
32
67
  next_commands = {}
@@ -37,31 +72,180 @@ module WebappWorker
37
72
  end
38
73
  (commands.sort_by { |key,value| value }).collect { |key,value| c.store(key,value) }
39
74
 
40
- counter = 0
41
75
  c.each do |key,value|
42
76
  next_commands.store(key,value)
43
- counter = counter + 1
44
- break if counter >= til
45
77
  end
46
78
 
47
79
  return next_commands
48
80
  end
49
81
 
50
- def run
51
- #Going to need to do memory/process management, or fork processes not threads...
82
+ def check_file_modification_time
83
+ mtime = File.mtime(@file)
84
+
85
+ if mtime != @file_mtime
86
+ @file_mtime = mtime
87
+ self.parse_yaml(@file)
88
+ end
89
+ end
90
+
91
+ def check_for_directory
92
+ dir = "/tmp/webapp_worker"
93
+
94
+ if Dir.exists?(dir)
95
+ else
96
+ Dir.mkdir(dir, 0700)
97
+ end
98
+ end
99
+
100
+ def create_pid(logger)
101
+ logger.info "Creating Pid File at /tmp/webapp_worker/waw.pid"
102
+
103
+ File.open("/tmp/webapp_worker/waw.pid", 'w') { |f| f.write(Process.pid) }
104
+ $0="Web App Worker - Job File: #{@file}"
105
+
106
+ logger.info "Pid File created: #{Process.pid} at /tmp/webapp_worker/waw.pid"
107
+ end
108
+
109
+ def check_for_process(logger)
110
+ file = "/tmp/webapp_worker/waw.pid"
111
+
112
+ if File.exists?(file)
113
+ possible_pid = ""
114
+ pid_file = File.open(file, 'r').each { |f| possible_pid+= f }
115
+ pid_file.close
116
+
117
+ if Process.alive?(possible_pid)
118
+ puts "Already found webapp_worker running, pid is: #{possible_pid}, exiting..."
119
+ logger.fatal "Found webapp_worker already running with pid: #{possible_pid}, Pid File: #{file} exiting..."
120
+ exit 1
121
+ else
122
+ logger.warn "Found pid file, but no process running, recreating pid file with my pid: #{Process.pid}"
123
+ File.delete(file)
124
+ self.create_pid(logger)
125
+ end
126
+ else
127
+ self.create_pid(logger)
128
+ end
129
+
130
+ logger.info "Starting Webapp Worker"
131
+ end
132
+
133
+ def graceful_termination(logger)
134
+ stop_loop = true
135
+
136
+ begin
137
+ puts
138
+ puts "Graceful Termination started, waiting 60 seconds before KILL signal send"
139
+ logger.info "Graceful Termination started, waiting 60 seconds before KILL signal send"
140
+
141
+ Timeout::timeout(60) do
142
+ @command_processes.each do |pid,command|
143
+ logger.debug "Sending INT Signal to #{command} Process with PID: #{pid}"
144
+ begin
145
+ Process.kill("INT",pid.to_i)
146
+ rescue => error
147
+ end
148
+ end
149
+
150
+ @threads.each do |thread,command|
151
+ thread.join
152
+ end
153
+ end
154
+ rescue Timeout::Error
155
+ puts "Graceful Termination bypassed, killing processes and threads"
156
+ logger.info "Graceful Termination bypassed, killing processes and threads"
157
+
158
+ @command_processes.each do |pid,command|
159
+ logger.debug "Killing #{command} Process with PID: #{pid}"
160
+ begin
161
+ Process.kill("KILL",pid.to_i)
162
+ rescue => error
163
+ end
164
+ end
165
+
166
+ @threads.each do |thread,command|
167
+ logger.debug "Killing Command Thread: #{command}"
168
+ Thread.kill(thread)
169
+ end
170
+ end
171
+
172
+ puts "Stopping Webapp Worker"
173
+ logger.info "Stopping Webapp Worker"
174
+ file = "/tmp/webapp_worker/waw.pid"
175
+ File.delete(file)
176
+ exit 0
177
+ end
178
+
179
+ def run(debug=nil,verbose=nil)
180
+ self.check_for_directory
181
+
182
+ logger = Logger.new("/tmp/webapp_worker/#{@environment}.log", 5, 5242880)
183
+
184
+ if debug
185
+ logger.level = Logger::DEBUG
186
+ elsif verbose
187
+ logger.level = Logger::INFO
188
+ else
189
+ logger.level = Logger::WARN
190
+ end
191
+
52
192
  p = Process.fork do
53
- Signal.trap('HUP', 'IGNORE')
193
+ begin
194
+ self.check_for_process(logger)
195
+ rescue => error
196
+ puts error.inspect
197
+ logger.fatal error.inspect
198
+ end
54
199
 
200
+ @command_processes = {}
55
201
  @threads = {}
202
+ stop_loop = false
203
+
204
+ Signal.trap('HUP', 'IGNORE')
205
+
206
+ %w(INT QUIT TERM TSTP).each do |sig|
207
+ Signal.trap(sig) do
208
+ logger.warn "Received a #{sig} signal, stopping current commands."
209
+ self.graceful_termination(logger)
210
+ end
211
+ end
212
+
213
+ Signal.trap('USR1') do
214
+ version = WebappWorker::VERSION
215
+ puts
216
+ puts "Webapp Worker Version: #{version}"
217
+ logger.info "Received USR1 signal, sent version: #{version}"
218
+ end
219
+
220
+ Signal.trap('USR2') do
221
+ logger.level = Logger::DEBUG
222
+ puts
223
+ puts "Changed logger level to Debug"
224
+ logger.info "Changed logger level to Debug"
225
+ end
56
226
 
57
- loop do
227
+ #Signal.trap('STOP') do |s|
228
+ # #Stop Looping until
229
+ # stop_loop = true
230
+ # logger.warn "Received signal #{s}, pausing current loop."
231
+ #end
232
+ #
233
+ #Signal.trap('CONT') do
234
+ # #Start Looping again (catch throw?)
235
+ # stop_loop = false
236
+ # logger.warn "Received signal #{s}, starting current loop."
237
+ #end
238
+
239
+ logger.debug "Going into Loop"
240
+ until stop_loop
58
241
  @threads.each do |thread,command|
59
242
  if thread.status == false
243
+ logger.debug "Deleting Old Thread from Array of Jobs"
60
244
  @threads.delete(thread)
61
245
  end
62
246
  end
63
247
 
64
- data = self.next_command_run?(1)
248
+ data = self.commands_to_run
65
249
 
66
250
  data.each do |command,time|
67
251
  time = time[0]
@@ -69,15 +253,47 @@ module WebappWorker
69
253
  range = (time - now).to_i
70
254
 
71
255
  if @threads.detect { |thr,com| com == command }
72
- sleep(range) unless range <= 0
256
+ data.delete(command)
73
257
  else
74
258
  t = Thread.new do
259
+ logger.debug "Creating New Thread for command: #{command} - may need to sleep for: #{range} seconds"
75
260
  sleep(range) unless range <= 0
76
- `#{command}`
261
+ logger.debug "Running Command: #{command}"
262
+
263
+ pid, stdin, stdout, stderr = Open4::popen4 command
264
+ @command_processes.store(pid,command)
265
+
266
+ ignored, status = Process::waitpid2 pid
267
+
268
+ if status.to_i == 0
269
+ logger.debug "Completed Command: #{command}"
270
+ else
271
+ logger.fatal "Command: #{command} Failure! Exited with Status: #{status.to_i}, Standard Out and Error Below"
272
+ logger.fatal "STDOUT BELOW:"
273
+ stdout.each_line do |line|
274
+ logger.fatal line
275
+ end
276
+ logger.fatal "STDERR BELOW:"
277
+ stderr.each_line do |line|
278
+ logger.fatal line
279
+ end
280
+ logger.fatal "Command: #{command} Unable to Complete! Standard Out and Error Above"
281
+ end
77
282
  end
78
283
  @threads.store(t,command)
79
284
  end
80
285
  end
286
+
287
+ logger.debug Thread.list
288
+ logger.debug @threads.inspect
289
+ logger.debug @command_processes.inspect
290
+
291
+ time = self.next_command_run_time?
292
+ now = Time.now.utc
293
+ range = (time - now).to_i
294
+ range = range - 1
295
+ logger.debug "Sleeping for #{range} seconds after looping through all jobs found"
296
+ sleep(range) unless range <= 0
81
297
  end
82
298
  end
83
299
 
@@ -1,3 +1,3 @@
1
1
  module WebappWorker
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
7
7
  s.version = WebappWorker::VERSION
8
8
  s.authors = ["Nick Willever"]
9
9
  s.email = ["nickwillever@gmail.com"]
10
- s.homepage = ""
10
+ s.homepage = "https://nictrix.github.com/webapp_worker"
11
11
  s.summary = %q{Provides a worker for your webapp}
12
12
  s.description = %q{Allow the webapp to handle your workers, no need to use a job scheduler}
13
13
 
@@ -16,5 +16,6 @@ Gem::Specification.new do |s|
16
16
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
17
  s.require_paths = ["lib"]
18
18
 
19
- s.add_runtime_dependency 'trollop'
19
+ s.add_runtime_dependency 'trollop', '=1.16.2'
20
+ s.add_runtime_dependency 'open4', '=1.3.0'
20
21
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webapp_worker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,19 +9,30 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-12-15 00:00:00.000000000Z
12
+ date: 2012-01-16 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: trollop
16
- requirement: &81315550 !ruby/object:Gem::Requirement
16
+ requirement: &83276430 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
- - - ! '>='
19
+ - - =
20
20
  - !ruby/object:Gem::Version
21
- version: '0'
21
+ version: 1.16.2
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *81315550
24
+ version_requirements: *83276430
25
+ - !ruby/object:Gem::Dependency
26
+ name: open4
27
+ requirement: &83272260 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - =
31
+ - !ruby/object:Gem::Version
32
+ version: 1.3.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *83272260
25
36
  description: Allow the webapp to handle your workers, no need to use a job scheduler
26
37
  email:
27
38
  - nickwillever@gmail.com
@@ -40,7 +51,7 @@ files:
40
51
  - lib/webapp_worker/job.rb
41
52
  - lib/webapp_worker/version.rb
42
53
  - webapp_worker.gemspec
43
- homepage: ''
54
+ homepage: https://nictrix.github.com/webapp_worker
44
55
  licenses: []
45
56
  post_install_message:
46
57
  rdoc_options: []