bean_counter 0.0.4 → 0.1.0

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