mloughran-job_queue 0.0.5 → 0.0.6

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.
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :minor: 0
3
- :patch: 5
3
+ :patch: 6
4
4
  :major: 0
@@ -4,33 +4,87 @@ class JobQueue::BeanstalkAdapter
4
4
  def initialize(options = {})
5
5
  @hosts = options[:hosts] || 'localhost:11300'
6
6
  end
7
-
8
- def put(string, queue, priority)
9
- beanstalk_pool(queue).put(string)
7
+
8
+ def put(string, queue, priority, ttr)
9
+ ttr = ttr.floor #rounding because Beanstalk doesnt accept float numbers
10
+ raise JobQueue::ArgumentError, "TTR must be greater than 1" if ttr < 2
11
+
12
+ delay = 0
13
+ job_info = beanstalk_pool(queue).put_and_report_conn \
14
+ string, priority, delay, ttr
15
+ "#{job_info[:host]}_#{job_info[:id]}"
16
+ rescue Beanstalk::NotConnected
17
+ raise JobQueue::NoConnectionAvailable
10
18
  end
11
-
12
- def subscribe(error_report, queue, &block)
19
+
20
+ def subscribe(error_report, cleanup_task, queue, &block)
13
21
  pool = Beanstalk::Pool.new([@hosts].flatten, queue)
14
22
  loop do
15
23
  begin
16
24
  job = pool.reserve(1)
25
+ time_left = job.stats["time-left"]
17
26
  JobQueue.logger.info "Beanstalk received #{job.body}"
18
- begin
27
+ Timeout::timeout([time_left - 1, 1].max) do
19
28
  yield job.body
20
- rescue => e
21
- error_report.call(job.body, e)
29
+ end
30
+ job.delete
31
+ rescue Timeout::Error
32
+ cleanup_task.call(job.body)
33
+ JobQueue.logger.warn "Job timed out"
34
+ begin
22
35
  job.delete
36
+ rescue Beanstalk::NotFoundError
37
+ JobQueue.logger.error "Job timed out and could not be deleted"
23
38
  end
24
39
  rescue Beanstalk::TimedOut
25
40
  # Do nothing - retry to reseve (from another host?)
41
+ rescue => e
42
+ if job
43
+ error_report.call(job.body, e)
44
+ begin
45
+ job.delete
46
+ rescue Beanstalk::NotFoundError
47
+ JobQueue.logger.error "Job failed but could not be deleted"
48
+ end
49
+ else
50
+ JobQueue.logger.error "Unhandled exception: #{e.message}\n" \
51
+ "#{e.backtrace.join("\n")}\n"
52
+ end
26
53
  end
27
54
  end
28
55
  end
29
-
30
- def beanstalk_pool(queue)
56
+
57
+ def job_stats(job_id)
58
+ host, id = job_id.split('_')
59
+ beanstalk_pool.job_stats(id).select { |k, v| k == host }[0][1]
60
+ rescue Beanstalk::NotFoundError
61
+ nil
62
+ end
63
+
64
+ def beanstalk_pool(queue='default')
31
65
  @beanstalk_pools ||= {}
32
66
  @beanstalk_pools[queue] ||= begin
33
67
  Beanstalk::Pool.new([@hosts].flatten, queue)
34
68
  end
35
69
  end
70
+
71
+ module BeanstalkExtension
72
+ def put_and_report_conn(body, pri=65536, delay=0, ttr=120)
73
+ send_to_rand_conn_and_report(:put, body, pri, delay, ttr)
74
+ end
75
+
76
+ def send_to_rand_conn_and_report(*args)
77
+ connect()
78
+ retry_wrap{
79
+ conn = pick_connection
80
+ {:host => conn.addr, :id => call_wrap(conn, *args)}
81
+ }
82
+ end
83
+
84
+ def job_stats(id)
85
+ make_hash(send_to_all_conns(:job_stats, id))
86
+ end
87
+ end
36
88
  end
89
+
90
+ Beanstalk::Pool.send(:include, JobQueue::BeanstalkAdapter::BeanstalkExtension)
@@ -29,28 +29,39 @@ class JobQueue
29
29
  end
30
30
  end
31
31
  end
32
-
32
+
33
33
  def self.put(string, options = {})
34
34
  queue = options[:queue] || 'default'
35
35
  priority = options[:priority] || 50
36
- adapter.put(string, queue, priority)
36
+ ttr = options[:ttr] || 60
37
+ adapter.put(string, queue, priority, ttr)
37
38
  end
38
-
39
+
39
40
  def self.subscribe(options = {}, &block)
40
41
  queue = options[:queue] || 'default'
41
- error_report = options[:error_report] || begin
42
- Proc.new do |job_body, e|
43
- JobQueue.logger.error \
44
- "Job failed\n" \
45
- "==========\n" \
46
- "Job content: #{job_body.inspect}\n" \
47
- "Exception: #{e.message}\n" \
48
- "#{e.backtrace.join("\n")}\n" \
49
- "\n"
50
- end
42
+ error_report = options[:error_report] || Proc.new do |job_body, e|
43
+ JobQueue.logger.error \
44
+ "Job failed\n" \
45
+ "==========\n" \
46
+ "Job content: #{job_body.inspect}\n" \
47
+ "Exception: #{e.message}\n" \
48
+ "#{e.backtrace.join("\n")}\n" \
49
+ "\n"
51
50
  end
51
+ cleanup_task = options[:cleanup] || lambda {}
52
52
  catch :stop do
53
- adapter.subscribe(error_report, queue, &block)
53
+ adapter.subscribe(error_report, cleanup_task, queue, &block)
54
54
  end
55
55
  end
56
+
57
+ # Returns a hash of info (exact details dependent on adapter)
58
+ def self.job_stats(job_id)
59
+ adapter.job_stats(job_id)
60
+ end
61
+
62
+ class NoConnectionAvailable < RuntimeError
63
+ end
64
+
65
+ class ArgumentError < ::ArgumentError
66
+ end
56
67
  end
@@ -40,6 +40,137 @@ describe JobQueue::BeanstalkAdapter do
40
40
  end
41
41
  end
42
42
 
43
+ describe "put" do
44
+ before :all do
45
+ JobQueue.adapter = JobQueue::BeanstalkAdapter.new
46
+ end
47
+
48
+ it "should return the job id" do
49
+ job_id = JobQueue.put("hello 1")
50
+ job_id.should == "localhost:11300_1"
51
+ end
52
+
53
+ it "should assign job priority" do
54
+ jobs = ["1","2","3"]
55
+ JobQueue.put(jobs[2], :priority => 3)
56
+ JobQueue.put(jobs[1], :priority => 2)
57
+ JobQueue.put(jobs[0], :priority => 1)
58
+
59
+ jobs_received = []
60
+ should_not_timeout(0.5) {
61
+ index = 0
62
+ JobQueue.subscribe do |job_body|
63
+ index += 1
64
+ jobs_received << job_body
65
+ throw :stop if index == 3
66
+ end
67
+ }
68
+
69
+ jobs_received.should == jobs
70
+ end
71
+
72
+ it "should be able to retrieve job stats by id" do
73
+ job_id = JobQueue.put("hello 1")
74
+ job_id.should == "localhost:11300_1"
75
+ JobQueue.put("hello 2")
76
+ stats = JobQueue.job_stats("localhost:11300_1")
77
+
78
+ stats["id"].should == 1
79
+ stats["tube"].should == "default"
80
+ end
81
+
82
+ it "should raise error when no connections exist" do
83
+ system "killall beanstalkd"
84
+ lambda {
85
+ JobQueue.put('test')
86
+ }.should raise_error(JobQueue::NoConnectionAvailable)
87
+ end
88
+
89
+ it "should succeed when one connection fails" do
90
+ JobQueue.adapter = JobQueue::BeanstalkAdapter.new({
91
+ :hosts => ['localhost:10001', 'localhost:666']
92
+ })
93
+ 10.times{ job_id = JobQueue.put("hello 1")}
94
+ end
95
+
96
+ it "should raise an error if a ttr of < 2 is specified" do
97
+ lambda {
98
+ JobQueue.put('test', :ttr => 1.9)
99
+ }.should raise_error(JobQueue::ArgumentError)
100
+
101
+ lambda {
102
+ JobQueue.put('test', :ttr => 2)
103
+ }.should_not raise_error(JobQueue::ArgumentError)
104
+ end
105
+ end
106
+
107
+ describe "subscribe" do
108
+ before :all do
109
+ JobQueue.adapter = JobQueue::BeanstalkAdapter.new
110
+ end
111
+
112
+ it "should delete a job once it has been succesfully excecuted" do
113
+ job_id = JobQueue.put('testdeleted')
114
+ JobQueue.put('foo')
115
+ index = 0
116
+ JobQueue.subscribe do |body|
117
+ index += 1
118
+ throw :stop if index == 2
119
+ end
120
+ JobQueue.job_stats(job_id).should be_nil
121
+ end
122
+
123
+ it "should report and error and delete the job if a job times out" do
124
+ job_id = JobQueue.put("job1", :ttr => 2)
125
+ JobQueue.put('test')
126
+
127
+ JobQueue.logger.should_receive(:warn).with("Job timed out")
128
+
129
+ index = 0
130
+ JobQueue.subscribe do |body|
131
+ index += 1
132
+ throw :stop if index == 2
133
+ sleep 2.2
134
+ end
135
+
136
+ JobQueue.job_stats(job_id).should be_nil
137
+ end
138
+
139
+ it "should allow a client to cleanup if a job times out" do
140
+ JobQueue.put('jobcleanup', :ttr => 2)
141
+ JobQueue.put('test')
142
+
143
+ cleanup = nil
144
+
145
+ index = 0
146
+ JobQueue.subscribe(:cleanup => lambda { |job| FileUtils.rm(job) }) do |body|
147
+ file = File.open(body, 'w')
148
+ file << "hello"
149
+ file.flush
150
+
151
+ index += 1
152
+ throw :stop if index == 2
153
+ sleep 2.2
154
+ end
155
+
156
+ File.exists?('jobcleanup').should be_false
157
+ end
158
+ end
159
+
160
+ describe "job_stats" do
161
+ before :all do
162
+ JobQueue.adapter = JobQueue::BeanstalkAdapter.new
163
+ end
164
+
165
+ it "should gracefully deal with jobs where connection no longer exists" do
166
+ JobQueue.job_stats("localhost:11305_1").should be_nil
167
+ end
168
+
169
+ it "should gracefully deal with jobs where job doesn't exist" do
170
+ JobQueue.job_stats("localhost:11300_1").should be_nil
171
+ end
172
+ end
173
+
43
174
  describe "when connecting to one instance" do
44
175
  before :all do
45
176
  JobQueue.adapter = JobQueue::BeanstalkAdapter.new
@@ -126,7 +257,7 @@ describe JobQueue::BeanstalkAdapter do
126
257
  throw :stop
127
258
  end
128
259
  end
129
-
260
+
130
261
  # TODO: This test is brittle.
131
262
  it "should be possible to retrieve all jobs supplied" do
132
263
  # Put some jobs on the queue
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mloughran-job_queue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martyn Loughran
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-11 00:00:00 -07:00
12
+ date: 2009-06-12 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15