stalk_climber 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -15,9 +15,30 @@ module StalkClimber
15
15
  end
16
16
 
17
17
 
18
+ # Climb over all jobs on all connections in the connection pool.
19
+ # An instance of Job is yielded. For more information see Connection#climb
20
+ def climb
21
+ enum = to_enum
22
+ return enum unless block_given?
23
+ loop do
24
+ begin
25
+ yield enum.next
26
+ rescue StopIteration
27
+ return ($ERROR_INFO.nil? || $ERROR_INFO.result.nil?) ? nil : $ERROR_INFO.result
28
+ end
29
+ end
30
+ end
31
+ alias_method :each, :climb
32
+
33
+
18
34
  # Perform a threaded climb across all connections in the connection pool.
35
+ # This method cannot be used for enumerable enumeration because a break
36
+ # called from one of the threads will cause a LocalJumpError. This could be
37
+ # fixed, but expected behavior on break varies as to whether or not to wait
38
+ # for all threads before returning a result. However, still useful for
39
+ # operations that always visit all jobs.
19
40
  # An instance of Job is yielded to +block+
20
- def climb(&block)
41
+ def climb_threaded(&block)
21
42
  threads = []
22
43
  self.connection_pool.connections.each do |connection|
23
44
  threads << Thread.new { connection.each(&block) }
@@ -25,7 +46,7 @@ module StalkClimber
25
46
  threads.each(&:join)
26
47
  return
27
48
  end
28
- alias_method :each, :climb
49
+ alias_method :each_threaded, :climb_threaded
29
50
 
30
51
 
31
52
  # Creates a new Climber instance, optionally yielding the instance
@@ -36,5 +57,29 @@ module StalkClimber
36
57
  yield(self) if block_given?
37
58
  end
38
59
 
60
+
61
+ # Returns a hash with connections as keys and max_job_ids as values
62
+ def max_job_ids
63
+ connection_pairs = connection_pool.connections.map do |connection|
64
+ [connection, connection.max_job_id]
65
+ end
66
+ return Hash[connection_pairs]
67
+ end
68
+
69
+
70
+ # Returns an Enumerator for enumerating jobs on all connections.
71
+ # Connections are enumerated in the order defined. See Connection#to_enum
72
+ # for more information
73
+ # A job is yielded with each iteration.
74
+ def to_enum
75
+ return Enumerator.new do |yielder|
76
+ self.connection_pool.connections.each do |connection|
77
+ connection.each do |job|
78
+ yielder << job
79
+ end
80
+ end
81
+ end
82
+ end
83
+
39
84
  end
40
85
  end
@@ -1,6 +1,6 @@
1
1
  module StalkClimber
2
2
  class Connection < Beaneater::Connection
3
- include LazyEnumerable
3
+ include RUBY_VERSION >= '2.0.0' ? LazyEnumerable : Enumerable
4
4
 
5
5
  DEFAULT_TUBE = 'stalk_climber'
6
6
  PROBE_TRANSMISSION = "put 4294967295 0 300 2\r\n{}"
@@ -22,15 +22,57 @@ module StalkClimber
22
22
  end
23
23
 
24
24
 
25
- # Handles job enumeration in descending ID order, passing an instance of Job to +block+ for
26
- # each existing job on the beanstalk server. Jobs are enumerated in three phases. Jobs between
27
- # max_job_id and the max_climbed_job_id are pulled from beanstalk, cached, and given to
28
- # +block+. Jobs that have already been cached are yielded to +block+ if they still exist,
29
- # otherwise they are deleted from the cache. Finally, jobs between min_climbed_job_id and 1
30
- # are pulled from beanstalk, cached, and given to +block+.
25
+ # Interface for job enumerator/enumeration in descending ID order. Returns an instance of
26
+ # Job for each existing job on the beanstalk server. Jobs are enumerated in three phases. Jobs
27
+ # between max_job_id and the max_climbed_job_id are pulled from beanstalk, cached, and yielded.
28
+ # Jobs that have already been cached are yielded if they still exist, otherwise they are deleted
29
+ # from the cache. Finally, jobs between min_climbed_job_id and 1 are pulled from beanstalk, cached,
30
+ # and yielded.
31
31
  # Connection#each fulfills Enumberable contract, allowing connection to behave as an Enumerable.
