jobster 1.0.1 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b62587aac678545a75383e10bb98737ec8ddc0b2
4
- data.tar.gz: 83f880bacae8180651f0cbeae92fee0f42937560
3
+ metadata.gz: 30f6aef2178e0d7768841444674349210d567b03
4
+ data.tar.gz: f0d28eb0e772a1ec33b28bb9fc108feaf6521b45
5
5
  SHA512:
6
- metadata.gz: cd0fece9c0900cb682360535585e6731441194afa46c9605c98f4befedf2aef3b09efe6090261cee9841d356bfe82e32b76c297e2b10e2be0bbba468b23ef775
7
- data.tar.gz: e1dc4e0823f5b1e1cc35d861ebdbcd00b8d912357c6ea01436f8667bbf79bef8b63192af80cebd1a038c7a406a93838836d670a4ec31159b7d0d508ad5a8d83f
6
+ metadata.gz: 2d58fc83c4df5ed65e71c1e6fe54d0013cee2d567cf26b48d4def603a4f749db523365ee03d2d596fd54e72207d765b0987526f47df7781d0d3736206bed1825
7
+ data.tar.gz: 335bae815fea0868611fa84740a1ef2b870aca0a5174c3893d2e6cd34bd1a6d0ef9166375c17112a1828f1f37201ee3153c0a702c024f610a8e53830174137ac
@@ -2,46 +2,45 @@ require 'bunny'
2
2
  require 'jobster/job'
3
3
  require 'jobster/worker'
4
4
  require 'jobster/version'
5
+ require 'jobster/config'
5
6
 
6
7
  module Jobster
7
8
 
8
- class << self
9
+ def self.config
10
+ @config ||= Config.new
11
+ end
9
12
 
10
- def bunny
11
- @bunny ||= begin
12
- connection = Bunny.new
13
- connection.start
14
- connection
15
- end
16
- end
17
- attr_writer :bunny
13
+ def self.configure(&block)
14
+ block.call(self.config)
15
+ end
18
16
 
19
- def logger
20
- @logger ||= Logger.new(STDOUT)
21
- end
22
- attr_writer :logger
17
+ def self.channel
18
+ @channel ||= config.bunny.create_channel(nil, config.worker_threads)
19
+ end
23
20
 
24
- def ttl
25
- @ttl ||= 60 * 60 * 10
26
- end
27
- attr_writer :ttl
21
+ def self.exchange
22
+ @exchange ||= channel.exchange(config.exchange_name, :type => :direct, :durable => true, :auto_delete => false)
23
+ end
28
24
 
29
- def queue_prefix
30
- @queue_prefix ||= "jobster"
31
- end
32
- attr_writer :queue_prefix
25
+ def self.delay_exchange
26
+ @delay_exchange ||= channel.exchange(config.delay_exchange_name, :type => :fanout, :durable => true, :auto_delete => false)
27
+ end
33
28
 
34
- def channel
35
- @channel ||= bunny.create_channel
29
+ def self.delay_queue
30
+ @delay_queue ||= begin
31
+ queue = channel.queue(config.delay_queue_name, :durable => true, :auto_delete => false, :arguments => {'x-dead-letter-exchange' => config.exchange_name})
32
+ queue.bind(delay_exchange)
33
+ queue
36
34
  end
35
+ end
37
36
 
38
- def queue(name)
39
- @queues ||= {}
40
- @queues[name] ||= begin
41
- channel.queue("#{queue_prefix}-#{name}", :durable => true, :arguments => {'x-message-ttl' => self.ttl})
42
- end
37
+ def self.queue(name)
38
+ @queues ||= {}
39
+ @queues[name] ||= begin
40
+ queue = channel.queue("#{config.queue_name_prefix}-#{name}", :durable => true, :auto_delete => false)
41
+ queue.bind(exchange, :routing_key => name)
42
+ queue
43
43
  end
44
-
45
44
  end
46
45
 
47
46
  end
