background_queue 0.2.0

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 (91) hide show
  1. data/.document +5 -0
  2. data/.rspec +1 -0
  3. data/.rvmrc +48 -0
  4. data/Gemfile +19 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.md +69 -0
  7. data/Rakefile +42 -0
  8. data/TODO +2 -0
  9. data/VERSION +1 -0
  10. data/background_queue.gemspec +158 -0
  11. data/bin/bg_queue +26 -0
  12. data/lib/background_queue.rb +8 -0
  13. data/lib/background_queue/client.rb +96 -0
  14. data/lib/background_queue/client_lib/command.rb +36 -0
  15. data/lib/background_queue/client_lib/config.rb +109 -0
  16. data/lib/background_queue/client_lib/connection.rb +105 -0
  17. data/lib/background_queue/client_lib/job_handle.rb +19 -0
  18. data/lib/background_queue/command.rb +49 -0
  19. data/lib/background_queue/config.rb +118 -0
  20. data/lib/background_queue/server_lib/balanced_queue.rb +108 -0
  21. data/lib/background_queue/server_lib/config.rb +339 -0
  22. data/lib/background_queue/server_lib/event_connection.rb +133 -0
  23. data/lib/background_queue/server_lib/event_server.rb +35 -0
  24. data/lib/background_queue/server_lib/job.rb +252 -0
  25. data/lib/background_queue/server_lib/job_registry.rb +30 -0
  26. data/lib/background_queue/server_lib/lru.rb +193 -0
  27. data/lib/background_queue/server_lib/owner.rb +54 -0
  28. data/lib/background_queue/server_lib/priority_queue.rb +156 -0
  29. data/lib/background_queue/server_lib/queue_registry.rb +123 -0
  30. data/lib/background_queue/server_lib/server.rb +314 -0
  31. data/lib/background_queue/server_lib/sorted_workers.rb +52 -0
  32. data/lib/background_queue/server_lib/task.rb +79 -0
  33. data/lib/background_queue/server_lib/task_registry.rb +51 -0
  34. data/lib/background_queue/server_lib/thread_manager.rb +121 -0
  35. data/lib/background_queue/server_lib/worker.rb +18 -0
  36. data/lib/background_queue/server_lib/worker_balancer.rb +97 -0
  37. data/lib/background_queue/server_lib/worker_client.rb +94 -0
  38. data/lib/background_queue/server_lib/worker_thread.rb +70 -0
  39. data/lib/background_queue/utils.rb +40 -0
  40. data/lib/background_queue/worker/base.rb +46 -0
  41. data/lib/background_queue/worker/calling.rb +59 -0
  42. data/lib/background_queue/worker/config.rb +35 -0
  43. data/lib/background_queue/worker/environment.rb +70 -0
  44. data/lib/background_queue/worker/worker_loader.rb +94 -0
  45. data/lib/background_queue_server.rb +21 -0
  46. data/lib/background_queue_worker.rb +5 -0
  47. data/spec/background_queue/client_lib/command_spec.rb +75 -0
  48. data/spec/background_queue/client_lib/config_spec.rb +156 -0
  49. data/spec/background_queue/client_lib/connection_spec.rb +170 -0
  50. data/spec/background_queue/client_spec.rb +95 -0
  51. data/spec/background_queue/command_spec.rb +34 -0
  52. data/spec/background_queue/config_spec.rb +134 -0
  53. data/spec/background_queue/server_lib/balanced_queue_spec.rb +122 -0
  54. data/spec/background_queue/server_lib/config_spec.rb +443 -0
  55. data/spec/background_queue/server_lib/event_connection_spec.rb +190 -0
  56. data/spec/background_queue/server_lib/event_server_spec.rb +48 -0
  57. data/spec/background_queue/server_lib/integration/full_test_spec.rb +247 -0
  58. data/spec/background_queue/server_lib/integration/queue_integration_spec.rb +98 -0
  59. data/spec/background_queue/server_lib/integration/serialize_spec.rb +127 -0
  60. data/spec/background_queue/server_lib/job_registry_spec.rb +65 -0
  61. data/spec/background_queue/server_lib/job_spec.rb +525 -0
  62. data/spec/background_queue/server_lib/owner_spec.rb +33 -0
  63. data/spec/background_queue/server_lib/priority_queue_spec.rb +182 -0
  64. data/spec/background_queue/server_lib/server_spec.rb +353 -0
  65. data/spec/background_queue/server_lib/sorted_workers_spec.rb +122 -0
  66. data/spec/background_queue/server_lib/task_registry_spec.rb +69 -0
  67. data/spec/background_queue/server_lib/task_spec.rb +20 -0
  68. data/spec/background_queue/server_lib/thread_manager_spec.rb +106 -0
  69. data/spec/background_queue/server_lib/worker_balancer_spec.rb +127 -0
  70. data/spec/background_queue/server_lib/worker_client_spec.rb +196 -0
  71. data/spec/background_queue/server_lib/worker_thread_spec.rb +125 -0
  72. data/spec/background_queue/utils_spec.rb +27 -0
  73. data/spec/background_queue/worker/base_spec.rb +35 -0
  74. data/spec/background_queue/worker/calling_spec.rb +103 -0
  75. data/spec/background_queue/worker/environment_spec.rb +67 -0
  76. data/spec/background_queue/worker/worker_loader_spec.rb +103 -0
  77. data/spec/background_queue_spec.rb +7 -0
  78. data/spec/resources/config-client.yml +7 -0
  79. data/spec/resources/config-serialize.yml +12 -0
  80. data/spec/resources/config.yml +12 -0
  81. data/spec/resources/example_worker.rb +4 -0
  82. data/spec/resources/example_worker_with_error.rb +4 -0
  83. data/spec/resources/test_worker.rb +8 -0
  84. data/spec/shared/queue_registry_shared.rb +216 -0
  85. data/spec/spec_helper.rb +15 -0
  86. data/spec/support/default_task.rb +9 -0
  87. data/spec/support/private.rb +23 -0
  88. data/spec/support/simple_server.rb +28 -0
  89. data/spec/support/simple_task.rb +58 -0
  90. data/spec/support/test_worker_server.rb +205 -0
  91. metadata +254 -0
@@ -0,0 +1,52 @@
1
+ #we want a list of workers where the first in the list is the next worker to use.
2
+ #the next worker to use is the worker with the least number of running connections
3
+
4
+ class BackgroundQueue::ServerLib::SortedWorkers
5
+
6
+ attr_reader :worker_list
7
+
8
+ def initialize
9
+ @worker_list = []
10
+ end
11
+
12
+ #add the worker back in the correct position
13
+ def add_worker(worker)
14
+ idx = 0
15
+ while idx < @worker_list.length && @worker_list[idx].connections < worker.connections
16
+ idx += 1
17
+ end
18
+ if idx == 0
19
+ @worker_list.unshift(worker)
20
+ else
21
+ @worker_list.insert(idx, worker)
22
+ end
23
+ end
24
+
25
+ def remove_worker(worker)
26
+ @worker_list.delete(worker)
27
+ end
28
+
29
+
30
+ def adjust_worker(worker)
31
+ idx = @worker_list.index(worker)
32
+ raise "Worker not found (#{worker.inspect} not in #{@worker_list.inspect})" if idx.nil?
33
+ swap_idx = idx - 1
34
+ while swap_idx >= 0 && @worker_list[swap_idx].connections > worker.connections
35
+ swap_idx -= 1
36
+ end
37
+ swap_idx += 1
38
+ if swap_idx == idx #we didnt move forward, try backwards
39
+ swap_idx = idx + 1
40
+ while swap_idx < @worker_list.length && @worker_list[swap_idx].connections < worker.connections
41
+ swap_idx += 1
42
+ end
43
+ swap_idx -= 1
44
+ end
45
+ if swap_idx != idx
46
+ tmp = @worker_list[swap_idx]
47
+ @worker_list[swap_idx] = @worker_list[idx]
48
+ @worker_list[idx] = tmp
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,79 @@
1
+ module BackgroundQueue::ServerLib
2
+ class Task
3
+
4
+ attr_accessor :id
5
+ attr_accessor :priority
6
+
7
+ attr_accessor :owner_id
8
+ attr_accessor :job_id
9
+
10
+ attr_accessor :worker
11
+
12
+ attr_accessor :running
13
+
14
+ attr_accessor :options
15
+
16
+ def initialize(owner_id, job_id, id, priority, worker, params, options)
17
+ @owner_id = owner_id
18
+ @job_id = job_id
19
+ @id = id
20
+ @priority = priority
21
+ @worker = worker
22
+ @running = false
23
+ @options = options
24
+ @params = params
25
+ end
26
+
27
+ def to_json(dummy=true)
28
+ to_json_object(false).to_json
29
+ end
30
+
31
+ def to_json_object(full)
32
+ jo = {:owner_id=>@owner_id, :job_id=>@job_id, :id=>@id, :priority=>@priority, :worker=>@worker, :params=>@params }
33
+ if full
34
+ jo[:options] = @options
35
+ end
36
+ jo
37
+ end
38
+
39
+ def running?
40
+ @running
41
+ end
42
+
43
+ def domain
44
+ @options[:domain]
45
+ end
46
+
47
+ def set_job(job)
48
+ @job = job
49
+ end
50
+
51
+ def is_excluded_from_count?
52
+ @options[:exclude] == true
53
+ end
54
+
55
+ def synchronous?
56
+ @options[:synchronous] == true
57
+ end
58
+
59
+ def weighted?
60
+ @options[:weight] && @options[:weight] > 0
61
+ end
62
+
63
+ def weighted_percent
64
+ @options[:weight]
65
+ end
66
+
67
+ def initial_progress_caption
68
+ @options[:initial_progress_caption]
69
+ end
70
+
71
+ def set_worker_status(status)
72
+ raise "Task without job set" if @job.nil?
73
+ status[:task_id] = self.id
74
+ status[:exclude] = self.is_excluded_from_count?
75
+ status[:weight] = self.weighted_percent if self.weighted?
76
+ @job.set_worker_status(status)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,51 @@
1
+ module BackgroundQueue::ServerLib
2
+
3
+ #keep track if tasks already queued and running so if the same task comes in, we know to remove it.
4
+ class TaskRegistry
5
+
6
+ def initialize
7
+ @waiting_tasks = {}
8
+ @tasks = {}
9
+ end
10
+
11
+ def register(task)
12
+ existing_task = @tasks[task.id]
13
+ if existing_task.nil?
14
+ @tasks[task.id] = task
15
+ [:new, nil]
16
+ elsif existing_task.running?
17
+ register_waiting_task(task)
18
+ [:waiting, nil]
19
+ else
20
+ @tasks[task.id] = task
21
+ [:existing, existing_task]
22
+ end
23
+ end
24
+
25
+ def de_register(task_id)
26
+ @tasks.delete(task_id)
27
+ waiting = get_waiting_task(task_id)
28
+ if waiting
29
+ @tasks[task_id] = waiting
30
+ end
31
+ waiting
32
+ end
33
+
34
+ def register_waiting_task(task)
35
+ @waiting_tasks[task.id] = task
36
+ end
37
+
38
+ def get_waiting_task(task_id)
39
+ @waiting_tasks.delete(task_id)
40
+ end
41
+
42
+ def waiting_tasks
43
+ @waiting_tasks
44
+ end
45
+
46
+ def tasks
47
+ @tasks
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,121 @@
1
+ #make sure threads are schedules and the max number of threads is controlled
2
+ class BackgroundQueue::ServerLib::ThreadManager
3
+
4
+ attr_accessor :max_threads
5
+ attr_reader :running_threads
6
+
7
+ def initialize(server, max_threads)
8
+ @server = server
9
+ @max_threads = max_threads
10
+ @running_threads = 0
11
+ @mutex = Mutex.new
12
+ @condvar = ConditionVariable.new
13
+ @threads = []
14
+ end
15
+
16
+ def protect_access(&block)
17
+ @mutex.synchronize {
18
+ block.call
19
+ }
20
+ end
21
+
22
+ def control_access(&block)
23
+ @mutex.synchronize {
24
+ if @running_threads >= @max_threads && @server.running?
25
+ @running_threads -= 1
26
+ @condvar.wait(@mutex)
27
+ @running_threads += 1
28
+ end
29
+ block.call
30
+ }
31
+ end
32
+
33
+ #signal any waiting threads
34
+ #this should only be called from with a protect_access/control_access block
35
+ #will do nothing if there are already too many threads running
36
+ def signal_access
37
+ @condvar.signal unless @running_threads >= @max_threads
38
+ end
39
+
40
+ #wait for the condition
41
+ #must be called from within protect_access/control_access block
42
+ def wait_on_access
43
+ if @server.running?
44
+ @running_threads -= 1
45
+ #puts "waiting"
46
+ @condvar.wait(@mutex)
47
+ #puts "woken"
48
+ @running_threads += 1
49
+ end
50
+ end
51
+
52
+ def change_concurrency(max_threads)
53
+ @mutex.synchronize {
54
+ if max_threads > @max_threads
55
+ for i in @max_threads...max_threads
56
+ @condvar.signal
57
+ end
58
+ end
59
+ @max_threads = max_threads
60
+ }
61
+ end
62
+
63
+
64
+ def start(clazz)
65
+ @mutex.synchronize {
66
+ for i in 0...@max_threads
67
+ runner = clazz.new(@server)
68
+ @running_threads += 1
69
+ #puts "started thread, running=#{@running_threads}"
70
+ @threads << Thread.new(runner) { |runner|
71
+ begin
72
+ runner.run
73
+ rescue Exception=>e
74
+ @server.logger.error("Error in thread: #{e.message}")
75
+ @server.logger.debug(e.backtrace.join("\n"))
76
+ end
77
+ @mutex.synchronize {
78
+ @running_threads -= 1
79
+ #puts "finished thread, running=#{@running_threads}"
80
+ }
81
+ }
82
+ end
83
+ }
84
+ end
85
+
86
+ def wait(timeout_limit = 100)
87
+ #for thread in @threads
88
+ @mutex.synchronize {
89
+ @condvar.broadcast
90
+ }
91
+ #end
92
+ #while @running_threads > 0
93
+ # @mutex.synchronize {
94
+ # @condvar.signal
95
+ # }
96
+ # sleep(0.01)
97
+ #end
98
+ begin
99
+ Timeout::timeout(timeout_limit) {
100
+ for thread in @threads
101
+ thread.join
102
+ end
103
+ }
104
+ rescue Timeout::Error => te
105
+ for thread in @threads
106
+ begin
107
+ if thread.alive?
108
+ thread.raise BackgroundQueue::ServerLib::ThreadManager::ForcedStop.new("Timeout when forcing threads to stop")
109
+ end
110
+ rescue Exception=>e
111
+ #ignore
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ #Error raised when unable to load configuration
118
+ class ForcedStop < Exception
119
+
120
+ end
121
+ end
@@ -0,0 +1,18 @@
1
+ #this is a worker which keeps track of how many connections are getting used by the worker.
2
+ class BackgroundQueue::ServerLib::Worker
3
+
4
+ attr_accessor :uri
5
+ attr_accessor :connections
6
+ attr_accessor :offline
7
+
8
+ def initialize(uri)
9
+ @uri = uri
10
+ @connections = 0
11
+ @offline = false
12
+ end
13
+
14
+ def offline?
15
+ @offline
16
+ end
17
+
18
+ end
@@ -0,0 +1,97 @@
1
+ module BackgroundQueue::ServerLib
2
+ #make sure each worker gets its fair share of tasks
3
+ #track the number of active connections to use as the balancing metric
4
+ class WorkerBalancer
5
+
6
+ attr_reader :available_workers
7
+ attr_reader :offline_workers
8
+
9
+ def initialize(server)
10
+ @server = server
11
+ @mutex = Mutex.new
12
+ @offline_workers = []
13
+ @available_workers = SortedWorkers.new
14
+ for worker_config in server.config.workers.reverse
15
+ worker = Worker.new(worker_config.uri)
16
+ @available_workers.add_worker(worker)
17
+ end
18
+ end
19
+
20
+ #poll the workers that are marked as offline, and mark them online if the polling succeeded
21
+ def check_offline
22
+
23
+ workers_to_check = @mutex.synchronize { @offline_workers.clone }
24
+
25
+ for worker in workers_to_check
26
+ client = BackgroundQueue::ServerLib::WorkerClient.new(@server)
27
+ if client.send_request(worker, build_poll_task, @server.config.secret)
28
+ register_online(worker)
29
+ end
30
+ end
31
+ end
32
+
33
+ #get the worker with the least number of connections using it
34
+ def get_next_worker
35
+ @mutex.synchronize {
36
+ worker = @available_workers.worker_list.first
37
+ unless worker.nil?
38
+ register_start(worker)
39
+ end
40
+ worker
41
+ }
42
+ end
43
+
44
+ def finish_using_worker(worker, online)
45
+ @mutex.synchronize {
46
+ if online
47
+ register_finish(worker)
48
+ else
49
+ register_offline(worker)
50
+ end
51
+ }
52
+ end
53
+
54
+ private
55
+
56
+ def register_start(worker)
57
+ worker.connections += 1
58
+ @available_workers.adjust_worker(worker)
59
+ end
60
+
61
+ def register_finish(worker)
62
+ worker.connections -= 1
63
+ @available_workers.adjust_worker(worker)
64
+ end
65
+
66
+ def register_offline(worker)
67
+ worker.connections -= 1
68
+ unless worker.offline?
69
+ worker.offline = true
70
+ @available_workers.remove_worker(worker)
71
+ @offline_workers << worker
72
+ end
73
+ end
74
+
75
+ def register_online(worker)
76
+ if worker.offline?
77
+ worker.offline = false
78
+ @offline_workers.delete(worker)
79
+ @available_workers.add_worker(worker)
80
+ end
81
+ end
82
+
83
+ def build_poll_task
84
+ if @poll_task.nil?
85
+ @poll_task = BackgroundQueue::ServerLib::Task.new(:owner_id, :job_id, :id, 1, :poll_worker, {}, @server.config.system_task_options)
86
+ @poll_task.set_job(BackgroundQueue::ServerLib::NullJob.new)
87
+ end
88
+ @poll_task
89
+ end
90
+ end
91
+
92
+ class NullJob
93
+ def set_worker_status(status)
94
+ #do nothing
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,94 @@
1
+ require 'net/http'
2
+ require 'json'
3
+
4
+ module BackgroundQueue::ServerLib
5
+ #The client to a worker.
6
+ #Use http to connect to the worker, send the command, and process the streamed response of json encoded status updates.
7
+ class WorkerClient
8
+ def initialize(server)
9
+ @server = server
10
+ end
11
+
12
+ #send a request to the specified worker, passing the task and authenticating using the secret
13
+ def send_request(worker, task, secret)
14
+ @current_task = task
15
+ req = build_request(worker.uri, task, secret)
16
+ begin
17
+ Net::HTTP.start(worker.uri.host, worker.uri.port) do |server|
18
+ server.request(req) do |response|
19
+ read_response(worker, response, task)
20
+ end
21
+ end
22
+ true
23
+ rescue Exception=>e
24
+ @server.logger.error("Error sending request #{task.id} to worker: #{e.message}")
25
+ @server.logger.debug(e.backtrace.join("\n"))
26
+ return false
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def build_request(uri, task, secret)
33
+ req = Net::HTTP::Post.new(uri.path)
34
+ req.set_form_data({:task=>task.to_json, :auth=>secret, :server_port=>@server.config.address.port})
35
+ req["host"] = task.domain
36
+ req
37
+ end
38
+
39
+
40
+
41
+ def read_response(worker, http_response, task)
42
+ if http_response.code == "200"
43
+ http_response.read_body do |chunk|
44
+ process_chunk(chunk, task)
45
+ end
46
+ #the last chunk did not end in a newline... process it
47
+ unless @prev_chunk.nil?
48
+ process_line(@prev_chunk.strip, task)
49
+ @prev_chunk = nil
50
+ end
51
+ else
52
+ raise "Invalid response code (#{http_response.code}) when calling #{worker.uri.to_s}"
53
+ end
54
+ end
55
+
56
+ def process_chunk(chunk, task)
57
+ #puts "process_chunk: #{chunk}"
58
+ chunk.each_line do |line|
59
+ unless @prev_chunk.nil?
60
+ line = @prev_chunk + line
61
+ @prev_chunk = nil
62
+ end
63
+ if line[-1,1] == "\n" #it ends in a newline so its a complete line
64
+ process_line(line.strip, task)
65
+ else
66
+ @prev_chunk = line
67
+ end
68
+ end
69
+ end
70
+
71
+ def process_line(line, task)
72
+ hash_data = nil
73
+ begin
74
+ hash_data = JSON.load(line)
75
+ if hash_data.kind_of?(Hash)
76
+ set_worker_status(hash_data, task)
77
+ else
78
+ raise "Invalid Status Line (wrong datatype: #{hash_data.class.name}"
79
+ end
80
+ true
81
+ rescue Exception=>e
82
+ @server.logger.error("Error processing status line of task #{task.id}: #{e.message}")
83
+ @server.logger.debug(e.backtrace.join("\n"))
84
+ false
85
+ end
86
+ end
87
+
88
+ def set_worker_status(status, task)
89
+ status_map = BackgroundQueue::Utils::AnyKeyHash.new(status)
90
+ task.set_worker_status(status_map)
91
+ end
92
+
93
+ end
94
+ end