32
- def each(&block)
33
- climb(&block)
32
+ def climb
33
+ enum = to_enum
34
+ return enum unless block_given?
35
+ loop do
36
+ begin
37
+ yield enum.next
38
+ rescue StopIteration
39
+ return ($ERROR_INFO.nil? || $ERROR_INFO.result.nil?) ? nil : $ERROR_INFO.result
40
+ end
41
+ end
42
+ end
43
+ alias_method :each, :climb
44
+
45
+
46
+ # Safe form of fetch_job!, returns a Job instance for the specified +job_id+.
47
+ # If the job does not exist, the error is caught and nil is passed returned instead.
48
+ def fetch_job(job_id)
49
+ begin
50
+ job = fetch_job!(job_id)
51
+ rescue Beaneater::NotFoundError
52
+ job = nil
53
+ end
54
+ return job
55
+ end
56
+
57
+
58
+ # Returns a Job instance for the specified +job_id+. If the job does not exist,
59
+ # a Beaneater::NotFoundError will bubble up from Beaneater. The job is not cached.
60
+ def fetch_job!(job_id)
61
+ return Job.new(transmit("peek #{job_id}"))
62
+ end
63
+
64
+
65
+ # Like fetch_job, but fetches all job ids in +job_ids+. Jobs are not cached and
66
+ # nil is returned if any of the jobs don't exist.
67
+ def fetch_jobs(*job_ids)
68
+ return job_ids.flatten.map { |job_id| fetch_job(job_id) }
69
+ end
70
+
71
+
72
+ # Similar to fetch_job!, but fetches all job ids in +job_ids+. Jobs are not cached
73
+ # and a Beaneater::NotFoundError is raised if any of the listed jobs don't exist.
74
+ def fetch_jobs!(*job_ids)
75
+ return job_ids.flatten.map { |job_id| fetch_job!(job_id) }
34
76
  end
35
77
 
36
78
 
@@ -68,63 +110,47 @@ module StalkClimber
68
110
  end
69
111
 
70
112
 
71
- # Safe form of with_job!, yields a Job instance to +block+ for the specified +job_id+.
72
- # If the job does not exist, the error is caught and nil is passed to +block+ instead.
73
- def with_job(job_id, &block)
74
- begin
75
- with_job!(job_id, &block)
76
- rescue Beaneater::NotFoundError
77
- block.call(nil)
78
- end
79
- end
113
+ # Returns an Enumerator for crawling all existing jobs for a connection.
114
+ # See Connection#each for more information.
115
+ def to_enum
116
+ return Enumerator.new do |yielder|
117
+ max_id = max_job_id
80
118
 
119
+ initial_cached_jobs = cache.values_at(*cache.keys.sort.reverse)
81
120
 
82
- # Yields a Job instance to +block+ for the specified +job_id+.
83
- # If the job does not exist, a Beaneater::NotFoundError will bubble up from Beaneater.
84
- def with_job!(job_id, &block)
85
- job = Job.new(transmit("peek #{job_id}"))
86
- block.call(job)
87
- end
88
-
121
+ max_id.downto(self.max_climbed_job_id + 1) do |job_id|
122
+ job = fetch_and_cache_job(job_id)
123
+ yielder << job unless job.nil?
124
+ end
89
125
 
90
- protected
126
+ initial_cached_jobs.each do |job|
127
+ if job.exists?
128
+ yielder << job
129
+ else
130
+ self.cache.delete(job.id)
131
+ end
132
+ end
91
133
 
92
- # Helper method, similar to with_job, that retrieves the job identified by
93
- # +job_id+, caches it, and updates counters before yielding the job.
94
- # If the job does not exist, +block+ is not called and nothing is cached,
95
- # however counters will be updated.
96
- def cache_job_and_yield(job_id, &block)
97
- with_job(job_id) do |job|
98
- self.cache[job_id] = job unless job.nil?
99
- @min_climbed_job_id = job_id if job_id < @min_climbed_job_id
100
- @max_climbed_job_id = job_id if job_id > @max_climbed_job_id
101
- yield(job) unless job.nil?
134
+ ([self.min_climbed_job_id - 1, max_id].min).downto(1) do |job_id|
135
+ job = fetch_and_cache_job(job_id)
136
+ yielder << job unless job.nil?
137
+ end
102
138
  end
103
139
  end
104
140
 
105
141
 
