bean_counter 0.0.4 → 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e093642cbffceb8f4714bc9f8fece7a8dd7c44c4
4
+ data.tar.gz: 49f7dc68b35365ebca7d40544936a8b6a8a3a23b
5
+ SHA512:
6
+ metadata.gz: 28d291b01222cbe8b50158f4ec55a1cdb836bbda6abd3c7a3b65b8222e2fd005af07232982b363fe9a7c405a9a23472f2e62a965a76029575ff4f731582311f8
7
+ data.tar.gz: 60bc42d02c66e50642faf210b897ec977d0bff9d279655b6286fa6ae24c4bd5f873c3cb3840badb746d5c6c588f1fc6d8ab92c1c2d4daff3948ed0814127e18f
@@ -3,6 +3,7 @@ language: ruby
3
3
  rvm:
4
4
  - 1.9.3
5
5
  - 2.0.0
6
+ - 2.1.0
6
7
  - rbx-19mode
7
8
 
8
9
  before_install:
data/README.md CHANGED
@@ -172,16 +172,42 @@ BeanCounter.beanstalkd_url
172
172
  ###BeanCounter.strategy
173
173
  `BeanCounter.strategy` allows you to choose and configure the strategy
174
174
  BeanCounter uses for accessing and interacting with the beanstalkd pool. At
175
- present, there is only a single strategy available, but at least one other is
176
- in the works.
175
+ present, there are two strategies available: StalkClimberStrategy and
176
+ GemeraldBeanstalkStrategy.
177
177
 
