background_queue 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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