106
- # Handles job enumeration. See Connection#each for more information.
107
- def climb(&block)
108
- max_id = max_job_id
109
-
110
- initial_cached_jobs = cache.values_at(*cache.keys.sort.reverse)
111
-
112
- max_id.downto(self.max_climbed_job_id + 1) do |job_id|
113
- cache_job_and_yield(job_id, &block)
114
- end
115
-
116
- initial_cached_jobs.each do |job|
117
- if job.exists?
118
- yield job
119
- else
120
- self.cache.delete(job.id)
121
- end
122
- end
142
+ protected
123
143
 
124
- ([self.min_climbed_job_id - 1, max_id].min).downto(1) do |job_id|
125
- cache_job_and_yield(job_id, &block)
126
- end
127
- return
144
+ # Helper method, similar to fetch_job, that retrieves the job identified by
145
+ # +job_id+, caches it, and updates counters before returning the job.
146
+ # If the job does not exist, nothing is cached, however counters will be updated,
147
+ # and nil is returned
148
+ def fetch_and_cache_job(job_id)
149
+ job = fetch_job(job_id)
150
+ self.cache[job_id] = job unless job.nil?
151
+ @min_climbed_job_id = job_id if job_id < @min_climbed_job_id
152
+ @max_climbed_job_id = job_id if job_id > @max_climbed_job_id
153
+ return job
128
154
  end
129
155
 
130
156
 
@@ -15,7 +15,7 @@ module StalkClimber
15
15
 
16
16
  # Returns or fetches the body of the job obtained via the peek command
17
17
  def body
18
- return @body ||= JSON.parse(connection.transmit("peek #{id}")[:body])
18
+ return @body ||= connection.transmit("peek #{id}")[:body]
19
19
  end
20
20
 
21
21
 
@@ -28,6 +28,7 @@ module StalkClimber
28
28
  # Deletes the job from beanstalk. If the job is not found it is assumed that it
29
29
  # has already been otherwise deleted.
30
30
  def delete
31
+ return true if @status == 'DELETED'
31
32
  begin
32
33
  @connection.transmit("delete #{id}")
33
34
  rescue Beaneater::NotFoundError
@@ -45,6 +46,7 @@ module StalkClimber
45
46
  # the job, the peek command could return a much larger response. Rather than waste
46
47
  # the trip to the server, stats are updated each time the method is called.
47
48
  def exists?
49
+ return false if @status == 'DELETED'
48
50
  begin
49
51
  stats(:force_refresh)
50
52
  return true
@@ -76,7 +78,7 @@ module StalkClimber
76
78
  @body = @stats = nil
77
79
  when 'FOUND' # peek
78
80
  @id = job_data[:id].to_i
79
- @body = JSON.parse(job_data[:body])
81
+ @body = job_data[:body]
80
82
  @stats = nil
81
83
  when 'OK' # stats-job
82
84
  @body = nil
@@ -1,3 +1,3 @@
1
1
  module StalkClimber
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.3'
3
3
  end
@@ -2,7 +2,7 @@ require 'test_helper'
2
2
 
3
3
  class ClimberTest < Test::Unit::TestCase
4
4
 
5
- def test_climb_caches_jobs_for_later_use
5
+ def test_each_caches_jobs_for_later_use
6
6
  climber = StalkClimber::Climber.new(BEANSTALK_ADDRESSES)
7
7
 
8
8
  test_jobs = {}
@@ -30,6 +30,35 @@ class ClimberTest < Test::Unit::TestCase
30
30
  end
31
31
 
32
32
 
33
+ def test_each_threaded_works_for_non_break_situation
34
+ climber = StalkClimber::Climber.new(BEANSTALK_ADDRESSES)
35
+ test_jobs = {}
36
+ climber.connection_pool.connections.each do |connection|
37
+ test_jobs[connection.address] = []
38
+ 5.times.to_a.map! do
39
+ test_jobs[connection.address] << StalkClimber::Job.new(connection.transmit(StalkClimber::Connection::PROBE_TRANSMISSION))
40
+ end
41
+ end
42
+
43
+ assert_raise LocalJumpError do
44
+ climber.each_threaded do |job|
45
+ break
46
+ end
47
+ end
48
+
49
+ assert_nothing_raised do
50
+ # test normal enumeration
51
+ climber.each_threaded do |job|
52
+ job
53
+ end
54
+ end
55
+
56
+ climber.connection_pool.connections.each do |connection|
57
+ test_jobs[connection.address].map(&:delete)
58
+ end
59
+ end
60
+
61
+
33
62
  def test_connection_pool_creates_a_connection_pool