@@ -0,0 +1,61 @@
1
+ module Jobster
2
+ class Config
3
+
4
+ def bunny
5
+ @bunny ||= begin
6
+ connection = Bunny.new
7
+ connection.start
8
+ connection
9
+ end
10
+ end
11
+ attr_writer :bunny
12
+
13
+ def logger
14
+ @logger ||= Logger.new(STDOUT)
15
+ end
16
+ attr_writer :logger
17
+
18
+ def exchange_name
19
+ @exchange_name || "jobster"
20
+ end
21
+ attr_writer :exchange_name
22
+
23
+ def delay_exchange_name
24
+ @delay_exchange_name || "#{self.exchange_name}-delay-exch"
25
+ end
26
+ attr_writer :delay_exchange_name
27
+
28
+ def delay_queue_name
29
+ @delay_queue_name || "#{self.exchange_name}-delay-queue"
30
+ end
31
+ attr_writer :delay_queue_name
32
+
33
+ def queue_name_prefix
34
+ @queue_name_prefix || "#{self.exchange_name}-queue"
35
+ end
36
+ attr_writer :queue_name_prefix
37
+
38
+ def worker_threads
39
+ @worker_threads || 1
40
+ end
41
+ attr_writer :worker_threads
42
+
43
+ def worker_callbacks
44
+ @worker_callbacks ||= {}
45
+ end
46
+
47
+ def worker_callback(name, &block)
48
+ worker_callbacks[name.to_sym] ||= []
49
+ worker_callbacks[name.to_sym] << block
50
+ end
51
+
52
+ def worker_error_handlers
53
+ @worker_error_handlers ||= []
54
+ end
55
+
56
+ def worker_error_handler(&block)
57
+ worker_error_handlers << block
58
+ end
59
+
60
+ end
61
+ end
@@ -16,19 +16,34 @@ module Jobster
16
16
  end
17
17
 
18
18
  def log(text)
19
- Jobster.logger.info "[#{@id}] #{text}"
19
+ Jobster.config.logger.info "[#{@id}] #{text}"
20
20
  end
21
21
 
22
- def self.queue(queue = {}, params = {})
22
+ def self.queue(queue = {}, params = {}, &block)
23
+ queue, params = parse_args_for_queue(queue, params)
24
+ self.queue_job(Jobster.exchange, queue, self.name, params, &block)
25
+ end
26
+
27
+ def self.queue_with_delay(delay, queue = {}, params = {}, &block)
28
+ queue, params = parse_args_for_queue(queue, params)
29
+ self.queue_job(Jobster.delay_exchange, queue, self.name, params, :ttl => delay, &block)
30
+ end
23
31
 
24
- if queue.is_a?(Hash)
25
- params = queue
26
- queue = :main
27
- end
32
+ def self.parse_args_for_queue(queue, params)
33
+ queue.is_a?(Hash) ? [:main, queue] : [queue, params]
34
+ end
28
35
 
36
+ def self.queue_job(exchange, queue_name, class_name, params, options = {}, &block)
29
37
  job_id = SecureRandom.uuid[0,8]
30
- job_payload = {'params' => params, 'class_name' => self.name, 'id' => job_id, 'queue' => queue}
31
- Jobster.queue(queue).publish(job_payload.to_json, :persistent => false)
38
+ job_payload = {'params' => params, 'class_name' => class_name, 'id' => job_id, 'queue' => queue_name}
39
+ publish_opts = {}
40
+ publish_opts[:persistent] = true
41
+ publish_opts[:routing_key] = queue_name
42
+ publish_opts[:expiration] = options[:ttl] * 1000 if options[:ttl]
43
+ block.call(job_id, publish_opts) if block_given?
44
+ a = exchange.publish(job_payload.to_json, publish_opts)
45
+ when_string = (options[:ttl] ? "in #{options[:ttl]}s" : "immediately")
46
+ Jobster.config.logger.info "[#{job_id}] \e[34m#{class_name}\e[0m queued to run #{when_string} on #{queue_name} queue"
32
47
  job_id
33
48
  end
34
49
 
@@ -1,3 +1,3 @@
1
1
  module Jobster
2
- VERSION = '1.0.1'
2
+ VERSION = '1.2.0'
3
3
  end
@@ -4,30 +4,43 @@ module Jobster
4
4
  def initialize(queues = nil)
5
5
  @initial_queues = queues || self.class.queues || [:main]
6
6
  @active_queues = {}
