More job mgmt overhaul work - warvox - VoIP based wardialing tool, forked from rapid7/warvox.
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
---
(DIR) commit aa07abc9e7aaabc1723625d72daac6dd9e1edfcd
(DIR) parent 6da2bec72d9c7e6c7e8114fc4dfba5ac5e30598e
(HTM) Author: HD Moore <hd_moore@rapid7.com>
Date: Mon, 31 Dec 2012 19:10:51 -0600
More job mgmt overhaul work
Diffstat:
M db/migrate/20121228171549_initial_… | 1 +
M lib/warvox/jobs/analysis.rb | 162 ++++++++++++++-----------------
M lib/warvox/jobs/dialer.rb | 184 ++++++++++++++++---------------
3 files changed, 167 insertions(+), 180 deletions(-)
---
(DIR) diff --git a/db/migrate/20121228171549_initial_schema.rb b/db/migrate/20121228171549_initial_schema.rb
@@ -87,6 +87,7 @@ class InitialSchema < ActiveRecord::Migration
t.integer "provider_id", :null => false
t.boolean "answered"
t.boolean "busy"
+ t.text "error"
t.integer "audio_length"
t.integer "ring_length"
t.text "caller_id"
(DIR) diff --git a/lib/warvox/jobs/analysis.rb b/lib/warvox/jobs/analysis.rb
@@ -6,12 +6,7 @@ class Analysis < Base
require 'tempfile'
require 'open3'
- @@kissfft_loaded = false
- begin
- require 'kissfft'
- @@kissfft_loaded = true
- rescue ::LoadError
- end
+ require 'kissfft'
class Classifier
@@ -39,59 +34,91 @@ class Analysis < Base
'analysis'
end
- def initialize(job_id)
- @name = job_id
- if(not @@kissfft_loaded)
- raise RuntimeError, "The KissFFT module is not available, analysis failed"
- end
+ def initialize(job_id, conf)
+ @job_id = job_id
+ @conf = conf
+ @tasks = []
+ @calls = []
end
- def get_job
- ::DialJob.find(@name)
+ def stop
+ @calls = []
+ @tasks.each do |t|
+ t.kill rescue nil
+ end
+ @tasks = []
end
def start
- @status = 'active'
-
- begin
- start_processing()
-
- model = get_job
- model.processed = true
+ @calls = []
- db_save(model)
+ query = nil
- stop()
+ ::ActiveRecord::Base.connection_pool.with_connection {
- rescue ::Exception => e
- $stderr.puts "Exception in the job queue: #{e.class} #{e} #{e.backtrace}"
+ job = Job.find(@job_id)
+ if not job
+ raise RuntimeError, "The parent job no longer exists"
end
- end
- def stop
- @status = 'completed'
- end
+ case @conf[:scope]
+ when 'job'
+ if @conf[:force]
+ query = {:job_id => job.id, :answered => true, :busy => false}
+ else
+ query = {:job_id => job.id, :answered => true, :busy => false, :analysis_started_at => nil}
+ end
+ when 'project'
+ if @conf[:force]
+ query = {:project_id => job.project_id, :answered => true, :busy => false}
+ else
+ query = {:project_id => job.project_id, :answered => true, :busy => false, :analysis_started_at => nil}
+ end
+ when 'global'
+ if @conf[:force]
+ query = {:answered => true, :busy => false}
+ else
+ query = {:answered => true, :busy => false, :analysis_started_at => nil}
+ end
+ end
- def start_processing
- jobs = ::DialResult.where(:dial_job_id => @name, :processed => false, :completed => true, :busy => false).all
max_threads = WarVOX::Config.analysis_threads
+ last_update = Time.now
+
+ @total_calls = Call.count(:conditions => query)
+ @completed_calls = 0
- while(not jobs.empty?)
- threads = []
- output = []
- 1.upto(max_threads) do
- j = jobs.shift || break
- output << j
- threads << Thread.new { run_analyze_call(j) }
+ Call.find_each(:conditions => query) do |call|
+ while @tasks.length < max_threads
+ call.analysis_started_at = Time.now.utc
+ call.analysis_job_id = job.id
+ @tasks << Thread.new(call) { |c| ::ActiveRecord::Base.connection_pool.with_connection { run_analyze_call(c) }}
end
+ clear_stale_tasks
- # Wait for the threads to complete
- threads.each {|t| t.join}
+ # Update progress every 10 seconds or so
+ if Time.now.to_f - last_update.to_f > 10
+ update_progress((@completed_calls / @total_calls.to_f) * 100)
+ last_update = Time.now
+ end
- # Save the results to the database
- output.each {|r| db_save(r) if r.processed }
+ clear_zombies()
end
+
+ }
+ end
+
+ def clear_stale_tasks
+ # Remove dead threads from the task list
+ @tasks = @tasks.select{ |x| x.status }
+ IO.select(nil, nil, nil, 0.25)
+ end
+
+ def update_progress(pct)
+ ::ActiveRecord::Base.connection_pool.with_connection {
+ Job.update({ :progress => pct }, { :id => @job_id })
+ }
end
def run_analyze_call(dr)
@@ -121,8 +148,7 @@ class Analysis < Base
end
end
- dr.processed_at = Time.now
- dr.processed = true
+ dr.analysis_completed_at = Time.now.utc
rescue ::Interrupt
ensure
@@ -131,8 +157,9 @@ class Analysis < Base
end
mr.save
+ dr.save
- true
+ @completed_calls += 1
end
# Takes the raw file path as an argument, returns a hash
@@ -341,52 +368,5 @@ class Analysis < Base
end
-class CallAnalysis < Analysis
-
- @@kissfft_loaded = false
- begin
- require 'kissfft'
- @@kissfft_loaded = true
- rescue ::LoadError
- end
-
- def type
- 'call_analysis'
- end
-
- def initialize(result_id)
- @name = result_id
- if(not @@kissfft_loaded)
- raise RuntimeError, "The KissFFT module is not available, analysis failed"
- end
- end
-
- def get_job
- ::DialResult.find(@name)
- end
-
- def start
- @status = 'active'
-
- begin
- start_processing()
- stop()
- rescue ::Exception => e
- $stderr.puts "Exception in the job queue: #{e.class} #{e} #{e.backtrace}"
- end
- end
-
- def stop
- @status = 'completed'
- end
-
- def start_processing
- r = get_job()
- return if not r.completed
- return if r.busy
- analyze_call(r)
- end
-end
-
end
end
(DIR) diff --git a/lib/warvox/jobs/dialer.rb b/lib/warvox/jobs/dialer.rb
@@ -8,18 +8,21 @@ class Dialer < Base
'dialer'
end
- def initialize(job_id)
- @name = job_id
- @job = get_job
- @range = @job.range
- @seconds = @job.seconds
- @lines = @job.lines
+ def initialize(job_id, conf)
+ @job_id = job_id
+ @conf = conf
+ @range = @conf[:range]
+ @seconds = @conf[:seconds]
+ @lines = @conf[:lines]
@nums = shuffle_a(WarVOX::Phone.crack_mask(@range))
+ @tasks = []
+ @provs = get_providers
+
# CallerID modes (SELF or a mask)
- @cid_self = @job.cid_mask == 'SELF'
+ @cid_self = @conf[:cid_mask] == 'SELF'
if(not @cid_self)
- @cid_range = WarVOX::Phone.crack_mask(@job.cid_mask)
+ @cid_range = WarVOX::Phone.crack_mask(@conf[:cid_mask])
end
end
@@ -44,52 +47,34 @@ class Dialer < Base
def get_providers
res = []
- ::Provider.where(:enabled => true).all.each do |prov|
- info = {
- :name => prov.name,
- :id => prov.id,
- :port => prov.port,
- :host => prov.host,
- :user => prov.user,
- :pass => prov.pass,
- :lines => prov.lines
- }
- 1.upto(prov.lines) {|i| res.push(info) }
- end
+ ::ActiveRecord::Base.connection_pool.with_connection {
+ ::Provider.where(:enabled => true).all.each do |prov|
+ info = {
+ :name => prov.name,
+ :id => prov.id,
+ :port => prov.port,
+ :host => prov.host,
+ :user => prov.user,
+ :pass => prov.pass,
+ :lines => prov.lines
+ }
+ 1.upto(prov.lines) {|i| res.push(info) }
+ end
+ }
shuffle_a(res)
end
- def get_job
- ::DialJob.find(@name)
- end
-
- def start
- begin
-
- model = get_job
- model.status = 'active'
- model.started_at = Time.now
- db_save(model)
-
- start_dialing()
-
- stop()
-
- rescue ::Exception => e
- $stderr.puts "Exception in the job queue: #{$e.class} #{e} #{e.backtrace}"
- end
- end
def stop
- @status = 'completed'
- model = get_job
- model.status = 'completed'
- model.completed_at = Time.now
- db_save(model)
+ @nums = []
+ @tasks.each do |t|
+ t.kill rescue nil
+ end
+ @tasks = []
end
- def start_dialing
+ def start
# Scrub all numbers matching the blacklist
list = WarVOX::Config.blacklist_load
list.each do |b|
@@ -102,24 +87,19 @@ class Dialer < Base
end
end
+ last_update = Time.now
@nums_total = @nums.length
- while(@nums.length > 0)
- @calls = []
- @provs = get_providers
- tasks = []
- max_tasks = [@provs.length, @lines].min
- 1.upto(max_tasks) do
- tasks << Thread.new do
+ max_tasks = [@provs.length, @lines].min
- Thread.current.kill if @nums.length == 0
- Thread.current.kill if @provs.length == 0
+ while(@nums.length > 0)
+ while( @tasks.length < max_tasks ) do
+ tnum = @nums.shift
+ break unless tnum
- num = @nums.shift
- prov = @provs.shift
+ tprov = allocate_provider
- Thread.current.kill if not num
- Thread.current.kill if not prov
+ @tasks << Thread.new(tnum,tprov) do |num,prov|
out_fd = Tempfile.new("rawfile")
out = out_fd.path
@@ -165,30 +145,36 @@ class Dialer < Base
end
end
- res = ::DialResult.new
- res.number = num
- res.cid = cid
- res.dial_job_id = @name
- res.provider_id = prov[:id]
- res.completed = (fail == 0) ? true : false
- res.busy = (busy == 1) ? true : false
- res.seconds = (byte / 16000) # 8khz @ 16-bit
- res.ringtime = ring
- res.processed = false
- res.save
-
- if(File.exists?(out))
- File.open(out, "rb") do |fd|
- med = res.media
- med.audio = fd.read(fd.stat.size)
- med.save
+ :ActiveRecord::Base.connection_pool.with_connection do
+ job = Job.find(@job_id)
+ if not job
+ raise RuntimeError, "The parent job is not available"
end
- end
- out_fd.close
- ::FileUtils.rm_f(out)
+ res = ::Call.new
+ res.number = num
+ res.job_id = job.id
+ res.project_id = job.project_id
+ res.provider_id = prov[:id]
+ res.answered = (fail == 0) ? true : false
+ res.busy = (busy == 1) ? true : false
+ res.audio_seconds = (byte / 16000) # 8khz @ 16-bit
+ res.ring_seconds = ring
+ res.caller_id = cid
+
+ res.save
+
+ if(File.exists?(out))
+ File.open(out, "rb") do |fd|
+ med = res.media
+ med.audio = fd.read(fd.stat.size)
+ med.save
+ end
+ end
- @calls << res
+ out_fd.close
+ ::FileUtils.rm_f(out)
+ end
rescue ::Exception => e
$stderr.puts "ERROR: #{e.class} #{e} #{e.backtrace} #{num} #{prov.inspect}"
@@ -198,24 +184,44 @@ class Dialer < Base
# END NEW THREAD
end
# END SPAWN THREADS
- tasks.map{|t| t.join if t}
- # Iterate through the results
- @calls.each do |r|
- db_save(r)
- end
+ clear_stale_tasks
- # Update the progress bar
- model = get_job
- model.progress = ((@nums_total - @nums.length) / @nums_total.to_f) * 100
- db_save(model)
+ # Update progress every 10 seconds or so
+ if Time.now.to_f - last_update.to_f > 10
+ update_progress(((@nums_total - @nums.length) / @nums_total.to_f) * 100)
+ last_update = Time.now.to_f
+ end
clear_zombies()
end
+ while @tasks.length > 0
+ clear_stale_tasks
+ end
+
# ALL DONE
end
+ def clear_stale_tasks
+ # Remove dead threads from the task list
+ @tasks = @tasks.select{ |x| x.status }
+ IO.select(nil, nil, nil, 0.25)
+ end
+
+ def update_progress(pct)
+ ::ActiveRecord::Base.connection_pool.with_connection {
+ Job.update({ :progress => pct }, { :id => @job_id })
+ }
+ end
+
+ def allocate_provider
+ @prov_idx ||= 0
+ prov = @provs[ @prov_idx % @provs.length ]
+ @prov_idx += 1
+ prov
+ end
+
end
end
end