34
63
  climber = StalkClimber::Climber.new('beanstalk://localhost')
35
64
  assert_kind_of StalkClimber::ConnectionPool, climber.connection_pool
@@ -44,6 +73,34 @@ class ClimberTest < Test::Unit::TestCase
44
73
  end
45
74
 
46
75
 
76
+ def test_enumerable_works_correctly
77
+ climber = StalkClimber::Climber.new(BEANSTALK_ADDRESSES)
78
+ test_jobs = {}
79
+ climber.connection_pool.connections.each do |connection|
80
+ test_jobs[connection.address] = []
81
+ 5.times.to_a.map! do
82
+ test_jobs[connection.address] << StalkClimber::Job.new(connection.transmit(StalkClimber::Connection::PROBE_TRANSMISSION))
83
+ end
84
+ end
85
+
86
+ assert_nothing_raised do
87
+ # verify enumeration can be short circuited
88
+ climber.any? do |job|
89
+ true
90
+ end
91
+
92
+ # test normal enumeration
93
+ climber.all? do |job|
94
+ job
95
+ end
96
+ end
97
+
98
+ climber.connection_pool.connections.each do |connection|
99
+ test_jobs[connection.address].map(&:delete)
100
+ end
101
+ end
102
+
103
+
47
104
  def test_each_is_an_alias_for_climb