178
- The strategy currently available, BeanCounter::Strategy::StalkClimber, utilizes
178
+ The default BeanCounter::Strategy::StalkClimber strategy utilizes
179
179
  [StalkClimber](https://github.com/gemeraldbeanstalk/stalk_climber.git) to
180
180
  navigate the beanstalkd pool. The job traversal method employed by StalkClimber
181
181
  suffers from some inefficiencies that come with trying to sequential access a
182
182
  queue, but overall it is a powerful strategy that supports multiple servers and
183
183
  offers solid performance given the design of beanstalkd.
184
184
 
185
+ The BeanCounter::Strategy::GemeraldBeanstalkStrategy strategy utilizes
186
+ [GemeraldBeanstalk](https://github.com/gemeraldbeanstalk/gemerald_beanstalk.git)
187
+ servers as the backend for Beaneater. This means that no beanstalkd server is
188
+ required for testing. Beaneater still connects and communicates with
189
+ GemeraldBeanstalk via a TCPSocket, however BeanCounter is able to talk directly
190
+ to the GemeraldBeanstalk servers allowing for faster and more consistent testing.
191
+ When using the GemeraldBeanstalkStrategy, BeanCounter will automatically start
192
+ a GemeraldBeanstalk server at each of the addresses specified by beanstalkd_url.
193
+
194
+ ```ruby
195
+ # No configuration is required to use the StalkClimberStrategy
196
+
197
+ # To use the GemeraldBeanstalkStrategy
198
+ BeanCounter.strategy = :'BeanCounter::Strategy::GemeraldBeanstalkStrategy'
199
+ #=> :"BeanCounter::Strategy::GemeraldBeanstalkStrategy"
200
+
201
+ # Because the GemeraldBeanstalkStrategy starts a GemeraldBeanstalk server
202
+ # immediately, you may want to configure a different address/port for
203
+ # your GemeraldBeanstalk servers to avoid conflicts when you have a Beanstalkd
204
+ # server running:
205
+ BeanCounter.beanstalkd_url = ['127.0.0.1:11400']
206
+ BeanCounter.strategy = :'BeanCounter::Strategy::GemeraldBeanstalkStrategy'
207
+ #=> :"BeanCounter::Strategy::GemeraldBeanstalkStrategy"
208
+ ```
209
+
210
+
185
211
  ## Usage
186
212
  Whether you are using the TestUnit/MiniTest or RSpec matchers, the usage is the
187
213
  same, the only difference is the invocation.
@@ -312,7 +338,7 @@ verify that the exports tube is paused:
312
338
  For more detailed explanations and more examples make sure to check out the
313
339
  docs, expectations, and respective tests:
314
340
 
315
- - [docs](http://rubydoc.info/gems/bean_counter/0.0.4/frames)
341
+ - [docs](http://rubydoc.info/gems/bean_counter/0.1.0/frames)
316
342
  - [enqueued_expectation](https://github.com/gemeraldbeanstalk/bean_counter/tree/master/lib/bean_counter/enqueued_expectation.rb)
317
343
  - [tube_expectation](https://github.com/gemeraldbeanstalk/bean_counter/tree/master/lib/bean_counter/tube_expectation.rb)
318
344
  - [test_assertions](https://github.com/gemeraldbeanstalk/bean_counter/tree/master/lib/bean_counter/test_assertions.rb)
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_dependency 'beaneater'
22
22
  spec.add_dependency 'stalk_climber', '>= 0.1.0'
23
+ spec.add_dependency 'gemerald_beanstalk', '>= 0.0.1'
23
24
 
24
25
  spec.add_development_dependency 'bundler', '~> 1.3'
25
26
  spec.add_development_dependency 'rake'
@@ -4,5 +4,6 @@ require 'bean_counter/version'
4
4
  require 'bean_counter/core'
5
5
  require 'bean_counter/strategy'
6
6
  require 'bean_counter/strategies/stalk_climber_strategy'
7
+ require 'bean_counter/strategies/gemerald_beanstalk_strategy'
7
8
  require 'bean_counter/enqueued_expectation'
8
9
  require 'bean_counter/tube_expectation'
@@ -0,0 +1,3 @@
1
+ require 'bean_counter/strategies/gemerald_beanstalk_strategy/strategy'
2
+ require 'bean_counter/strategies/gemerald_beanstalk_strategy/tube'
3
+ require 'bean_counter/strategies/gemerald_beanstalk_strategy/job'
@@ -0,0 +1,73 @@
1
+ require 'forwardable'
2
+
3
+ class BeanCounter::Strategy::GemeraldBeanstalkStrategy::Job
4
+
5
+ extend Forwardable
6
+
7
+ def_delegators :@gemerald_job, :body, :stats
8
+
9
+ attr_reader :connection
10
+
11
+ # Attributes that should be retrieved via the gemerald job's stats. This
12
+ # list is used to dynamically create the appropriate accessor methods
13
+ STATS_METHODS = %w[
14
+ age buries delay id kicks pri releases reserves
15
+ state time-left timeouts ttr tube
16
+ ]
17
+
18
+ # Simple accessors allowing direct access to job stats attributes
19
+ STATS_METHODS.each do |stat_method|
20
+ define_method stat_method.gsub(/-/, '_') do
21
+ return @gemerald_job.stats[stat_method]
22
+ end
23
+ end
24
+
25
+
26
+ # Attempts to delete the job. Returns true if deletion succeeds or if job
27
+ # does not exist. Returns false if job could not be deleted (typically due
28
+ # to it being reserved by another connection).
29
+ #
30
+ # @return [Boolean] If the given job was successfully deleted or does not
31
+ # exist, returns true. Otherwise returns false.
32
+ def delete
33
+ response = connection.transmit("delete #{id}")
34
+ if response == "DELETED\r\n" || !exists?
35
+ return true
36
+ else
37
+ return false
38
+ end
39
+ end
40
+
41
+
42
+ # Returns a Boolean indicating whether or not the job still exists.
43
+ # @return [Boolean] If job state is deleted or the job does not exist
44
+ # on the beanstalk server, returns false. If job state is not deleted,
45
+ # returns true if the job exists on the beanstalk server.
46
+ def exists?
47
+ return false if state == 'deleted'
48
+ return connection.transmit("stats-job #{id}") != "NOT_FOUND\r\n"
49
+ end
50
+
51
+
52
+ # Initialize a new GemeraldBeanstalkStrategy::Job wrapping the provided
53
+ # `gemerald_job` in the context of the provided `connection`.
54
+ def initialize(gemerald_job, connection)
55
+ @gemerald_job = gemerald_job
56
+ @connection = connection
57
+ end
58
+
59
+
60
+ # Augment job stats to provide a Hash representation of the job.
61
+ # @return [Hash] Hash representation of the job
62
+ def to_hash
63
+ stats_pairs = stats.to_a
64
+ stats_pairs << ['body', body]
65
+ stats_pairs << ['connection', connection]
66
+
67
+ hash = Hash[stats_pairs.sort_by!(&:first)]
68
+ hash.delete('file')
69
+
70
+ return hash
71
+ end
72
+
73
+ end
@@ -0,0 +1,251 @@
1
+ require 'gemerald_beanstalk'
2
+ require 'gemerald_beanstalk/plugins/introspection'
3
+ require 'gemerald_beanstalk/plugins/direct_connection'
4
+ require 'forwardable'
5
+
6
+ class BeanCounter::Strategy::GemeraldBeanstalkStrategy < BeanCounter::Strategy
7
+
8
+ # Regex for checking for valid V4 IP addresses
9
+ V4_IP_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?::\d+)?$/
10
+
11
+ extend Forwardable
12
+
13
+ def_delegator :job_enumerator, :each, :jobs
14
+ def_delegator :tube_enumerator, :each, :tubes
15
+
16
+
17
+ # Collects all jobs enqueued during the execution of the provided `block`.
18
+ # Returns an Array of GemeraldBeanstalkStrategy::Job.
19
+ #
20
+ # Fulfills {BeanCounter::Strategy#collect_new_jobs} contract.
21
+ #
22
+ # @see BeanCounter::Strategy#collect_new_jobs
23
+ # @see BeanCounter::Strategy::GemeraldBeanstalkStrategy::Job
24
+ # @yield Nothing is yielded to the provided `block`
25
+ # @raise [ArgumentError] if a block is not provided.
26
+ # @return [Array<GemeraldBeanstalkStrategy::Job>] all jobs enqueued during the
27
+ # execution of the provided `block`
28
+ def collect_new_jobs
29
+ raise ArgumentError, 'Block required' unless block_given?
30
+
31
+ max_id_pairs = beanstalks.map do |beanstalk|
32
+ [beanstalk, beanstalk.jobs[-1] ? beanstalk.jobs[-1].id : 0]
33
+ end
34
+ max_ids = Hash[max_id_pairs]
35
+
36
+ yield
37
+
38
+ new_jobs = []
39
+ # Jobs are collected in reverse, so iterate beanstalks in reverse too
40
+ beanstalks.reverse.each do |beanstalk|
41
+ jobs = beanstalk.jobs
42
+ index = -1
43
+ while (job = jobs[index]) && job.id > max_ids[beanstalk] do
44
+ new_jobs << strategy_job(job)
45
+ index -= 1
46
+ end
47
+ end
48
+ # Flip new jobs to maintain beanstalk, id asc order
49
+ return new_jobs.reverse
50
+ end
51
+
52
+
53
+ # Attempts to delete the provided GemeraldBeanstalkStrategy::Job `job`.
54
+ # Returns true if deletion succeeds or if `job` does not exist. Returns false
55
+ # if `job` could not be deleted (typically due to it being reserved by
56
+ # another connection).
57
+ #
58
+ # Fulfills {BeanCounter::Strategy#delete_job} contract.
59
+ #
60
+ # @see BeanCounter::Strategy#delete_job
61
+ # @param job [GemeraldBeanstalkStrategy::Job] the job to be deleted
62
+ # @return [Boolean] If the given job was successfully deleted or does not
63
+ # exist, returns true. Otherwise returns false.
64
+ def delete_job(job)
65
+ return job.delete
66
+ end
67
+
68
+
69
+ # Initialize a new GemeraldBeanstalkStrategy. This includes initializing
70
+ # GemeraldBeanstalk::Servers for each of the beanstalk_urls visible to
71
+ # BeanCounter and also spawning direct connections to those servers.
72
+ def initialize
73
+ @clients = {}
74
+ @beanstalk_servers = []
75
+ BeanCounter.beanstalkd_url.each do |url|
76
+ server = GemeraldBeanstalk::Server.new(*parse_url(url))
77
+ @clients[server.beanstalk] = server.beanstalk.direct_connection_client
78
+ @beanstalk_servers << server
79
+ end
80
+ end
81
+
82
+
83
+ # Returns a Boolean indicating whether or not the provided
84
+ # GemeraldBeanstalkStrategy::Job `job` matches the given Hash of `options`.
85
+ #
86
+ # See {MATCHABLE_JOB_ATTRIBUTES} for a list of
87
+ # attributes that can be used when matching.
88
+ #
89
+ # Fulfills {BeanCounter::Strategy#job_matches?} contract.
90
+ #
91
+ # @see MATCHABLE_JOB_ATTRIBUTES
92
+ # @see BeanCounter::Strategy#job_matches?
93
+ # @param job [GemeraldBeanstalkStrategy::Job] the job to evaluate for a matche.
94
+ # @param options
95
+ # [Hash{String, Symbol => Numeric, Proc, Range, Regexp, String, Symbol}]
96
+ # Options to be used to evaluate a match.
97
+ # @return [Boolean] If job exists and matches the provided options, returns
98
+ # true. Otherwise, returns false.
99
+ def job_matches?(job, options = {})
100
+ return matcher(MATCHABLE_JOB_ATTRIBUTES, job, options)
101
+ end
102
+
103
+
104
+ # Returns a String representation of the GemeraldBeanstalkStrategy::Job `job`
105
+ # in a pretty, human readable format.
106
+ #
107
+ # Fulfills {BeanCounter::Strategy#pretty_print_job} contract.
108
+ #
109
+ # @see BeanCounter::Strategy#pretty_print_job
110
+ # @param job [GemeraldBeanstalkStrategy::Job] the job to print in a more
111
+ # readable format.
112
+ # @return [String] A more human-readable representation of `job`.
113
+ def pretty_print_job(job)
114
+ hash = job.to_hash
115
+ hash.delete('connection')
116
+ return hash.to_s
117
+ end
118
+
119
+
120
+ # Returns a String representation of `tube` in a pretty, human readable format.
121
+ #
122
+ # Fulfills {BeanCounter::Strategy#pretty_print_tube} contract.
123
+ #
124
+ # @see BeanCounter::Strategy#pretty_print_tube
125
+ # @param tube [GemeraldBeanstalkStrategy::Tube] the tube to print in a more
126
+ # readable format.
127
+ # @return [String] A more human-readable representation of `tube`.
128
+ def pretty_print_tube(tube)
129
+ return tube.to_hash.to_s
130
+ end
131
+
132
+
133
+ # Returns a boolean indicating whether or not the provided
134
+ # GemeraldBeanstalkStrategy::Tube, `tube`, matches the given Hash of `options`.
135
+ #
136
+ # See {MATCHABLE_TUBE_ATTRIBUTES} for a list of attributes that can be used
137
+ # when evaluating a match.
138
+ #
139
+ # Fulfills {BeanCounter::Strategy#tube_matches?} contract.
140
+ #
141
+ # @see BeanCounter::Strategy#tube_matches?
142
+ # @param tube [GemeraldBeanstalkStrategy::Tube] the tube to evaluate a match
143
+ # against.
144
+ # @param options
145
+ # [Hash{String, Symbol => Numeric, Proc, Range, Regexp, String, Symbol}]
146
+ # a Hash of options to use when evaluating a match.
147
+ # @return [Boolean] If `tube` exists and matches against the provided options,
148
+ # returns true. Otherwise returns false.
149
+ def tube_matches?(tube, options = {})
150
+ return false if tube.nil?
151
+ return matcher(MATCHABLE_TUBE_ATTRIBUTES, tube, options)
152
+ end
153
+
154
+ private
155
+
156
+ # The collection of GemeraldBeanstalk servers the strategy built during
157
+ # initialization
158
+ attr_reader :beanstalk_servers
159
+
160
+
161
+ # The collection of beanstalks belonginging to the GemeraldBeanstalk servers
162
+ # that were created during initialization.
163
+ #
164
+ # @return [Array<GemeraldBeanstalk::Beanstalk>] the Beanstalks of the
165
+ # GemeraldBeanstalk::Servers
166
+ def beanstalks
167
+ return @beanstalks ||= @beanstalk_servers.map(&:beanstalk)
168
+ end
169
+
170
+
171
+ # Returns an enumerator that enumerates all jobs on all beanstalk servers in
172
+ # the order the beanstalk servers are defined in ascending order. Jobs are
173
+ # returned as GemeraldBeanstalkStrategy::Job.
174
+ def job_enumerator
175
+ return Enumerator.new do |yielder|
176
+ beanstalks.each do |beanstalk|
177
+ beanstalk.jobs.each do |job|
178
+ yielder << strategy_job(job)
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+
185
+ # Generic match evaluator that compares the Hash of `options` given to the
186
+ # Strategy representation of the given `matchable`.
187
+ def matcher(valid_attributes, matchable, options = {})
188
+ return false unless matchable.exists?
189
+ return (options.keys & valid_attributes).all? do |key|
190
+ options[key] === matchable.send(key.to_s.gsub(/-/, '_'))
191
+ end
192
+ end
193
+
194
+
195
+ # Parses a variety of forms of beanstalk URL and returns a pair including the
196
+ # hostname and possibly a port given by the url.
197
+ def parse_url(url)
198
+ unless V4_IP_REGEX === url
199
+ uri = URI.parse(url)
200
+ if uri.scheme && uri.host
201
+ raise(ArgumentError, "Invalid beanstalk URI: #{url}") unless uri.scheme == 'beanstalk'
202
+ host = uri.host
203
+ port = uri.port
204
+ end
205
+ end
206
+ unless host
207
+ match = url.split(/:/)
208
+ host = match[0]
209
+ port = match[1]
210
+ end
211
+ return port ? [host, Integer(port)] : [host]
212
+ end
213
+
214
+
215
+ # Helper method to transform a GemeraldBeanstalk::Job into a
216
+ # BeanCounter::Strategy::GemeraldBeanstalkStrategy::Job. The strategy
217
+ # specific job class is intended to hide native job implementation interface
218
+ # specifics and prevent meddling with native Job internals.
219
+ def strategy_job(gemerald_job)
220
+ return BeanCounter::Strategy::GemeraldBeanstalkStrategy::Job.new(gemerald_job, @clients[gemerald_job.beanstalk])
221
+ end
222
+
223
+
224
+ # Helper method to transform a `tube_name` into a
225
+ # BeanCounter::Strategy::GemeraldBeanstalkStrategy::Tube. The strategy
226
+ # specific tube class is intended to hide native tube implementation interface
227
+ # specifics and prevent meddling with native Tube internals. Also handles
228
+ # merging of tube data from multiple servers, as such, requires access to
229
+ # beanstalk servers.
230
+ def strategy_tube(tube_name)
231
+ return BeanCounter::Strategy::GemeraldBeanstalkStrategy::Tube.new(tube_name, beanstalks)
232
+ end
233
+
234
+
235
+ # Returns an enumerator that enumerates all tubes on all beanstalk servers.
236
+ # Each tube is included in the enumeration only once, regardless of how many
237
+ # servers contain an instance of that tube. Tube stats are merged before they
238
+ # are yielded by the enumerator as a GemeraldBeanstalkdStrategy::Tube.
239
+ def tube_enumerator
240
+ tubes_in_pool = beanstalks.inject([]) do |memo, beanstalk|
241
+ memo.concat(beanstalk.tubes.keys)
242
+ end
243
+ tubes_in_pool.uniq!
244
+ return Enumerator.new do |yielder|
245
+ tubes_in_pool.each do |tube_name|
246
+ yielder << strategy_tube(tube_name)
247
+ end
248
+ end
249
+ end
250
+
251
+ end
@@ -0,0 +1,56 @@
1
+ class BeanCounter::Strategy::GemeraldBeanstalkStrategy::Tube
2
+
3
+ # The name of the tube represented by the object
4
+ attr_reader :name
5
+
6
+ # Attributes that should be obtained via the tube's stats. This list is used
7
+ # to dynamically create the attribute accessor methods.
8
+ STATS_METHODS = %w[
9
+ cmd-delete cmd-pause-tube current-jobs-buried current-jobs-delayed
10
+ current-jobs-ready current-jobs-reserved current-jobs-urgent current-using
11
+ current-waiting current-watching pause pause-time-left total-jobs
12
+ ]
13
+
14
+ STATS_METHODS.each do |attr_method|
15
+ define_method attr_method.gsub(/-/, '_') do
16
+ return to_hash[attr_method]
17
+ end
18
+ end
19
+
20
+ # Returns a Boolean indicating whether or not the tube exists in the pool.
21
+ # @return [Boolean] Returns true if tube exists on any of the servers in the
22
+ # pool, otherwise returns false.
23
+ def exists?
24
+ tubes_in_pool = @beanstalks.inject([]) do |memo, beanstalk|
25
+ memo.concat(beanstalk.tubes.keys)
26
+ end
27
+ tubes_in_pool.uniq!
28
+ return tubes_in_pool.include?(@name)
29
+ end
30
+
31
+
32
+ # Initialize a new GemeraldBeanstalkStrategy::Tube representing all tubes
33
+ # named `tube_name` in the given pool of `beanstalks`.
34
+ def initialize(tube_name, beanstalks)
35
+ @name = tube_name
36
+ @beanstalks = beanstalks
37
+ end
38
+
39
+
40
+ # Retrieves stats for the given `tube_name` from all known servers and merges
41
+ # numeric stats. Matches Beaneater's treatment of a tube as a collective
42
+ # entity and not a per-server entity.
43
+ def to_hash
44
+ return @beanstalks.inject({}) do |hash, beanstalk|
45
+ next hash if (tube = beanstalk.tubes[name]).nil?
46
+ next tube.stats if hash.empty?
47
+
48
+ tube.stats.each do |stat, value|
49
+ next unless value.is_a?(Numeric)
50
+ hash[stat] += value
51
+ end
52
+ hash
53
+ end
54
+ end
55
+
56
+ end