7
+ @running_jobs = []
7
8
  @process_name = $0
9
+ set_process_name
10
+ end
11
+
12
+ def set_process_name
13
+ prefix = @process_name.to_s
14
+ prefix += " [exiting]" if @exit
15
+ if @running_jobs.empty?
16
+ $0 = "#{prefix} (idle)"
17
+ else
18
+ $0 = "#{prefix} (running #{@running_jobs.join(', ')})"
19
+ end
8
20
  end
9
21
 
10
22
  def work
11
- logger.info "Jobster worker started"
12
- self.class.run_callbacks(:after_start)
23
+ logger.info "Jobster worker started (#{Jobster.config.worker_threads} thread(s))"
24
+ run_callbacks :after_start
13
25
 
14
- @running_job = false
15
- Signal.trap("INT") { @exit = true }
16
- Signal.trap("TERM") { @exit = true }
26
+ Jobster.delay_queue # Declare it
17
27
 
18
- Jobster.channel.prefetch(1)
28
+ Signal.trap("INT") { @exit = true; set_process_name }
29
+ Signal.trap("TERM") { @exit = true; set_process_name }
30
+
31
+ Jobster.channel.prefetch(Jobster.config.worker_threads)
19
32
  @initial_queues.uniq.each { |queue | join_queue(queue) }
20
33
 
21
34
  exit_checks = 0
22
35
  loop do
23
- if @exit && @running_job == false
24
- logger.info "Exiting immediately because no job running"
25
- self.class.run_callbacks(:before_quit, :immediate)
36
+ if @exit && @running_jobs.empty?
37
+ logger.info "Exiting immediately because no jobs running"
38
+ run_callbacks :before_quit, :immediate
26
39
  exit 0
27
40
  elsif @exit
28
41
  if exit_checks >= 300
29
42
  logger.info "Job did not finish in a timely manner. Exiting"
30
- self.class.run_callbacks(:before_quit, :timeout)
43
+ run_callbacks :before_quit, :timeout
31
44
  exit 0
32
45
  end
33
46
  if exit_checks == 0
@@ -41,43 +54,49 @@ module Jobster
41
54
  end
42
55
  end
43
56
 
57
+ def perform_job(class_name, params = {}, id = nil)
58
+ id ||= SecureRandom.uuid[0,8]
59
+ start_time = Time.now
60
+ exception = nil
61
+ logger.info "[#{id}] Started processing \e[34m#{class_name}\e[0m job"
62
+ begin
63
+ klass = Object.const_get(class_name).new(id, params)
64
+ run_callbacks :before_job, klass
65
+ klass.perform
66
+ rescue Job::Abort => e
67
+ exception = e
68
+ logger.info "[#{id}] Job aborted (#{e.message})"
69
+ rescue => e
70
+ exception = e
71
+ logger.warn "[#{id}] \e[31m#{e.class}: #{e.message}\e[0m"
72
+ e.backtrace.each do |line|
73
+ logger.warn "[#{id}] " + line
74
+ end
75
+ Jobster.config.worker_error_handlers.each { |handler| handler.call(e, klass) }
76
+ ensure
77
+ run_callbacks :after_job, klass, exception
78
+ logger.info "[#{id}] Finished processing \e[34m#{class_name}\e[0m job in #{Time.now - start_time}s"
79
+ end
80
+ end
81
+
44
82
  private
45
83
 
46
- def receive_job(delivery_info, properties, body)
47
- @running_job = true
84
+ def receive_job(properties, body)
48
85
  begin
49
86
  message = JSON.parse(body) rescue nil
50
87
  if message && message['class_name']
51
- start_time = Time.now
52
- $0 = "#{@process_name} (running #{message['class_name']})"
53
88
  Thread.current[:job_id] = message['id']