48
105
  assert_equal(
49
106
  StalkClimber::Climber.instance_method(:climb),
@@ -53,6 +110,29 @@ class ClimberTest < Test::Unit::TestCase
53
110
  end
54
111
 
55
112
 
113
+ def test_each_threaded_is_an_alias_for_climb_threaded
114
+ assert_equal(
115
+ StalkClimber::Climber.instance_method(:climb_threaded),
116
+ StalkClimber::Climber.instance_method(:each_threaded),
117
+ 'Expected StalkClimber::Climber#each_threaded to be an alias for StalkClimber::Climber#climb_threaded'
118
+ )
119
+ end
120
+
121
+
122
+ def test_max_job_ids_returns_the_correct_max_job_ids
123
+ climber = StalkClimber::Climber.new(BEANSTALK_ADDRESSES, 'test_tube')
124
+ max_ids = climber.max_job_ids
125
+ max_ids.each do |connection, max_id|
126
+ assert_equal connection.max_job_id - 1, max_id
127
+ end
128
+ end
129
+
130
+
131
+ def test_to_enum_returns_an_enumerator
132
+ assert_kind_of Enumerator, StalkClimber::Climber.new(BEANSTALK_ADDRESSES).to_enum
133
+ end
134
+
135
+
56
136
  def test_with_a_test_tube
57
137
  climber = StalkClimber::Climber.new(BEANSTALK_ADDRESSES, 'test_tube')
58
138
  assert_equal 'test_tube', climber.test_tube
@@ -34,6 +34,19 @@ class ConnectionTest < Test::Unit::TestCase
34
34
  end
35
35
 
36
36
 
37
+ def test_deleted_jobs_should_not_be_enumerated
38
+ seeds = seed_jobs
39
+ seeds.map(&:delete)
40
+ seed_ids = seeds.map(&:id)
41
+
42
+ deleted = @connection.detect do |job|
43
+ seed_ids.include?(job.id)
44
+ end
45
+
46
+ assert_nil deleted, "Deleted job found in enumeration: #{deleted}"
47
+ end
48
+
49
+
37
50
  def test_each_caches_jobs_for_later_use
38
51
  seeds = seed_jobs
39
52
 
@@ -113,12 +126,62 @@ class ConnectionTest < Test::Unit::TestCase
113
126
  end
114
127
 
115
128
 
116
- def test_each_calls_climb
117
- @connection.expects(:climb)
129
+ def test_each_calls_to_enum
130
+ @connection.expects(:to_enum).returns(Enumerator.new { |yielder| yielder << 1 })
118
131
  @connection.each {}
119
132
  end
120
133
 
121
134
 
135
+ def test_fetch_job_returns_nil_or_the_requested_job_if_it_exists
136
+ assert_nothing_raised do
137
+ job = @connection.fetch_job(@connection.max_job_id)
138
+ assert_equal nil, job
139
+ end
140
+
141
+ probe = seed_jobs(1).first
142
+ assert_equal probe.id, @connection.fetch_job(probe.id).id
143
+ probe.delete
144
+ end
145
+
146
+
147
+ def test_fetch_job_bang_returns_requested_job_or_raises_an_error_if_job_does_not_exist
148
+ assert_raise Beaneater::NotFoundError do
149
+ @connection.fetch_job!(@connection.max_job_id)
150
+ end
151
+
152
+ probe = seed_jobs(1).first
153
+ assert_equal probe.id, @connection.fetch_job!(probe.id).id
154
+ probe.delete
155
+ end
156
+
157
+
158
+ def test_fetch_jobs_returns_each_requested_job_or_nil
159
+ assert_nothing_raised do
160
+ jobs = @connection.fetch_jobs(@connection.max_job_id, @connection.max_job_id)
161
+ assert_equal [nil, nil], jobs
162
+ end
163
+
164
+ probes = seed_jobs(2)
165
+ probe_ids = probes.map(&:id)
166
+ assert_equal probe_ids, @connection.fetch_jobs(probe_ids).map(&:id)
167
+ probes.map(&:delete)
168
+ end
169
+
170
+
171
+ def test_fetch_jobs_bang_returns_requested_jobs_or_raises_an_error_if_any_job_does_not_exist
172
+ probes = seed_jobs(2)
173
+
174
+ probe_ids = probes.map(&:id)
175
+ assert_equal probe_ids, @connection.fetch_jobs!(probe_ids).map(&:id)
176
+
177
+ assert_raise Beaneater::NotFoundError do
178
+ @connection.fetch_jobs!(probe_ids.first, @connection.max_job_id, probe_ids.last)
179
+ end
180
+
181
+ probes.map(&:delete)
182
+ end
183
+
184
+
122
185
  def test_max_job_id_returns_expected_max_job_id
123
186
  initial_max = @connection.max_job_id
124
187
  seed_jobs(3).map(&:delete)
@@ -181,28 +244,8 @@ class ConnectionTest < Test::Unit::TestCase
181
244
  end
182
245
 
183
246
 
184
- def test_with_job_yields_nil_or_the_requested_job_if_it_exists
185
- assert_nothing_raised do
186
- @connection.with_job(@connection.max_job_id) do |job|
187
- assert_equal nil, job
188
- end
189
- end
190
-
191
- probe = seed_jobs(1).first
192
- @connection.with_job(probe.id) do |job|
193
- assert_equal probe.id, job.id
194
- end
195
- probe.delete
196
- end
197
-
198
-
199
- def test_with_job_bang_does_not_execute_block_and_raises_error_if_job_does_not_exist
200
- block = lambda {}
201
- block.expects(:call).never
202
- assert_raise Beaneater::NotFoundError do
203
- Object.any_instance.expects(:yield).never
204
- @connection.with_job!(@connection.max_job_id, &block)
205
- end
247
+ def test_to_enum_returns_an_enumerator
248
+ assert_kind_of Enumerator, @connection.to_enum
206
249
  end
207
250
 
208
251
  end
@@ -9,11 +9,11 @@ class Job < Test::Unit::TestCase
9
9
  end
10
10
 
11
11
 
12
- def test_body_retrives_performs_peek_and_parses_json
13
- body = {'test' => true}
12
+ def test_body_performs_peek_and_parses_json
13
+ body = {'test' => true}.to_json
14
14
 
15
15
  @job.connection.expects(:transmit).returns({
16
- :body => body.to_json,
16
+ :body => body,
17
17
  })
18
18
  assert_equal body, @job.body
19
19
 
@@ -53,7 +53,7 @@ class Job < Test::Unit::TestCase
53
53
  assert_equal @connection, job.connection
54
54
  assert_equal @job.id, job.id
55
55
  assert_equal 'FOUND', job.instance_variable_get(:@status)
56
- assert_equal({}, job.body)
56
+ assert_equal('{}', job.body)
57
57
  refute job.instance_variable_get(:@stats)
58
58
  end
59
59
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stalk_climber
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-09-04 00:00:00.000000000 Z
12
+ date: 2013-11-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -117,3 +117,4 @@ test_files:
117
117
  - test/unit/connection_pool_test.rb
118
118
  - test/unit/connection_test.rb
119
119
  - test/unit/job_test.rb
120
+ has_rdoc: