stalk_climber 0.0.2 → 0.0.3

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.
@@ -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: