mloughran-job_queue 0.0.5 → 0.0.6

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