54
- logger.info "[#{message['id']}] Started processing \e[34m#{message['class_name']}\e[0m job"
55
- begin
56
- klass = Object.const_get(message['class_name']).new(message['id'], message['params'])
57
- self.class.run_callbacks(:before_job, klass)
58
- klass.perform
59
- self.class.run_callbacks(:after_job, klass)
60
- rescue Job::Abort => e
61
- logger.info "[#{message['id']}] Job aborted (#{e.message})"
62
- rescue => e
63
- logger.warn "[#{message['id']}] \e[31m#{e.class}: #{e.message}\e[0m"
64
- e.backtrace.each do |line|
65
- logger.warn "[#{message['id']}] " + line
66
- end
67
- self.class.run_callbacks(:after_job, klass, e)
68
- self.class.error_handlers.each { |handler| handler.call(e, klass) }
69
- ensure
70
- logger.info "[#{message['id']}] Finished processing \e[34m#{message['class_name']}\e[0m job in #{Time.now - start_time}s"
71
- end
89
+ @running_jobs << message['id']
90
+ set_process_name
91
+ perform_job(message['class_name'], message['params'] || {}, message['id'])
72
92
  end
73
93
  ensure
74
94
  Thread.current[:job_id] = nil
75
- $0 = @process_name
76
- Jobster.channel.ack(delivery_info.delivery_tag)
77
- @running_job = false
78
- if @exit
79
- logger.info "Exiting because a job has ended."
80
- self.class.run_callbacks(:before_quit, :job_completed)
95
+ @running_jobs.delete(message['id']) if message['id']
96
+ set_process_name
97
+ if @exit && @running_jobs.empty?
98
+ logger.info "Exiting because all jobs have finished."
99
+ run_callbacks :before_quit, :job_completed
81
100
  exit 0
82
101
  end
83
102
  end
@@ -87,12 +106,16 @@ module Jobster
87
106
  if @active_queues[queue]
88
107
  logger.info "Attempted to join queue #{queue} but already joined."
89
108
  else
90
- self.class.run_callbacks(:before_queue_join, queue)
109
+ run_callbacks :before_queue_join, queue
91
110
  consumer = Jobster.queue(queue).subscribe(:manual_ack => true) do |delivery_info, properties, body|
92
- receive_job(delivery_info, properties, body)
111
+ begin
112
+ receive_job(properties, body)
113
+ ensure
114
+ Jobster.channel.ack(delivery_info.delivery_tag)
115
+ end
93
116
  end
94
117
  @active_queues[queue] = consumer
95
- self.class.run_callbacks(:after_queue_join, queue, consumer)
118
+ run_callbacks :after_queue_join, queue, consumer
96
119
  logger.info "Joined \e[32m#{queue}\e[0m queue"
97
120
  end
98
121
  end
@@ -108,33 +131,16 @@ module Jobster
108
131
  end
109
132
 
110
133
  def logger
111
- Jobster.logger
134
+ Jobster.config.logger
112
135
  end
113
136
 
114
137
  def self.queues
115
138
  @queues ||= [:main]
116
139
  end
117
140
 
118
- def self.error_handlers
119
- @error_handlers ||= []
120
- end
121
-
122
- def self.register_error_handler(&block)
123
- error_handlers << block
124
- end
125
-
126
- def self.callbacks
127
- @callbacks ||= {}
128
- end
129
-
130
- def self.add_callback(event, &block)
131
- callbacks[event] ||= []
132
- callbacks[event] << block
133
- end
134
-
135
- def self.run_callbacks(event, *args)
136
- if callbacks[event]
137
- callbacks[event].each do |callback|
141
+ def run_callbacks(event, *args)
142
+ if callbacks = Jobster.config.worker_callbacks[event]
143
+ callbacks.each do |callback|
138
144
  callback.call(*args)
139
145
  end
140
146
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jobster
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Cooke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-27 00:00:00.000000000 Z
11
+ date: 2017-12-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -40,6 +40,7 @@ extra_rdoc_files: []
40
40
  files:
41
41
  - bin/jobster
42
42
  - lib/jobster.rb
43
+ - lib/jobster/config.rb
43
44
  - lib/jobster/job.rb
44
45
  - lib/jobster/version.rb
45
46
  - lib/jobster/worker.rb
@@ -63,9 +64,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
63
64
  version: '0'
64
65
  requirements: []
65
66
  rubyforge_project:
66
- rubygems_version: 2.5.1
67
+ rubygems_version: 2.5.2
67
68
  signing_key:
68
69
  specification_version: 4
69
70
  summary: A RabbitMQ-based job queueing system
70
71
  test_files: []
71
- has_rdoc: