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,339 @@
1
+ require 'uri'
2
+ require 'ipaddress'
3
+
4
+ module BackgroundQueue::ServerLib
5
+
6
+ #The server configuration which is stored as a YAML file containing a root key for each environments configuration, much like database.yml.
7
+ #
8
+ #Example
9
+ #=======
10
+ #
11
+ # development:
12
+ # address:
13
+ # host: 127.0.0.1
14
+ # port: 3000
15
+ # workers:
16
+ # - http://127.0.0.1:801/background_queue
17
+ # secret: this_is_used_to_make_sure_it_is_secure
18
+ # task_file: /path/to/file/to/save/running/tasks
19
+ # production:
20
+ # address:
21
+ # host: 0.0.0.0
22
+ # port: 3000
23
+ # connections_per_worker: 10
24
+ # workers:
25
+ # - http://192.168.3.1:801/background_queue
26
+ # - http://192.168.3.2:801/background_queue
27
+ # secret: this_is_used_to_make_sure_it_is_secure
28
+ # system_task_options:
29
+ # domain: the_default_domain
30
+ # jobs
31
+ # - cron: "0 22 * * 1-5"
32
+ # worker: some_worker
33
+ # args:
34
+ # arg1: 22
35
+ # arg2: "hello"
36
+ class Config < BackgroundQueue::Config
37
+
38
+ #the list of workers that are called using http
39
+ attr_reader :workers
40
+
41
+
42
+ #the shared secret to make sure the worker is not called directly from the internet
43
+ attr_reader :secret
44
+
45
+ #a path where tasks are saved when the server shuts down, and loaded when it starts back up. This will store tasks being lost when restarting the server.
46
+ attr_reader :task_file
47
+
48
+ #an array of scheduled jobs
49
+ attr_reader :jobs
50
+
51
+ #the address to listen on
52
+ attr_reader :address
53
+
54
+ #the number of connections allowed for each active worker
55
+ attr_reader :connections_per_worker
56
+
57
+ #used for polling task and jobs. Should include a domain entry if your worker uses domain lookups
58
+ attr_reader :system_task_options
59
+
60
+ #load the configration using a hash just containing the environment
61
+ def self.load_hash(env_config, path)
62
+ BackgroundQueue::ServerLib::Config.new(
63
+ build_worker_entries(env_config, path),
64
+ get_secret_entry(env_config, path),
65
+ get_address_entry(env_config, path),
66
+ get_connections_per_worker_entry(env_config, path),
67
+ get_jobs_entry(env_config, path),
68
+ get_system_task_options_entry(env_config, path),
69
+ get_task_file_entry(env_config, path)
70
+ )
71
+ end
72
+
73
+ class << self
74
+ private
75
+
76
+ def build_worker_entries(env_config, path)
77
+ entries = []
78
+ workers_entry = BackgroundQueue::Utils.get_hash_entry(env_config, :workers)
79
+ if workers_entry && workers_entry.kind_of?(Array)
80
+ workers_entry.each_with_index do |entry, index|
81
+ begin
82
+ entries << BackgroundQueue::ServerLib::Config::Worker.new(entry)
83
+ rescue Exception=>e
84
+ raise BackgroundQueue::LoadError, "Error loading 'worker' entry (#{index + 1}) from background queue server configuration file #{full_path(path)}: #{e.message}"
85
+ end
86
+ end
87
+ elsif workers_entry
88
+ raise BackgroundQueue::LoadError, "Error loading 'workers' entries configuration file #{full_path(path)}: invalid data type (#{workers_entry.class.name}), expecting Array"
89
+ else
90
+ raise BackgroundQueue::LoadError, "Missing 'workers' in background queue server configuration file #{full_path(path)}"
91
+ end
92
+ entries
93
+ end
94
+
95
+ def get_secret_entry(env_config, path)
96
+ secret_entry = BackgroundQueue::Utils.get_hash_entry(env_config, :secret)
97
+ if secret_entry && secret_entry.kind_of?(String)
98
+ secret_entry.strip!
99
+ if secret_entry.length < 20
100
+ raise BackgroundQueue::LoadError, "Error loading 'secret' entry in background queue server configuration file #{full_path(path)}: length too short (must be at least 20 characters long)"
101
+ end
102
+ secret_entry
103
+ elsif secret_entry
104
+ raise BackgroundQueue::LoadError, "Error loading 'secret' entry in background queue server configuration file #{full_path(path)}: invalid data type (#{secret_entry.class.name}), expecting String"
105
+ else
106
+ raise BackgroundQueue::LoadError, "Missing 'secret' entry in background queue server configuration file #{full_path(path)}"
107
+ end
108
+ end
109
+
110
+ def get_address_entry(env_config, path)
111
+ begin
112
+ BackgroundQueue::ServerLib::Config::Address.new(BackgroundQueue::Utils.get_hash_entry(env_config, :address))
113
+ rescue Exception=>e
114
+ raise BackgroundQueue::LoadError, "Error loading 'address' entry in background queue server configuration file #{full_path(path)}: #{e.message}"
115
+ end
116
+ end
117
+
118
+ def get_connections_per_worker_entry(env_config, path)
119
+ connections_per_worker_entry = BackgroundQueue::Utils.get_hash_entry(env_config, :connections_per_worker)
120
+ if connections_per_worker_entry && connections_per_worker_entry.kind_of?(Integer)
121
+ connections_per_worker_entry
122
+ elsif connections_per_worker_entry
123
+ raise BackgroundQueue::LoadError, "Error loading 'connections_per_worker' entry in background queue server configuration file #{full_path(path)}: invalid data type (#{connections_per_worker_entry.class.name}), expecting Integer"
124
+ else
125
+ raise BackgroundQueue::LoadError, "Missing 'connections_per_worker' entry in background queue server configuration file #{full_path(path)}"
126
+ end
127
+ end
128
+
129
+ def get_system_task_options_entry(env_config, path)
130
+ opts_entry = BackgroundQueue::Utils.get_hash_entry(env_config, :system_task_options)
131
+ return {} if opts_entry.nil?
132
+ if opts_entry.kind_of?(Hash)
133
+ opts_entry
134
+ else
135
+ raise BackgroundQueue::LoadError, "Error loading 'system_task_options' entry in background queue server configuration file #{full_path(path)}: invalid data type (#{opts_entry.class.name}), expecting Hash (of options)"
136
+ end
137
+ end
138
+
139
+ def get_jobs_entry(env_config, path)
140
+ jobs_entry = BackgroundQueue::Utils.get_hash_entry(env_config, :jobs)
141
+ return [] if jobs_entry.nil?
142
+ if jobs_entry.kind_of?(Array)
143
+ retval = []
144
+ for job in jobs_entry
145
+ begin
146
+ retval << BackgroundQueue::ServerLib::Config::Job.new(job)
147
+ rescue Exception=>e
148
+ raise BackgroundQueue::LoadError, "Error loading 'jobs' entry in background queue server configuration file #{full_path(path)}: #{e.message}"
149
+ end
150
+ end
151
+ retval
152
+ else
153
+ raise BackgroundQueue::LoadError, "Error loading 'jobs' entry in background queue server configuration file #{full_path(path)}: invalid data type (#{jobs_entry.class.name}), expecting Array (of jobs)"
154
+ end
155
+ end
156
+
157
+ def get_task_file_entry(env_config, path)
158
+ task_file = BackgroundQueue::Utils.get_hash_entry(env_config, :task_file)
159
+ if task_file && task_file.kind_of?(String)
160
+ task_file.strip!
161
+ #make sure the file exists of the directory is writable
162
+ if !File.exist?(task_file)
163
+ dir = File.dirname(task_file)
164
+ if !File.exist?(dir)
165
+ #check if we can create the directory
166
+ begin
167
+ FileUtils.mkdir_p dir
168
+ rescue Exception=>e
169
+ raise BackgroundQueue::LoadError, "Error loading 'task_file' entry in background queue server configuration file #{full_path(path)}: unable to create directory #{dir} (#{e.message})"
170
+ end
171
+ else
172
+ #check if we can write in the directory
173
+ begin
174
+ FileUtils.touch task_file
175
+ rescue Exception=>e
176
+ raise BackgroundQueue::LoadError, "Error loading 'task_file' entry in background queue server configuration file #{full_path(path)}: unable to write to file #{task_file} (#{e.message})"
177
+ end
178
+ FileUtils.rm task_file
179
+ end
180
+ end
181
+ task_file
182
+ elsif task_file
183
+ raise BackgroundQueue::LoadError, "Error loading 'task_file' entry in background queue server configuration file #{full_path(path)}: Invalid data type (#{task_file.class.name}), expecting String"
184
+ else
185
+ nil
186
+ end
187
+ end
188
+ end
189
+
190
+
191
+ #do not call this directly, use a load_* method
192
+ def initialize(workers, secret, address, connections_per_worker, jobs, system_task_options, task_file)
193
+ @workers = workers
194
+ @secret = secret
195
+ @address = address
196
+ @connections_per_worker = connections_per_worker
197
+ @jobs = jobs
198
+ @system_task_options = system_task_options
199
+ @task_file = task_file
200
+ end
201
+
202
+ class Address
203
+ attr_reader :host
204
+ attr_reader :port
205
+
206
+
207
+ def initialize(config_entry)
208
+ if config_entry.nil?
209
+ @host = "0.0.0.0"
210
+ @port = BackgroundQueue::Config::DEFAULT_PORT
211
+ else
212
+ port = BackgroundQueue::Utils.get_hash_entry(config_entry, :port)
213
+ if port.nil?
214
+ @port = BackgroundQueue::Config::DEFAULT_PORT
215
+ elsif port.kind_of?(Numeric)
216
+ @port = port.to_i
217
+ elsif port.kind_of?(String)
218
+ if port.to_s.strip == port.to_s.to_i.to_s
219
+ @port = port.to_i
220
+ else
221
+ raise "Invalid port: #{port}"
222
+ end
223
+ else
224
+ raise "Invalid port: should be number or string"
225
+ end
226
+ if @port <= 0
227
+ raise "Invalid port: must be greater then zero"
228
+ end
229
+ host = BackgroundQueue::Utils.get_hash_entry(config_entry, :host)
230
+ if host.nil?
231
+ @host = "0.0.0.0"
232
+ elsif host.kind_of?(String)
233
+ if IPAddress.valid? host
234
+ @host = host
235
+ else
236
+ raise "Invalid host: #{host}"
237
+ end
238
+ else
239
+ raise "Invalid host: should be string"
240
+ end
241
+ end
242
+ end
243
+
244
+ end
245
+
246
+ #A server entry in the configuration
247
+ class Worker
248
+
249
+ attr_reader :uri
250
+
251
+ def initialize(config_entry)
252
+ if config_entry.nil?
253
+ raise BackgroundQueue::LoadError, "Missing worker url"
254
+ elsif config_entry.kind_of?(String)
255
+ raise BackgroundQueue::LoadError, "Missing worker url" if config_entry.strip.length == 0
256
+ begin
257
+ @uri = URI.parse(config_entry)
258
+ rescue URI::InvalidURIError
259
+ raise BackgroundQueue::LoadError, "Invalid worker url (#{config_entry})"
260
+ end
261
+ else
262
+ raise BackgroundQueue::LoadError, "Invalid data type (#{config_entry.class.name}), expecting String (as a url)"
263
+ end
264
+ end
265
+
266
+ def url
267
+ @uri.to_s
268
+ end
269
+ end
270
+
271
+ class Job
272
+
273
+
274
+ attr_accessor :at
275
+ attr_accessor :in
276
+ attr_accessor :cron
277
+ attr_accessor :every
278
+ attr_accessor :type
279
+ attr_accessor :args
280
+
281
+ def initialize(job_entry)
282
+ raise "Empty Job Entry" if job_entry.nil?
283
+ @at = BackgroundQueue::Utils.get_hash_entry(job_entry, :at)
284
+ @in = BackgroundQueue::Utils.get_hash_entry(job_entry, :in)
285
+ @cron = BackgroundQueue::Utils.get_hash_entry(job_entry, :cron)
286
+ @every = BackgroundQueue::Utils.get_hash_entry(job_entry, :every)
287
+ if !@at.nil?
288
+ @type = :at
289
+ elsif !@in.nil?
290
+ @type = :in
291
+ elsif !@cron.nil?
292
+ @type=:cron
293
+ elsif !@every.nil?
294
+ @type=:every
295
+ else
296
+ raise "Job is missing timer designation (at, in or cron)"
297
+ end
298
+ @worker = BackgroundQueue::Utils.get_hash_entry(job_entry, :worker)
299
+ raise "Job is missing worker entry" if @worker.nil?
300
+
301
+ @args = {}
302
+ args_entry = BackgroundQueue::Utils.get_hash_entry(job_entry, :args)
303
+ unless args_entry.nil?
304
+ raise "Invalid 'args' entry in job: expecting Hash of arguments, got #{args_entry.class.name}" unless args_entry.kind_of?(Hash)
305
+ @args = args_entry
306
+ end
307
+
308
+ end
309
+
310
+ def schedule(scheduler, server)
311
+ case @type
312
+ when :at
313
+ scheduler.at @at do
314
+ run(server)
315
+ end
316
+ when :in
317
+ scheduler.in @in do
318
+ run(server)
319
+ end
320
+ when :cron
321
+ scheduler.cron @cron do
322
+ run(server)
323
+ end
324
+ when :every
325
+ scheduler.every @every do
326
+ run(server)
327
+ end
328
+ end
329
+ end
330
+
331
+ def run(server)
332
+ task = BackgroundQueue::ServerLib::Task.new(:system, :scheduled, self.object_id, 2, @worker, @args, server.config.system_task_options)
333
+ server.task_queue.add_task(task)
334
+ end
335
+
336
+ end
337
+ end
338
+
339
+ end
@@ -0,0 +1,133 @@
1
+ require 'eventmachine'
2
+
3
+ module BackgroundQueue::ServerLib
4
+ class EventConnection < EventMachine::Connection
5
+
6
+ attr_accessor :server
7
+
8
+ STAGE_LENGTH = 0
9
+ STAGE_BODY = 1
10
+
11
+ MAX_BODY_LENGTH = 9999999
12
+
13
+
14
+ def post_init
15
+ @data = ""
16
+ @length = 0
17
+ @stage = STAGE_LENGTH
18
+ end
19
+
20
+ def receive_data(data)
21
+ @data << data
22
+ if @stage == STAGE_LENGTH
23
+ if @data.length >= 6
24
+ s_header = @data.slice!(0,6)
25
+ version, length = s_header.unpack("SL")
26
+
27
+ if version == 1
28
+ @length = length
29
+ @stage = STAGE_BODY
30
+ if length > MAX_BODY_LENGTH || length <= 0
31
+ raise "Invalid length: #{length}"
32
+ end
33
+ else
34
+ raise "Invalid header version: #{version}"
35
+ end
36
+ end
37
+ end
38
+
39
+ if @stage == STAGE_BODY && @data.length == @length
40
+ #body received
41
+ process_data(@data)
42
+ end
43
+ end
44
+
45
+ def process_data(data)
46
+ begin
47
+ cmd = BackgroundQueue::Command.from_buf(data)
48
+ result = process_command(cmd)
49
+ send_result(result)
50
+ rescue Exception=>e
51
+ @server.logger.error("Error processing command: #{e.message}")
52
+ send_error(e.message)
53
+ end
54
+ end
55
+
56
+ def send_result(command)
57
+ send(command.to_buf)
58
+ end
59
+
60
+ def send_error(message)
61
+ send_result(build_simple_command(:error, message))
62
+ end
63
+
64
+ def build_simple_command(type, message)
65
+ BackgroundQueue::Command.new(type, {}, {:message=>message})
66
+ end
67
+
68
+ def send(data)
69
+ data_with_header = [1, data.length, data].pack("SLZ#{data.length}")
70
+ send_data(data_with_header)
71
+ end
72
+
73
+ def process_command(command)
74
+ case command.code.to_s
75
+ when 'add_task'
76
+ process_add_task_command(command)
77
+ when 'add_tasks'
78
+ process_add_tasks_command(command)
79
+ when 'remove_tasks'
80
+ process_remove_tasks_command(command)
81
+ when 'get_status'
82
+ process_get_status_command(command)
83
+ when 'stats'
84
+ process_stats_command(command)
85
+ else
86
+ raise "Unknown command: #{command.code.inspect}"
87
+ end
88
+ end
89
+
90
+ def process_add_task_command(command)
91
+ @server.logger.debug("add_task: #{command.args[:owner_id]}, #{command.args[:job_id]}, #{command.args[:task_id]}")
92
+ task = BackgroundQueue::ServerLib::Task.new(command.args[:owner_id], command.args[:job_id], command.args[:task_id], command.args[:priority], command.args[:worker], command.args[:params], command.options)
93
+ server.task_queue.add_task(task)
94
+ @server.change_stat(:tasks, 1)
95
+ build_simple_command(:result, "ok")
96
+ end
97
+
98
+ def process_add_tasks_command(command)
99
+ @server.logger.debug("add_tasks: #{command.args[:owner_id]}, #{command.args[:job_id]}, #{command.args[:tasks].inspect}")
100
+ shared_params = command.args[:shared_parameters]
101
+ shared_params = {} if shared_params.nil?
102
+ owner_id = command.args[:owner_id]
103
+ job_id = command.args[:job_id]
104
+ priority = command.args[:priority]
105
+ worker = command.args[:worker]
106
+ for task_data in command.args[:tasks]
107
+ if task_data[1].nil?
108
+ merged_params = shared_params
109
+ else
110
+ merged_params = shared_params.clone.update(task_data[1])
111
+ end
112
+ task = BackgroundQueue::ServerLib::Task.new(owner_id, job_id, task_data[0], priority, worker, merged_params, command.options)
113
+ server.task_queue.add_task(task)
114
+ end
115
+ @server.change_stat(:tasks, command.args[:tasks].length)
116
+ build_simple_command(:result, "ok")
117
+ end
118
+
119
+ def process_get_status_command(command)
120
+ job = @server.jobs.get_job(command.args[:job_id])
121
+ if job.nil?
122
+ build_simple_command(:job_not_found, "job #{command.args[:job_id]} not found")
123
+ else
124
+ BackgroundQueue::Command.new(:status, {}, job.get_current_progress)
125
+ end
126
+ end
127
+
128
+ def process_stats_command(command)
129
+ BackgroundQueue::Command.new(:stats, {}, @server.get_stats)
130
+ end
131
+
132
+ end
133
+ end